mirror of https://github.com/portainer/portainer
fix(app): various persisted folder fixes [EE-6235] (#10963)
Co-authored-by: testa113 <testa113>pull/10964/head
parent
7a04d1d4ea
commit
95474b7dc5
|
@ -278,14 +278,13 @@
|
||||||
></persisted-folders-form-section>
|
></persisted-folders-form-section>
|
||||||
|
|
||||||
<!-- #region DATA ACCESS POLICY -->
|
<!-- #region DATA ACCESS POLICY -->
|
||||||
<div ng-if="ctrl.showDataAccessPolicySection()">
|
|
||||||
<access-policy-form-section
|
<access-policy-form-section
|
||||||
|
ng-if="ctrl.showDataAccessPolicySection()"
|
||||||
value="ctrl.formValues.DataAccessPolicy"
|
value="ctrl.formValues.DataAccessPolicy"
|
||||||
on-change="(ctrl.onDataAccessPolicyChange)"
|
on-change="(ctrl.onDataAccessPolicyChange)"
|
||||||
is-edit="ctrl.state.isEdit"
|
is-edit="ctrl.state.isEdit"
|
||||||
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
|
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
|
||||||
></access-policy-form-section>
|
></access-policy-form-section>
|
||||||
</div>
|
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<resource-reservation-form-section
|
<resource-reservation-form-section
|
||||||
|
|
|
@ -156,6 +156,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.showDataAccessPolicySection = this.showDataAccessPolicySection.bind(this);
|
this.showDataAccessPolicySection = this.showDataAccessPolicySection.bind(this);
|
||||||
this.refreshReactComponent = this.refreshReactComponent.bind(this);
|
this.refreshReactComponent = this.refreshReactComponent.bind(this);
|
||||||
this.onChangeNamespaceName = this.onChangeNamespaceName.bind(this);
|
this.onChangeNamespaceName = this.onChangeNamespaceName.bind(this);
|
||||||
|
this.canSupportSharedAccess = this.canSupportSharedAccess.bind(this);
|
||||||
|
|
||||||
this.$scope.$watch(
|
this.$scope.$watch(
|
||||||
() => this.formValues,
|
() => this.formValues,
|
||||||
|
@ -209,7 +210,7 @@ class KubernetesCreateApplicationController {
|
||||||
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Global) {
|
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Global) {
|
||||||
return this.ApplicationTypes.DaemonSet;
|
return this.ApplicationTypes.DaemonSet;
|
||||||
}
|
}
|
||||||
if (this.formValues.PersistedFolders && this.formValues.PersistedFolders.length) {
|
if (this.formValues.PersistedFolders && this.formValues.PersistedFolders.length && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.Isolated) {
|
||||||
return this.ApplicationTypes.StatefulSet;
|
return this.ApplicationTypes.StatefulSet;
|
||||||
}
|
}
|
||||||
return this.ApplicationTypes.Deployment;
|
return this.ApplicationTypes.Deployment;
|
||||||
|
@ -365,7 +366,6 @@ class KubernetesCreateApplicationController {
|
||||||
this.formValues.PersistedFolders = values;
|
this.formValues.PersistedFolders = values;
|
||||||
if (values && values.length && !this.supportGlobalDeployment()) {
|
if (values && values.length && !this.supportGlobalDeployment()) {
|
||||||
this.onChangeDeploymentType(this.ApplicationDeploymentTypes.Replicated);
|
this.onChangeDeploymentType(this.ApplicationDeploymentTypes.Replicated);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this.updateApplicationType();
|
this.updateApplicationType();
|
||||||
});
|
});
|
||||||
|
@ -442,6 +442,13 @@ class KubernetesCreateApplicationController {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// from the pvcs in the form values, get all selected storage classes and find if they are all support RWX
|
||||||
|
canSupportSharedAccess() {
|
||||||
|
const formStorageClasses = this.formValues.PersistedFolders.map((pf) => pf.storageClass);
|
||||||
|
const isRWXSupported = formStorageClasses.every((sc) => sc.AccessModes.includes('RWX'));
|
||||||
|
return isRWXSupported;
|
||||||
|
}
|
||||||
|
|
||||||
// A StatefulSet is defined by DataAccessPolicy === 'Isolated'
|
// A StatefulSet is defined by DataAccessPolicy === 'Isolated'
|
||||||
isEditAndStatefulSet() {
|
isEditAndStatefulSet() {
|
||||||
return this.state.isEdit && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.Isolated;
|
return this.state.isEdit && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.Isolated;
|
||||||
|
|
|
@ -59,9 +59,13 @@ function getOptions(
|
||||||
label: 'Shared',
|
label: 'Shared',
|
||||||
description:
|
description:
|
||||||
'Application will be deployed as a Deployment with a shared storage access',
|
'Application will be deployed as a Deployment with a shared storage access',
|
||||||
tooltip: () =>
|
tooltip: () => {
|
||||||
isEdit ? 'Changing the data access policy is not allowed' : '',
|
if (persistedFoldersUseExistingVolumes) {
|
||||||
disabled: () => isEdit && value !== 'Shared',
|
return 'Changing the data access policy is not allowed';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
disabled: () => persistedFoldersUseExistingVolumes,
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { Boxes, Sliders } from 'lucide-react';
|
|
||||||
|
|
||||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
|
||||||
|
|
||||||
import { DeploymentType } from '../types';
|
|
||||||
|
|
||||||
export function getDeploymentOptions(
|
|
||||||
supportGlobalDeployment: boolean
|
|
||||||
): ReadonlyArray<BoxSelectorOption<DeploymentType>> {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'deployment_replicated',
|
|
||||||
label: 'Replicated',
|
|
||||||
value: 'Replicated',
|
|
||||||
icon: Sliders,
|
|
||||||
iconType: 'badge',
|
|
||||||
description: 'Run one or multiple instances of this container',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deployment_global',
|
|
||||||
disabled: () => !supportGlobalDeployment,
|
|
||||||
tooltip: () =>
|
|
||||||
!supportGlobalDeployment
|
|
||||||
? 'The storage or access policy used for persisted folders cannot be used with this option'
|
|
||||||
: '',
|
|
||||||
label: 'Global',
|
|
||||||
description:
|
|
||||||
'Application will be deployed as a DaemonSet with an instance on each node of the cluster',
|
|
||||||
value: 'Global',
|
|
||||||
icon: Boxes,
|
|
||||||
iconType: 'badge',
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
|
import { Boxes, Sliders } from 'lucide-react';
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { BoxSelector } from '@@/BoxSelector';
|
import { BoxSelector, BoxSelectorOption } from '@@/BoxSelector';
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
import { FormError } from '@@/form-components/FormError';
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
|
||||||
import { DeploymentType } from '../../types';
|
import { DeploymentType } from '../../types';
|
||||||
import { getDeploymentOptions } from '../../CreateView/deploymentOptions';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
values: DeploymentType;
|
values: DeploymentType;
|
||||||
|
@ -21,7 +21,7 @@ export function AppDeploymentTypeFormSection({
|
||||||
errors,
|
errors,
|
||||||
supportGlobalDeployment,
|
supportGlobalDeployment,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const options = getDeploymentOptions(supportGlobalDeployment);
|
const options = getOptions(supportGlobalDeployment);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSection title="Deployment">
|
<FormSection title="Deployment">
|
||||||
|
@ -39,3 +39,32 @@ export function AppDeploymentTypeFormSection({
|
||||||
</FormSection>
|
</FormSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOptions(
|
||||||
|
supportGlobalDeployment: boolean
|
||||||
|
): ReadonlyArray<BoxSelectorOption<DeploymentType>> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'deployment_replicated',
|
||||||
|
label: 'Replicated',
|
||||||
|
value: 'Replicated',
|
||||||
|
icon: Sliders,
|
||||||
|
iconType: 'badge',
|
||||||
|
description: 'Run one or multiple instances of this container',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deployment_global',
|
||||||
|
disabled: () => !supportGlobalDeployment,
|
||||||
|
tooltip: () =>
|
||||||
|
!supportGlobalDeployment
|
||||||
|
? 'The storage or access policy used for persisted folders cannot be used with this option'
|
||||||
|
: '',
|
||||||
|
label: 'Global',
|
||||||
|
description:
|
||||||
|
'Application will be deployed as a DaemonSet with an instance on each node of the cluster',
|
||||||
|
value: 'Global',
|
||||||
|
icon: Boxes,
|
||||||
|
iconType: 'badge',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ export function ConfigurationItem({
|
||||||
<Button
|
<Button
|
||||||
color="dangerlight"
|
color="dangerlight"
|
||||||
size="medium"
|
size="medium"
|
||||||
onClick={() => onRemoveItem()}
|
onClick={onRemoveItem}
|
||||||
className="!ml-0 vertical-center btn-only-icon"
|
className="!ml-0 vertical-center btn-only-icon"
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { ApplicationFormValues } from '../../types';
|
||||||
import { ExistingVolume, PersistedFolderFormValue } from './types';
|
import { ExistingVolume, PersistedFolderFormValue } from './types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialValues: PersistedFolderFormValue[];
|
initialValues?: PersistedFolderFormValue[];
|
||||||
item: PersistedFolderFormValue;
|
item: PersistedFolderFormValue;
|
||||||
onChange: (value: PersistedFolderFormValue) => void;
|
onChange: (value: PersistedFolderFormValue) => void;
|
||||||
error: ItemError<PersistedFolderFormValue>;
|
error: ItemError<PersistedFolderFormValue>;
|
||||||
|
@ -220,7 +220,6 @@ export function PersistedFolderItem({
|
||||||
function isToggleVolumeTypeVisible() {
|
function isToggleVolumeTypeVisible() {
|
||||||
return (
|
return (
|
||||||
!(isEdit && isExistingPersistedFolder()) && // if it's not an edit of an existing persisted folder
|
!(isEdit && isExistingPersistedFolder()) && // if it's not an edit of an existing persisted folder
|
||||||
applicationValues.ApplicationType !== 'StatefulSet' && // and if it's not a statefulset
|
|
||||||
applicationValues.Containers.length <= 1 // and if there is only one container);
|
applicationValues.Containers.length <= 1 // and if there is only one container);
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,10 @@ export function PersistedFoldersFormSection({
|
||||||
value={values}
|
value={values}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
isDeleteButtonHidden={isDeleteButtonHidden()}
|
isDeleteButtonHidden={
|
||||||
|
isEdit && applicationValues.ApplicationType === 'StatefulSet'
|
||||||
|
}
|
||||||
|
canUndoDelete={isEdit}
|
||||||
deleteButtonDataCy="k8sAppCreate-persistentFolderRemoveButton"
|
deleteButtonDataCy="k8sAppCreate-persistentFolderRemoveButton"
|
||||||
addButtonDataCy="k8sAppCreate-persistentFolderAddButton"
|
addButtonDataCy="k8sAppCreate-persistentFolderAddButton"
|
||||||
disabled={storageClasses.length === 0}
|
disabled={storageClasses.length === 0}
|
||||||
|
@ -77,12 +80,8 @@ export function PersistedFoldersFormSection({
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
itemBuilder={() => {
|
itemBuilder={() => ({
|
||||||
const newVolumeClaimName = `${applicationValues.Name}-${uuidv4()}`;
|
persistentVolumeClaimName: getNewPVCName(applicationValues.Name),
|
||||||
return {
|
|
||||||
persistentVolumeClaimName:
|
|
||||||
availableVolumes[0]?.PersistentVolumeClaim.Name ||
|
|
||||||
newVolumeClaimName,
|
|
||||||
containerPath: '',
|
containerPath: '',
|
||||||
size: '',
|
size: '',
|
||||||
sizeUnit: 'GB',
|
sizeUnit: 'GB',
|
||||||
|
@ -90,20 +89,11 @@ export function PersistedFoldersFormSection({
|
||||||
useNewVolume: true,
|
useNewVolume: true,
|
||||||
existingVolume: undefined,
|
existingVolume: undefined,
|
||||||
needsDeletion: false,
|
needsDeletion: false,
|
||||||
};
|
})}
|
||||||
}}
|
|
||||||
addLabel="Add persisted folder"
|
addLabel="Add persisted folder"
|
||||||
canUndoDelete={isEdit}
|
|
||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
);
|
);
|
||||||
|
|
||||||
function isDeleteButtonHidden() {
|
|
||||||
return (
|
|
||||||
(isEdit && applicationValues.ApplicationType === 'StatefulSet') ||
|
|
||||||
applicationValues.Containers.length >= 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePVCOptions(existingPVCs: ExistingVolume[]): Option<string>[] {
|
function usePVCOptions(existingPVCs: ExistingVolume[]): Option<string>[] {
|
||||||
|
@ -123,3 +113,10 @@ function getAddButtonError(storageClasses: StorageClass[]) {
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNewPVCName(applicationName: string) {
|
||||||
|
const name = `${applicationName}-${uuidv4()}`;
|
||||||
|
// limit it to 63 characters to avoid exceeding the limit for the volume name
|
||||||
|
const nameLimited = name.length > 63 ? name.substring(0, 63) : name;
|
||||||
|
return nameLimited;
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,12 @@ export function replicationValidation(
|
||||||
.test(
|
.test(
|
||||||
'scalable',
|
'scalable',
|
||||||
`The following storage option(s) do not support concurrent access from multiples instances: ${nonScalableStorage}. You will not be able to scale that application.`,
|
`The following storage option(s) do not support concurrent access from multiples instances: ${nonScalableStorage}. You will not be able to scale that application.`,
|
||||||
() => !!supportScalableReplicaDeployment // must have support scalable replica deployment
|
(value) => {
|
||||||
|
if (!value || value <= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!supportScalableReplicaDeployment;
|
||||||
|
} // must have support scalable replica deployment
|
||||||
)
|
)
|
||||||
.required('Instance count is required.'),
|
.required('Instance count is required.'),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue