fix(app): improve resource quota error handling [EE-5933] (#10951)

pull/10956/head
Ali 10 months ago committed by GitHub
parent 488fcc7cc5
commit 6d71a28584
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
export const KubernetesApplicationQuotaDefaults = {
CpuLimit: 0.1,
MemoryLimit: 64, // MB
MemoryLimit: 128, // MB
};
export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack';

@ -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
);

@ -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()"
></resource-reservation-form-section>
<!-- deployment options -->
<app-deployment-type-form-section
value="ctrl.formValues.DeploymentType"
values="ctrl.formValues.DeploymentType"
on-change="(ctrl.onChangeDeploymentType)"
support-global-deployment="ctrl.supportGlobalDeployment()"
radio-name="'deploymentType'"
validation-data="{isQuotaExceeded: ctrl.resourceReservationsOverflow()}"
></app-deployment-type-form-section>
<!-- replica count -->

@ -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) {

@ -97,10 +97,16 @@ export function ApplicationSummaryWidget() {
<>
{failedCreateCondition && (
<div
className="vertical-center alert alert-danger mb-2"
className="flex gap-1 items-start alert alert-danger mb-2"
data-cy="k8sAppDetail-failedCreateMessage"
>
<Icon icon={Info} className="mr-1" mode="danger" />
<div className="mt-0.5">
<Icon
icon={Info}
className="mr-1 shrink-0"
mode="danger"
/>
</div>
<div>
<div className="font-semibold">
Failed to create application

@ -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<DeploymentType>;
supportGlobalDeployment: boolean;
}
export function AppDeploymentTypeFormSection({
supportGlobalDeployment,
value,
values,
onChange,
errors,
supportGlobalDeployment,
}: Props) {
const options = getDeploymentOptions(supportGlobalDeployment);
@ -27,10 +31,11 @@ export function AppDeploymentTypeFormSection({
<BoxSelector
slim
options={options}
value={value}
value={values}
onChange={onChange}
radioName="deploymentType"
/>
{!!errors && <FormError>{errors}</FormError>}
</FormSection>
);
}

@ -0,0 +1,19 @@
import { SchemaOf, mixed } from 'yup';
import { DeploymentType } from '../../types';
type ValidationData = {
isQuotaExceeded: boolean;
};
export function deploymentTypeValidation(
validationData?: ValidationData
): SchemaOf<DeploymentType> {
return mixed()
.oneOf(['Replicated', 'Global'])
.test(
'exhaused',
`This application would exceed available resources. Please review resource reservations or the instance count.`,
() => !validationData?.isQuotaExceeded
);
}

@ -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."
>
<div className="col-xs-10">
<SliderWithInput
value={Number(values.memoryLimit) ?? 0}
onChange={(value) => onChange({ ...values, memoryLimit: value })}
max={maxMemoryLimit}
step={128}
dataCy="k8sAppCreate-memoryLimit"
visibleTooltip
/>
{maxMemoryLimit > 0 && (
<SliderWithInput
value={Number(values.memoryLimit) ?? 0}
onChange={(value) => onChange({ ...values, memoryLimit: value })}
max={maxMemoryLimit}
step={128}
dataCy="k8sAppCreate-memoryLimit"
visibleTooltip
/>
)}
{errors?.memoryLimit && (
<FormError className="pt-1">{errors.memoryLimit}</FormError>
)}
@ -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."
>
<div className="col-xs-10">
<Slider
onChange={(value) =>
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 && (
<Slider
onChange={(value) =>
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 && (
<FormError className="pt-1">{errors.cpuLimit}</FormError>
)}

@ -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(),
});

Loading…
Cancel
Save