From 6d71a28584c0b5b2efb84b146ee1dbb1afc3b1c9 Mon Sep 17 00:00:00 2001
From: Ali <83188384+testA113@users.noreply.github.com>
Date: Mon, 15 Jan 2024 13:29:35 +1300
Subject: [PATCH] fix(app): improve resource quota error handling [EE-5933]
(#10951)
---
.../models/application/models/constants.js | 2 +-
app/kubernetes/react/components/index.ts | 20 ++++----
.../create/createApplication.html | 5 +-
.../create/createApplicationController.js | 20 ++++++--
.../DetailsView/ApplicationSummaryWidget.tsx | 10 +++-
.../AppDeploymentTypeFormSection.tsx | 21 +++++---
.../deploymentTypeValidation.ts | 19 +++++++
.../ResourceReservationFormSection.tsx | 50 ++++++++++---------
.../resourceReservationValidation.ts | 25 +++++++++-
9 files changed, 122 insertions(+), 50 deletions(-)
rename app/react/kubernetes/applications/{CreateView => components/AppDeploymentTypeFormSection}/AppDeploymentTypeFormSection.tsx (62%)
create mode 100644 app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation.ts
diff --git a/app/kubernetes/models/application/models/constants.js b/app/kubernetes/models/application/models/constants.js
index d4caab60e..74f9767af 100644
--- a/app/kubernetes/models/application/models/constants.js
+++ b/app/kubernetes/models/application/models/constants.js
@@ -1,6 +1,6 @@
export const KubernetesApplicationQuotaDefaults = {
CpuLimit: 0.1,
- MemoryLimit: 64, // MB
+ MemoryLimit: 128, // MB
};
export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack';
diff --git a/app/kubernetes/react/components/index.ts b/app/kubernetes/react/components/index.ts
index 38f6d0bfc..4ad90edd8 100644
--- a/app/kubernetes/react/components/index.ts
+++ b/app/kubernetes/react/components/index.ts
@@ -7,7 +7,6 @@ import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/Acce
import { RegistriesSelector } from '@/react/kubernetes/namespaces/components/RegistriesFormSection/RegistriesSelector';
import { KubeServicesForm } from '@/react/kubernetes/applications/CreateView/application-services/KubeServicesForm';
import { kubeServicesValidation } from '@/react/kubernetes/applications/CreateView/application-services/kubeServicesValidation';
-import { AppDeploymentTypeFormSection } from '@/react/kubernetes/applications/CreateView/AppDeploymentTypeFormSection';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { withUIRouter } from '@/react-tools/withUIRouter';
import {
@@ -56,6 +55,8 @@ import {
NameFormSection,
appNameValidation,
} from '@/react/kubernetes/applications/components/NameFormSection';
+import { deploymentTypeValidation } from '@/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation';
+import { AppDeploymentTypeFormSection } from '@/react/kubernetes/applications/components/AppDeploymentTypeFormSection/AppDeploymentTypeFormSection';
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
@@ -122,14 +123,6 @@ export const ngModule = angular
'persistedFoldersUseExistingVolumes',
])
)
- .component(
- 'appDeploymentTypeFormSection',
- r2a(AppDeploymentTypeFormSection, [
- 'value',
- 'onChange',
- 'supportGlobalDeployment',
- ])
- )
.component(
'kubeYamlInspector',
r2a(withUIRouter(withReactQuery(withCurrentUser(YAMLInspector))), [
@@ -333,3 +326,12 @@ withFormValidation(
appNameValidation,
true
);
+
+withFormValidation(
+ ngModule,
+ AppDeploymentTypeFormSection,
+ 'appDeploymentTypeFormSection',
+ ['supportGlobalDeployment'],
+ deploymentTypeValidation,
+ true
+);
diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html
index 5373e20a4..9a3d789e0 100644
--- a/app/kubernetes/views/applications/create/createApplication.html
+++ b/app/kubernetes/views/applications/create/createApplication.html
@@ -294,16 +294,17 @@
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}"
+ validation-data="{maxMemoryLimit: ctrl.state.sliders.memory.max, maxCpuLimit: ctrl.state.sliders.cpu.max, isEnvironmentAdmin: ctrl.state.isEnvironmentAdmin}"
resource-quota-capacity-exceeded="ctrl.resourceQuotaCapacityExceeded()"
>
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js
index 77d5da03f..37c3ef8e3 100644
--- a/app/kubernetes/views/applications/create/createApplicationController.js
+++ b/app/kubernetes/views/applications/create/createApplicationController.js
@@ -994,13 +994,28 @@ class KubernetesCreateApplicationController {
this.state.nodes.cpu += item.CPU;
});
+ var namespace = '';
+ this.formValues.ResourcePool = this.resourcePools[0];
+
if (this.resourcePools.length) {
- this.namespaceWithQuota = await this.KubernetesResourcePoolService.get(this.resourcePools[0].Namespace.Name);
+ if (this.state.isEdit) {
+ namespace = this.$state.params.namespace;
+ this.formValues.ResourcePool = _.find(this.resourcePools, ['Namespace.Name', namespace]);
+ }
+
+ namespace = this.formValues.ResourcePool.Namespace.Name;
+ this.namespaceWithQuota = await this.KubernetesResourcePoolService.get(namespace);
this.formValues.ResourcePool.Quota = this.namespaceWithQuota.Quota;
+
+ // this.savedFormValues is being used in updateNamespaceLimits behind a check to see isEdit
+ if (this.state.isEdit) {
+ this.savedFormValues = angular.copy(this.formValues);
+ }
+
this.updateNamespaceLimits(this.namespaceWithQuota);
this.updateSliders(this.namespaceWithQuota);
}
- this.formValues.ResourcePool = this.resourcePools[0];
+
if (!this.formValues.ResourcePool) {
return;
}
@@ -1008,7 +1023,6 @@ class KubernetesCreateApplicationController {
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
this.nodeNumber = nodes.length;
- const namespace = this.state.isEdit ? this.$state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
await this.refreshNamespaceData(namespace);
if (this.state.isEdit) {
diff --git a/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx b/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx
index 1544ff579..44fddd4c2 100644
--- a/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx
+++ b/app/react/kubernetes/applications/DetailsView/ApplicationSummaryWidget.tsx
@@ -97,10 +97,16 @@ export function ApplicationSummaryWidget() {
<>
{failedCreateCondition && (
-
+
+
+
Failed to create application
diff --git a/app/react/kubernetes/applications/CreateView/AppDeploymentTypeFormSection.tsx b/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/AppDeploymentTypeFormSection.tsx
similarity index 62%
rename from app/react/kubernetes/applications/CreateView/AppDeploymentTypeFormSection.tsx
rename to app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/AppDeploymentTypeFormSection.tsx
index 0b099ca24..764fb64f0 100644
--- a/app/react/kubernetes/applications/CreateView/AppDeploymentTypeFormSection.tsx
+++ b/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/AppDeploymentTypeFormSection.tsx
@@ -1,21 +1,25 @@
+import { FormikErrors } from 'formik';
+
import { BoxSelector } 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 './deploymentOptions';
+import { DeploymentType } from '../../types';
+import { getDeploymentOptions } from '../../CreateView/deploymentOptions';
interface Props {
- value: DeploymentType;
- onChange(value: DeploymentType): void;
+ values: DeploymentType;
+ onChange(values: DeploymentType): void;
+ errors: FormikErrors
;
supportGlobalDeployment: boolean;
}
export function AppDeploymentTypeFormSection({
- supportGlobalDeployment,
- value,
+ values,
onChange,
+ errors,
+ supportGlobalDeployment,
}: Props) {
const options = getDeploymentOptions(supportGlobalDeployment);
@@ -27,10 +31,11 @@ export function AppDeploymentTypeFormSection({
+ {!!errors && {errors}}
);
}
diff --git a/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation.ts b/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation.ts
new file mode 100644
index 000000000..beb342735
--- /dev/null
+++ b/app/react/kubernetes/applications/components/AppDeploymentTypeFormSection/deploymentTypeValidation.ts
@@ -0,0 +1,19 @@
+import { SchemaOf, mixed } from 'yup';
+
+import { DeploymentType } from '../../types';
+
+type ValidationData = {
+ isQuotaExceeded: boolean;
+};
+
+export function deploymentTypeValidation(
+ validationData?: ValidationData
+): SchemaOf {
+ return mixed()
+ .oneOf(['Replicated', 'Global'])
+ .test(
+ 'exhaused',
+ `This application would exceed available resources. Please review resource reservations or the instance count.`,
+ () => !validationData?.isQuotaExceeded
+ );
+}
diff --git a/app/react/kubernetes/applications/components/ResourceReservationFormSection/ResourceReservationFormSection.tsx b/app/react/kubernetes/applications/components/ResourceReservationFormSection/ResourceReservationFormSection.tsx
index 802417f97..ef38d63dc 100644
--- a/app/react/kubernetes/applications/components/ResourceReservationFormSection/ResourceReservationFormSection.tsx
+++ b/app/react/kubernetes/applications/components/ResourceReservationFormSection/ResourceReservationFormSection.tsx
@@ -55,14 +55,16 @@ export function ResourceReservationFormSection({
tooltip="An instance of this application will reserve this amount of memory. If the instance memory usage exceeds the reservation, it might be subject to OOM."
>
-
onChange({ ...values, memoryLimit: value })}
- max={maxMemoryLimit}
- step={128}
- dataCy="k8sAppCreate-memoryLimit"
- visibleTooltip
- />
+ {maxMemoryLimit > 0 && (
+ onChange({ ...values, memoryLimit: value })}
+ max={maxMemoryLimit}
+ step={128}
+ dataCy="k8sAppCreate-memoryLimit"
+ visibleTooltip
+ />
+ )}
{errors?.memoryLimit && (
{errors.memoryLimit}
)}
@@ -74,21 +76,23 @@ export function ResourceReservationFormSection({
tooltip="An instance of this application will reserve this amount of CPU. If the instance CPU usage exceeds the reservation, it might be subject to CPU throttling."
>
-
- onChange(
- typeof value === 'number'
- ? { ...values, cpuLimit: value }
- : { ...values, cpuLimit: value[0] ?? 0 }
- )
- }
- value={values.cpuLimit}
- min={0}
- max={maxCpuLimit}
- step={0.01}
- dataCy="k8sAppCreate-cpuLimitSlider"
- visibleTooltip
- />
+ {maxCpuLimit > 0 && (
+
+ onChange(
+ typeof value === 'number'
+ ? { ...values, cpuLimit: value }
+ : { ...values, cpuLimit: value[0] ?? 0 }
+ )
+ }
+ value={values.cpuLimit}
+ min={0}
+ max={maxCpuLimit}
+ step={0.1}
+ dataCy="k8sAppCreate-cpuLimitSlider"
+ visibleTooltip
+ />
+ )}
{errors?.cpuLimit && (
{errors.cpuLimit}
)}
diff --git a/app/react/kubernetes/applications/components/ResourceReservationFormSection/resourceReservationValidation.ts b/app/react/kubernetes/applications/components/ResourceReservationFormSection/resourceReservationValidation.ts
index 11b5d708d..9b2791768 100644
--- a/app/react/kubernetes/applications/components/ResourceReservationFormSection/resourceReservationValidation.ts
+++ b/app/react/kubernetes/applications/components/ResourceReservationFormSection/resourceReservationValidation.ts
@@ -5,6 +5,7 @@ import { ResourceQuotaFormValues } from './types';
type ValidationData = {
maxMemoryLimit: number;
maxCpuLimit: number;
+ isEnvironmentAdmin: boolean;
};
export function resourceReservationValidation(
@@ -13,16 +14,36 @@ export function resourceReservationValidation(
return object().shape({
memoryLimit: number()
.min(0)
+ .test(
+ 'exhaused',
+ `The memory capacity for this namespace has been exhausted, so you cannot deploy the application.${
+ validationData?.isEnvironmentAdmin
+ ? ''
+ : ' Contact your administrator to expand the memory capacity of the namespace.'
+ }`,
+ () => !!validationData && validationData.maxMemoryLimit > 0
+ )
.max(
validationData?.maxMemoryLimit || 0,
- `Value must be between 0 and ${validationData?.maxMemoryLimit}`
+ ({ value }) =>
+ `Value must be between 0 and ${validationData?.maxMemoryLimit}MB now - the previous value of ${value} exceeds this`
)
.required(),
cpuLimit: number()
.min(0)
+ .test(
+ 'exhaused',
+ `The CPU capacity for this namespace has been exhausted, so you cannot deploy the application.${
+ validationData?.isEnvironmentAdmin
+ ? ''
+ : ' Contact your administrator to expand the CPU capacity of the namespace.'
+ }`,
+ () => !!validationData && validationData.maxCpuLimit > 0
+ )
.max(
validationData?.maxCpuLimit || 0,
- `Value must be between 0 and ${validationData?.maxCpuLimit}`
+ ({ value }) =>
+ `Value must be between 0 and ${validationData?.maxCpuLimit} now - the previous value of ${value} exceeds this`
)
.required(),
});