diff --git a/api/kubernetes/cli/nodes_limits.go b/api/kubernetes/cli/nodes_limits.go index ad7e04e28..91aa0a4eb 100644 --- a/api/kubernetes/cli/nodes_limits.go +++ b/api/kubernetes/cli/nodes_limits.go @@ -24,7 +24,7 @@ func (kcl *KubeClient) GetNodesLimits() (portainer.K8sNodesLimits, error) { for _, item := range nodes.Items { cpu := item.Status.Allocatable.Cpu().MilliValue() - memory := item.Status.Allocatable.Memory().Value() + memory := item.Status.Allocatable.Memory().Value() // bytes nodesLimits[item.ObjectMeta.Name] = &portainer.K8sNodeLimits{ CPU: cpu, @@ -57,7 +57,7 @@ func (client *KubeClient) GetMaxResourceLimits(skipNamespace string, overCommitE memory := int64(0) for _, node := range nodes.Items { limits.CPU += node.Status.Allocatable.Cpu().MilliValue() - memory += node.Status.Allocatable.Memory().Value() + memory += node.Status.Allocatable.Memory().Value() // bytes } limits.Memory = memory / 1000000 // B to MB diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 8c3ecd44d..63b41adbc 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -285,7 +285,7 @@ namespace-has-quota="ctrl.state.resourcePoolHasQuota" max-memory-limit="ctrl.state.sliders.memory.max" max-cpu-limit="ctrl.state.sliders.cpu.max" - validation-data="{maxMemoryLimit: ctrl.state.sliders.memory.max, maxCpuLimit: ctrl.state.sliders.cpu.max, isEnvironmentAdmin: ctrl.isAdmin}" + validation-data="{maxMemoryLimit: ctrl.state.sliders.memory.max, maxCpuLimit: ctrl.state.sliders.cpu.max, isEnvironmentAdmin: ctrl.isAdmin, nodeLimits: ctrl.nodesLimits.nodesLimits}" resource-quota-capacity-exceeded="ctrl.resourceQuotaCapacityExceeded()" > diff --git a/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation.ts b/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation.ts index beb342735..f7490580f 100644 --- a/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation.ts +++ b/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation.ts @@ -14,6 +14,12 @@ export function deploymentTypeValidation( .test( 'exhaused', `This application would exceed available resources. Please review resource reservations or the instance count.`, - () => !validationData?.isQuotaExceeded + (value) => { + // ignore this validation if the user has selected Replicated, in this case, the isQuotaExceeded will below the instance count input + if (value === 'Replicated') { + return true; + } + return !validationData?.isQuotaExceeded; + } ); } diff --git a/app/react/kubernetes/applications/components/ReplicationFormSection/replicationValidation.ts b/app/react/kubernetes/applications/components/ReplicationFormSection/replicationValidation.ts index 2a84e5d05..922b28e55 100644 --- a/app/react/kubernetes/applications/components/ReplicationFormSection/replicationValidation.ts +++ b/app/react/kubernetes/applications/components/ReplicationFormSection/replicationValidation.ts @@ -24,7 +24,13 @@ export function replicationValidation( .test( 'overflow', 'This application would exceed available resources. Please review resource reservations or the instance count.', - () => !resourceReservationsOverflow // must not have resource reservations overflow + (value) => { + // the user can't fix the error here with 1 replica. There are validation errors in the resource reservations section that are helpful in a case of resourceReservationsOverflow. + if (value === 1) { + return true; + } + return !resourceReservationsOverflow; + } ) .test( 'quota', diff --git a/app/react/kubernetes/applications/components/ResourceReservationFormSection/resourceReservationValidation.ts b/app/react/kubernetes/applications/components/ResourceReservationFormSection/resourceReservationValidation.ts index 9b2791768..d21971f48 100644 --- a/app/react/kubernetes/applications/components/ResourceReservationFormSection/resourceReservationValidation.ts +++ b/app/react/kubernetes/applications/components/ResourceReservationFormSection/resourceReservationValidation.ts @@ -1,11 +1,21 @@ -import { SchemaOf, number, object } from 'yup'; +import { SchemaOf, TestContext, number, object } from 'yup'; + +import KubernetesResourceReservationHelper from '@/kubernetes/helpers/resourceReservationHelper'; import { ResourceQuotaFormValues } from './types'; +type NodeLimit = { + CPU: number; + Memory: number; +}; + +type NodesLimits = Record; + type ValidationData = { maxMemoryLimit: number; maxCpuLimit: number; isEnvironmentAdmin: boolean; + nodeLimits: NodesLimits; }; export function resourceReservationValidation( @@ -28,6 +38,23 @@ export function resourceReservationValidation( ({ value }) => `Value must be between 0 and ${validationData?.maxMemoryLimit}MB now - the previous value of ${value} exceeds this` ) + .test( + 'hasSuitableNode', + `These reservations would exceed the resources currently available in the cluster.`, + // eslint-disable-next-line prefer-arrow-callback, func-names + function (value: number | undefined, context: TestContext) { + if (!validationData || value === undefined) { + // explicitely check for undefined, since 0 is a valid value + return true; + } + const { memoryLimit, cpuLimit } = context.parent; + return hasSuitableNode( + memoryLimit, + cpuLimit, + validationData.nodeLimits + ); + } + ) .required(), cpuLimit: number() .min(0) @@ -45,6 +72,41 @@ export function resourceReservationValidation( ({ value }) => `Value must be between 0 and ${validationData?.maxCpuLimit} now - the previous value of ${value} exceeds this` ) + .test( + 'hasSuitableNode', + `These reservations would exceed the resources currently available in the cluster.`, + // eslint-disable-next-line prefer-arrow-callback, func-names + function (value: number | undefined, context: TestContext) { + if (!validationData || value === undefined) { + // explicitely check for undefined, since 0 is a valid value + return true; + } + const { memoryLimit, cpuLimit } = context.parent; + return hasSuitableNode( + memoryLimit, + cpuLimit, + validationData.nodeLimits + ); + } + ) .required(), }); } + +function hasSuitableNode( + memoryLimit: number, + cpuLimit: number, + nodeLimits: NodesLimits +) { + // transform the nodelimits from bytes to MB + const limits = Object.values(nodeLimits).map((nodeLimit) => ({ + ...nodeLimit, + Memory: KubernetesResourceReservationHelper.megaBytesValue( + nodeLimit.Memory + ), + })); + // make sure there's a node available with enough memory and cpu + return limits.some( + (nodeLimit) => nodeLimit.Memory >= memoryLimit && nodeLimit.CPU >= cpuLimit + ); +}