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>
|
||||
|
||||
<!-- #region DATA ACCESS POLICY -->
|
||||
<div ng-if="ctrl.showDataAccessPolicySection()">
|
||||
<access-policy-form-section
|
||||
value="ctrl.formValues.DataAccessPolicy"
|
||||
on-change="(ctrl.onDataAccessPolicyChange)"
|
||||
is-edit="ctrl.state.isEdit"
|
||||
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
|
||||
></access-policy-form-section>
|
||||
</div>
|
||||
<access-policy-form-section
|
||||
ng-if="ctrl.showDataAccessPolicySection()"
|
||||
value="ctrl.formValues.DataAccessPolicy"
|
||||
on-change="(ctrl.onDataAccessPolicyChange)"
|
||||
is-edit="ctrl.state.isEdit"
|
||||
persisted-folders-use-existing-volumes="ctrl.state.persistedFoldersUseExistingVolumes"
|
||||
></access-policy-form-section>
|
||||
<!-- #endregion -->
|
||||
|
||||
<resource-reservation-form-section
|
||||
|
|
|
@ -156,6 +156,7 @@ class KubernetesCreateApplicationController {
|
|||
this.showDataAccessPolicySection = this.showDataAccessPolicySection.bind(this);
|
||||
this.refreshReactComponent = this.refreshReactComponent.bind(this);
|
||||
this.onChangeNamespaceName = this.onChangeNamespaceName.bind(this);
|
||||
this.canSupportSharedAccess = this.canSupportSharedAccess.bind(this);
|
||||
|
||||
this.$scope.$watch(
|
||||
() => this.formValues,
|
||||
|
@ -209,7 +210,7 @@ class KubernetesCreateApplicationController {
|
|||
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.Global) {
|
||||
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.Deployment;
|
||||
|
@ -365,7 +366,6 @@ class KubernetesCreateApplicationController {
|
|||
this.formValues.PersistedFolders = values;
|
||||
if (values && values.length && !this.supportGlobalDeployment()) {
|
||||
this.onChangeDeploymentType(this.ApplicationDeploymentTypes.Replicated);
|
||||
return;
|
||||
}
|
||||
this.updateApplicationType();
|
||||
});
|
||||
|
@ -442,6 +442,13 @@ class KubernetesCreateApplicationController {
|
|||
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'
|
||||
isEditAndStatefulSet() {
|
||||
return this.state.isEdit && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.Isolated;
|
||||
|
|
|
@ -59,9 +59,13 @@ function getOptions(
|
|||
label: 'Shared',
|
||||
description:
|
||||
'Application will be deployed as a Deployment with a shared storage access',
|
||||
tooltip: () =>
|
||||
isEdit ? 'Changing the data access policy is not allowed' : '',
|
||||
disabled: () => isEdit && value !== 'Shared',
|
||||
tooltip: () => {
|
||||
if (persistedFoldersUseExistingVolumes) {
|
||||
return 'Changing the data access policy is not allowed';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
disabled: () => persistedFoldersUseExistingVolumes,
|
||||
},
|
||||
] 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 { BoxSelector } from '@@/BoxSelector';
|
||||
import { BoxSelector, BoxSelectorOption } from '@@/BoxSelector';
|
||||
import { FormSection } from '@@/form-components/FormSection';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
|
||||
import { DeploymentType } from '../../types';
|
||||
import { getDeploymentOptions } from '../../CreateView/deploymentOptions';
|
||||
|
||||
interface Props {
|
||||
values: DeploymentType;
|
||||
|
@ -21,7 +21,7 @@ export function AppDeploymentTypeFormSection({
|
|||
errors,
|
||||
supportGlobalDeployment,
|
||||
}: Props) {
|
||||
const options = getDeploymentOptions(supportGlobalDeployment);
|
||||
const options = getOptions(supportGlobalDeployment);
|
||||
|
||||
return (
|
||||
<FormSection title="Deployment">
|
||||
|
@ -39,3 +39,32 @@ export function AppDeploymentTypeFormSection({
|
|||
</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
|
||||
color="dangerlight"
|
||||
size="medium"
|
||||
onClick={() => onRemoveItem()}
|
||||
onClick={onRemoveItem}
|
||||
className="!ml-0 vertical-center btn-only-icon"
|
||||
icon={Trash2}
|
||||
/>
|
||||
|
|
|
@ -16,7 +16,7 @@ import { ApplicationFormValues } from '../../types';
|
|||
import { ExistingVolume, PersistedFolderFormValue } from './types';
|
||||
|
||||
type Props = {
|
||||
initialValues: PersistedFolderFormValue[];
|
||||
initialValues?: PersistedFolderFormValue[];
|
||||
item: PersistedFolderFormValue;
|
||||
onChange: (value: PersistedFolderFormValue) => void;
|
||||
error: ItemError<PersistedFolderFormValue>;
|
||||
|
@ -220,7 +220,6 @@ export function PersistedFolderItem({
|
|||
function isToggleVolumeTypeVisible() {
|
||||
return (
|
||||
!(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);
|
||||
);
|
||||
}
|
||||
|
|
|
@ -57,7 +57,10 @@ export function PersistedFoldersFormSection({
|
|||
value={values}
|
||||
onChange={onChange}
|
||||
errors={errors}
|
||||
isDeleteButtonHidden={isDeleteButtonHidden()}
|
||||
isDeleteButtonHidden={
|
||||
isEdit && applicationValues.ApplicationType === 'StatefulSet'
|
||||
}
|
||||
canUndoDelete={isEdit}
|
||||
deleteButtonDataCy="k8sAppCreate-persistentFolderRemoveButton"
|
||||
addButtonDataCy="k8sAppCreate-persistentFolderAddButton"
|
||||
disabled={storageClasses.length === 0}
|
||||
|
@ -77,33 +80,20 @@ export function PersistedFoldersFormSection({
|
|||
initialValues={initialValues}
|
||||
/>
|
||||
)}
|
||||
itemBuilder={() => {
|
||||
const newVolumeClaimName = `${applicationValues.Name}-${uuidv4()}`;
|
||||
return {
|
||||
persistentVolumeClaimName:
|
||||
availableVolumes[0]?.PersistentVolumeClaim.Name ||
|
||||
newVolumeClaimName,
|
||||
containerPath: '',
|
||||
size: '',
|
||||
sizeUnit: 'GB',
|
||||
storageClass: storageClasses[0],
|
||||
useNewVolume: true,
|
||||
existingVolume: undefined,
|
||||
needsDeletion: false,
|
||||
};
|
||||
}}
|
||||
itemBuilder={() => ({
|
||||
persistentVolumeClaimName: getNewPVCName(applicationValues.Name),
|
||||
containerPath: '',
|
||||
size: '',
|
||||
sizeUnit: 'GB',
|
||||
storageClass: storageClasses[0],
|
||||
useNewVolume: true,
|
||||
existingVolume: undefined,
|
||||
needsDeletion: false,
|
||||
})}
|
||||
addLabel="Add persisted folder"
|
||||
canUndoDelete={isEdit}
|
||||
/>
|
||||
</FormSection>
|
||||
);
|
||||
|
||||
function isDeleteButtonHidden() {
|
||||
return (
|
||||
(isEdit && applicationValues.ApplicationType === 'StatefulSet') ||
|
||||
applicationValues.Containers.length >= 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function usePVCOptions(existingPVCs: ExistingVolume[]): Option<string>[] {
|
||||
|
@ -123,3 +113,10 @@ function getAddButtonError(storageClasses: StorageClass[]) {
|
|||
}
|
||||
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(
|
||||
'scalable',
|
||||
`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.'),
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue