diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts index 97329f898..0f44e3c66 100644 --- a/app/kubernetes/react/components/index.ts +++ b/app/kubernetes/react/components/index.ts @@ -34,6 +34,10 @@ import { ResourceReservationFormSection, resourceReservationValidation, } from '@/react/kubernetes/applications/components/ResourceReservationFormSection'; +import { + ReplicationFormSection, + replicationValidation, +} from '@/react/kubernetes/applications/components/ReplicationFormSection'; import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset'; @@ -238,3 +242,16 @@ withFormValidation( ], resourceReservationValidation ); + +withFormValidation( + ngModule, + withUIRouter(withCurrentUser(withReactQuery(ReplicationFormSection))), + 'replicationFormSection', + [ + 'supportScalableReplicaDeployment', + 'cpuLimit', + 'memoryLimit', + 'resourceReservationsOverflow', + ], + replicationValidation +); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index b0d0ce83f..d9320c6f2 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -451,71 +451,17 @@ > -
- -
- -
-
- -

Instance count is required.

-

Instance count must be greater than 0.

-
-
-
-
-
- - -
-
- -
- This application will reserve the following resources: - {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and - {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. -
-
-
- -
-
- - This application would exceed available resources. Please review resource reservations or the instance count. -
-
- -
-
- - This application would exceed available storage. Please review the persisted folders or the instance count. -
-
- -
-
- -
- The following storage option(s) do not support concurrent access from multiples instances: - {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. -
-
+
+
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 2d0dabe44..6a0abfac9 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -155,6 +155,7 @@ class KubernetesCreateApplicationController { this.onSecretsChange = this.onSecretsChange.bind(this); this.onChangePersistedFolder = this.onChangePersistedFolder.bind(this); this.onChangeResourceReservation = this.onChangeResourceReservation.bind(this); + this.onChangeReplicaCount = this.onChangeReplicaCount.bind(this); } /* #endregion */ @@ -517,6 +518,12 @@ class KubernetesCreateApplicationController { return false; } + onChangeReplicaCount(values) { + return this.$async(async () => { + this.formValues.ReplicaCount = values.replicaCount; + }); + } + // For each persisted folders, returns the non scalable deployments options (storage class that only supports RWO) getNonScalableStorage() { let storageOptions = []; diff --git a/app/react/docker/containers/CreateView/ResourcesTab/memory-utils.ts b/app/react/docker/containers/CreateView/ResourcesTab/memory-utils.ts index 6bb545e42..f6a5f6c04 100644 --- a/app/react/docker/containers/CreateView/ResourcesTab/memory-utils.ts +++ b/app/react/docker/containers/CreateView/ResourcesTab/memory-utils.ts @@ -14,7 +14,7 @@ export function toViewModelMemory(value = 0): number { return value / 1024 / 1024; } -function round(value: number, decimals: number) { +export function round(value: number, decimals: number) { const tenth = 10 ** decimals; return Math.round((value + Number.EPSILON) * tenth) / tenth; } diff --git a/app/react/kubernetes/applications/components/ReplicationFormSection/ReplicationFormSection.tsx b/app/react/kubernetes/applications/components/ReplicationFormSection/ReplicationFormSection.tsx new file mode 100644 index 000000000..c7ee59946 --- /dev/null +++ b/app/react/kubernetes/applications/components/ReplicationFormSection/ReplicationFormSection.tsx @@ -0,0 +1,60 @@ +import { FormikErrors } from 'formik'; +import { round } from 'lodash'; + +import { FormControl } from '@@/form-components/FormControl'; +import { Input } from '@@/form-components/Input'; +import { TextTip } from '@@/Tip/TextTip'; +import { FormError } from '@@/form-components/FormError'; + +import { ReplicaCountFormValues } from './types'; + +type Props = { + values: ReplicaCountFormValues; + onChange: (values: ReplicaCountFormValues) => void; + errors: FormikErrors; + cpuLimit: number; + memoryLimit: number; + resourceReservationsOverflow: boolean; + supportScalableReplicaDeployment: boolean; +}; + +export function ReplicationFormSection({ + values, + onChange, + errors, + supportScalableReplicaDeployment, + cpuLimit, + memoryLimit, + resourceReservationsOverflow, +}: Props) { + const hasResourceLimit = cpuLimit !== 0 || memoryLimit !== 0; + + return ( + <> + + + onChange({ replicaCount: e.target.valueAsNumber || 1 }) + } + className="w-1/4" + data-cy="k8sAppCreate-replicaCountInput" + /> + + {!resourceReservationsOverflow && + values.replicaCount > 1 && + hasResourceLimit && ( + + This application will reserve the following resources:{' '} + {round(cpuLimit * values.replicaCount, 2)} CPU and{' '} + {memoryLimit * values.replicaCount} MB of memory. + + )} + {errors?.replicaCount && {errors.replicaCount}} + + ); +} diff --git a/app/react/kubernetes/applications/components/ReplicationFormSection/index.ts b/app/react/kubernetes/applications/components/ReplicationFormSection/index.ts new file mode 100644 index 000000000..318f6a0b7 --- /dev/null +++ b/app/react/kubernetes/applications/components/ReplicationFormSection/index.ts @@ -0,0 +1,2 @@ +export { ReplicationFormSection } from './ReplicationFormSection'; +export { replicationValidation } from './replicationValidation'; diff --git a/app/react/kubernetes/applications/components/ReplicationFormSection/replicationValidation.ts b/app/react/kubernetes/applications/components/ReplicationFormSection/replicationValidation.ts new file mode 100644 index 000000000..9dcbadcab --- /dev/null +++ b/app/react/kubernetes/applications/components/ReplicationFormSection/replicationValidation.ts @@ -0,0 +1,41 @@ +import { SchemaOf, number, object } from 'yup'; + +import { ReplicaCountFormValues } from './types'; + +type ValidationData = { + resourceReservationsOverflow: boolean; + quotaExceeded: boolean; + nonScalableStorage: string; + supportScalableReplicaDeployment: boolean; +}; + +export function replicationValidation( + validationData?: ValidationData +): SchemaOf { + const { + resourceReservationsOverflow, + quotaExceeded, + nonScalableStorage, + supportScalableReplicaDeployment, + } = validationData || {}; + return object({ + replicaCount: number() + .min(1, 'Instance count must be greater than 0.') + .test( + 'overflow', + 'This application would exceed available resources. Please review resource reservations or the instance count.', + () => !resourceReservationsOverflow // must not have resource reservations overflow + ) + .test( + 'quota', + 'This application would exceed available storage. Please review the persisted folders or the instance count.', + () => !quotaExceeded // must not have quota exceeded + ) + .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 + ) + .required('Instance count is required.'), + }); +} diff --git a/app/react/kubernetes/applications/components/ReplicationFormSection/types.ts b/app/react/kubernetes/applications/components/ReplicationFormSection/types.ts new file mode 100644 index 000000000..87179fe9a --- /dev/null +++ b/app/react/kubernetes/applications/components/ReplicationFormSection/types.ts @@ -0,0 +1,3 @@ +export type ReplicaCountFormValues = { + replicaCount: number; +};