mirror of https://github.com/portainer/portainer
refactor(app): migrate resource reservations [EE-6236] (#10695)
* refactor(app): migrate resource reservations [EE-6236]pull/10705/head
parent
e07ee05ee7
commit
947ba4940b
|
@ -30,6 +30,10 @@ import { configurationsValidationSchema } from '@/react/kubernetes/applications/
|
||||||
import { ConfigMapsFormSection } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/ConfigMapsFormSection';
|
import { ConfigMapsFormSection } from '@/react/kubernetes/applications/components/ConfigurationsFormSection/ConfigMapsFormSection';
|
||||||
import { PersistedFoldersFormSection } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection';
|
import { PersistedFoldersFormSection } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection';
|
||||||
import { persistedFoldersValidation } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection/persistedFoldersValidation';
|
import { persistedFoldersValidation } from '@/react/kubernetes/applications/components/PersistedFoldersFormSection/persistedFoldersValidation';
|
||||||
|
import {
|
||||||
|
ResourceReservationFormSection,
|
||||||
|
resourceReservationValidation,
|
||||||
|
} from '@/react/kubernetes/applications/components/ResourceReservationFormSection';
|
||||||
|
|
||||||
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
|
import { EnvironmentVariablesFieldset } from '@@/form-components/EnvironmentVariablesFieldset';
|
||||||
|
|
||||||
|
@ -221,3 +225,16 @@ withFormValidation(
|
||||||
],
|
],
|
||||||
persistedFoldersValidation
|
persistedFoldersValidation
|
||||||
);
|
);
|
||||||
|
|
||||||
|
withFormValidation(
|
||||||
|
ngModule,
|
||||||
|
withUIRouter(withCurrentUser(withReactQuery(ResourceReservationFormSection))),
|
||||||
|
'resourceReservationFormSection',
|
||||||
|
[
|
||||||
|
'namespaceHasQuota',
|
||||||
|
'resourceQuotaCapacityExceeded',
|
||||||
|
'maxMemoryLimit',
|
||||||
|
'maxCpuLimit',
|
||||||
|
],
|
||||||
|
resourceReservationValidation
|
||||||
|
);
|
||||||
|
|
|
@ -432,102 +432,15 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Resource reservations </div>
|
<resource-reservation-form-section
|
||||||
<!-- #region RESOURCE RESERVATIONS -->
|
values="{memoryLimit: ctrl.formValues.MemoryLimit, cpuLimit: ctrl.formValues.CpuLimit}"
|
||||||
<div class="form-group" ng-if="!ctrl.state.resourcePoolHasQuota">
|
on-change="(ctrl.onChangeResourceReservation)"
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
namespace-has-quota="ctrl.state.resourcePoolHasQuota"
|
||||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
max-memory-limit="ctrl.state.sliders.memory.max"
|
||||||
Resource reservations are applied per instance of the application.
|
max-cpu-limit="ctrl.state.sliders.cpu.max"
|
||||||
</div>
|
validation-data="{maxMemoryLimit: ctrl.state.sliders.memory.max, maxCpuLimit: ctrl.state.sliders.cpu.max}"
|
||||||
</div>
|
resource-quota-capacity-exceeded="ctrl.resourceQuotaCapacityExceeded()"
|
||||||
|
></resource-reservation-form-section>
|
||||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded()">
|
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
|
||||||
<pr-icon icon="'info'" mode="'primary'"></pr-icon>
|
|
||||||
A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums
|
|
||||||
are inherited from the namespace quota.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.state.resourcePoolHasQuota && ctrl.resourceQuotaCapacityExceeded()">
|
|
||||||
<div class="col-sm-12 small text-muted vertical-center">
|
|
||||||
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
|
|
||||||
This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of
|
|
||||||
the namespace.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- memory-limit-input -->
|
|
||||||
<div
|
|
||||||
class="form-group flex"
|
|
||||||
ng-if="
|
|
||||||
(!ctrl.state.resourcePoolHasQuota || (ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded())) && ctrl.formValues.Containers.length <= 1
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<label for="memory-limit" class="space-right col-sm-3 col-lg-2 control-label flex flex-row items-center text-left">
|
|
||||||
Memory limit (MB)
|
|
||||||
<portainer-tooltip
|
|
||||||
message="'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.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-6">
|
|
||||||
<slider model="ctrl.formValues.MemoryLimit" floor="ctrl.state.sliders.memory.min" ceil="ctrl.state.sliders.memory.max" step="128"></slider>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-2 vertical-center">
|
|
||||||
<input
|
|
||||||
name="memory_limit"
|
|
||||||
ng-model="ctrl.formValues.MemoryLimit"
|
|
||||||
type="number"
|
|
||||||
min="{{ ctrl.state.sliders.memory.min }}"
|
|
||||||
max="{{ ctrl.state.sliders.memory.max }}"
|
|
||||||
class="form-control"
|
|
||||||
id="memory-limit"
|
|
||||||
required
|
|
||||||
data-cy="k8sAppCreate-memoryLimit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" ng-show="kubernetesApplicationCreationForm.memory_limit.$invalid">
|
|
||||||
<div class="col-sm-3 col-lg-2"></div>
|
|
||||||
<div class="col-sm-8 small text-warning">
|
|
||||||
<div ng-messages="kubernetesApplicationCreationForm.memory_limit.$error">
|
|
||||||
<p class="vertical-center"
|
|
||||||
><pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon> Value must be between {{ ctrl.state.sliders.memory.min }} and
|
|
||||||
{{ ctrl.state.sliders.memory.max }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !memory-limit-input -->
|
|
||||||
<!-- cpu-limit-input -->
|
|
||||||
<div
|
|
||||||
class="form-group flex"
|
|
||||||
ng-if="
|
|
||||||
(!ctrl.state.resourcePoolHasQuota || (ctrl.state.resourcePoolHasQuota && !ctrl.resourceQuotaCapacityExceeded())) && ctrl.formValues.Containers.length <= 1
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<label for="cpu-limit" class="space-right col-sm-3 col-lg-2 control-label flex flex-row items-center text-left">
|
|
||||||
CPU limit
|
|
||||||
<portainer-tooltip
|
|
||||||
message="'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.'"
|
|
||||||
>
|
|
||||||
</portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<slider model="ctrl.formValues.CpuLimit" floor="ctrl.state.sliders.cpu.min" ceil="ctrl.state.sliders.cpu.max" step="0.10" precision="2"></slider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-if="ctrl.nodeLimitsOverflow()">
|
|
||||||
<div class="col-sm-3 col-lg-2"></div>
|
|
||||||
<div class="col-sm-8 small text-muted">
|
|
||||||
<pr-icon icon="'alert-circle'" mode="'danger'"></pr-icon>
|
|
||||||
These reservations would exceed the resources currently available in the cluster.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !cpu-limit-input -->
|
|
||||||
<!-- #endregion -->
|
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Deployment </div>
|
<div class="col-sm-12 form-section-title"> Deployment </div>
|
||||||
<!-- #region DEPLOYMENT -->
|
<!-- #region DEPLOYMENT -->
|
||||||
|
|
|
@ -154,6 +154,7 @@ class KubernetesCreateApplicationController {
|
||||||
this.onConfigMapsChange = this.onConfigMapsChange.bind(this);
|
this.onConfigMapsChange = this.onConfigMapsChange.bind(this);
|
||||||
this.onSecretsChange = this.onSecretsChange.bind(this);
|
this.onSecretsChange = this.onSecretsChange.bind(this);
|
||||||
this.onChangePersistedFolder = this.onChangePersistedFolder.bind(this);
|
this.onChangePersistedFolder = this.onChangePersistedFolder.bind(this);
|
||||||
|
this.onChangeResourceReservation = this.onChangeResourceReservation.bind(this);
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
@ -539,6 +540,13 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeResourceReservation(values) {
|
||||||
|
return this.$async(async () => {
|
||||||
|
this.formValues.MemoryLimit = values.memoryLimit;
|
||||||
|
this.formValues.CpuLimit = values.cpuLimit;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
resourceQuotaCapacityExceeded() {
|
resourceQuotaCapacityExceeded() {
|
||||||
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
|
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
import { SliderWithInput } from '@@/form-components/Slider/SliderWithInput';
|
||||||
|
import { FormControl } from '@@/form-components/FormControl';
|
||||||
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
import { Slider } from '@@/form-components/Slider';
|
||||||
|
|
||||||
|
import { ResourceQuotaFormValues } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
values: ResourceQuotaFormValues;
|
||||||
|
onChange: (values: ResourceQuotaFormValues) => void;
|
||||||
|
errors: FormikErrors<ResourceQuotaFormValues>;
|
||||||
|
namespaceHasQuota: boolean;
|
||||||
|
resourceQuotaCapacityExceeded: boolean;
|
||||||
|
maxMemoryLimit: number;
|
||||||
|
maxCpuLimit: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ResourceReservationFormSection({
|
||||||
|
values,
|
||||||
|
onChange,
|
||||||
|
errors,
|
||||||
|
namespaceHasQuota,
|
||||||
|
resourceQuotaCapacityExceeded,
|
||||||
|
maxMemoryLimit,
|
||||||
|
maxCpuLimit,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<FormSection title="Resource reservations" titleSize="md">
|
||||||
|
{!namespaceHasQuota && (
|
||||||
|
<TextTip color="blue">
|
||||||
|
Resource reservations are applied per instance of the application.
|
||||||
|
</TextTip>
|
||||||
|
)}
|
||||||
|
{namespaceHasQuota && !resourceQuotaCapacityExceeded && (
|
||||||
|
<TextTip color="blue">
|
||||||
|
A resource quota is set on this namespace, you must specify resource
|
||||||
|
reservations. Resource reservations are applied per instance of the
|
||||||
|
application. Maximums are inherited from the namespace quota.
|
||||||
|
</TextTip>
|
||||||
|
)}
|
||||||
|
{namespaceHasQuota && resourceQuotaCapacityExceeded && (
|
||||||
|
<TextTip color="red">
|
||||||
|
This namespace has exhausted its resource capacity and you will not be
|
||||||
|
able to deploy the application. Contact your administrator to expand
|
||||||
|
the capacity of the namespace.
|
||||||
|
</TextTip>
|
||||||
|
)}
|
||||||
|
<FormControl
|
||||||
|
className="flex flex-row"
|
||||||
|
label="Memory limit (MB)"
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
{errors?.memoryLimit && (
|
||||||
|
<FormError className="pt-1">{errors.memoryLimit}</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
className="flex flex-row"
|
||||||
|
label="CPU limit"
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
{errors?.cpuLimit && (
|
||||||
|
<FormError className="pt-1">{errors.cpuLimit}</FormError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormSection>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { ResourceReservationFormSection } from './ResourceReservationFormSection';
|
||||||
|
export { resourceReservationValidation } from './resourceReservationValidation';
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { SchemaOf, number, object } from 'yup';
|
||||||
|
|
||||||
|
import { ResourceQuotaFormValues } from './types';
|
||||||
|
|
||||||
|
type ValidationData = {
|
||||||
|
maxMemoryLimit: number;
|
||||||
|
maxCpuLimit: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resourceReservationValidation(
|
||||||
|
validationData?: ValidationData
|
||||||
|
): SchemaOf<ResourceQuotaFormValues> {
|
||||||
|
return object().shape({
|
||||||
|
memoryLimit: number()
|
||||||
|
.min(0)
|
||||||
|
.max(
|
||||||
|
validationData?.maxMemoryLimit || 0,
|
||||||
|
`Value must be between 0 and ${validationData?.maxMemoryLimit}`
|
||||||
|
)
|
||||||
|
.required(),
|
||||||
|
cpuLimit: number()
|
||||||
|
.min(0)
|
||||||
|
.max(
|
||||||
|
validationData?.maxCpuLimit || 0,
|
||||||
|
`Value must be between 0 and ${validationData?.maxCpuLimit}`
|
||||||
|
)
|
||||||
|
.required(),
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type ResourceQuotaFormValues = {
|
||||||
|
memoryLimit: number;
|
||||||
|
cpuLimit: number;
|
||||||
|
};
|
Loading…
Reference in New Issue