1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

feat(k8s/application): add the ability to set the auto-scale policy of an application (#4118)

* feat(application): add horizontalpodautoscaler creation

* feat(application): Add the ability to set the auto-scale policy of an application

* feat(k8s/application): minor UI update

* fix(application): set api version and prevent to use hpa with global deployment type

* feat(settings): add a switch to enable features based on server metrics

* feat(k8s/applications): minor UI update

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
Maxime Bajeux 2020-08-05 00:08:11 +02:00 committed by GitHub
parent 909e1ef02c
commit 6756b04b67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 534 additions and 84 deletions

View file

@ -635,7 +635,13 @@
</label>
</div>
<div ng-if="ctrl.supportGlobalDeployment()">
<input type="radio" id="deployment_global" ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL" ng-model="ctrl.formValues.DeploymentType" />
<input
type="radio"
id="deployment_global"
ng-value="ctrl.ApplicationDeploymentTypes.GLOBAL"
ng-model="ctrl.formValues.DeploymentType"
ng-click="ctrl.unselectAutoScaler()"
/>
<label for="deployment_global">
<div class="boxselector_header">
<i class="fa fa-cubes" aria-hidden="true" style="margin-right: 2px;"></i>
@ -695,6 +701,116 @@
</div>
</div>
<!-- auto scaling -->
<div class="col-sm-12 form-section-title" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL">
Auto-scaling
</div>
<div class="form-group" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && ctrl.state.useServerMetrics">
<div class="col-sm-12">
<label for="enable_auto_scaling" class="control-label text-left">
Enable auto scaling for this application
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" class="form-control" name="enable_auto_scaling" ng-model="ctrl.formValues.AutoScaler.IsUsed" />
<i></i>
</label>
</div>
</div>
<div class="form-group" ng-if="ctrl.formValues.DeploymentType !== ctrl.ApplicationDeploymentTypes.GLOBAL && !ctrl.state.useServerMetrics">
<div class="col-sm-12 small text-muted">
<p ng-if="!ctrl.isAdmin">
This feature is currently disabled and must be enabled by an administrator user.
</p>
<p ng-if="ctrl.isAdmin">
Server metrics features must be enabled in the
<a ui-sref="portainer.endpoints.endpoint.kubernetesConfig({id: ctrl.endpoint.Id})" class="ctrl.isAdmin">endpoint configuration view</a>.
</p>
</div>
</div>
<div class="form-inline" ng-if="ctrl.formValues.AutoScaler.IsUsed">
<table class="table" style="margin-bottom: 0px;">
<tbody>
<tr class="small">
<td style="width: 33%; border: none; padding: 2px 0 2px 0;">Minimum instances</td>
<td style="width: 33%; border: none; padding: 2px 0 2px 0;">Maximum instances</td>
<td style="width: 33%; border: none; padding: 2px 0 2px 0;">
Target CPU usage (<b>%</b>)
<portainer-tooltip position="bottom" message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.">
</portainer-tooltip>
</td>
</tr>
<tr>
<td style="padding: 8px 5px 5px 0; border: none;">
<div class="input-group input-group-sm" style="width: 100%;">
<input
type="number"
class="form-control"
name="auto_scaler_min"
min="0"
ng-max="ctrl.formValues.AutoScaler.MaxReplicas"
ng-model="ctrl.formValues.AutoScaler.MinReplicas"
required
/>
</div>
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_min'].$invalid">
<div class="small text-warning" style="margin-top: 5px;">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_min'].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum instances is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum instances must be greater than 0.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Minimum instances must be smaller than maximum instances.</p>
</ng-messages>
</div>
</div>
</td>
<td style="padding: 8px 5px 5px 0; border: none;">
<div class="input-group input-group-sm" style="width: 100%;">
<input
type="number"
class="form-control"
name="auto_scaler_max"
ng-min="ctrl.formValues.AutoScaler.MinReplicas"
ng-model="ctrl.formValues.AutoScaler.MaxReplicas"
/>
</div>
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_max'].$invalid || ctrl.autoScalerOverflow()">
<div class="small text-warning" style="margin-top: 5px;">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_max'].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum instances is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Maximum instances must be greater than minimum instances.</p>
</ng-messages>
</div>
</div>
</td>
<td style="padding: 8px 5px 5px 0; border: none;">
<div class="input-group input-group-sm" style="width: 100%;">
<input type="number" class="form-control" name="auto_scaler_cpu" ng-model="ctrl.formValues.AutoScaler.TargetCPUUtilization" min="1" max="100" required />
</div>
<div class="input-group input-group-sm" ng-show="kubernetesApplicationCreationForm['auto_scaler_cpu'].$invalid">
<div class="small text-warning" style="margin-top: 5px;">
<ng-messages for="kubernetesApplicationCreationForm['auto_scaler_cpu'].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target CPU usage is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target CPU usage must be greater than 0.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Target CPU usage must be smaller than 100.</p>
</ng-messages>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div class="form-group" ng-if="ctrl.autoScalerOverflow()" style="margin-bottom: 10px;">
<div class="col-sm-12 small text-muted">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy.
</div>
</div>
</div>
<!-- !auto scaling -->
<div class="col-sm-12 form-section-title">
Publishing the application
</div>

View file

@ -22,6 +22,7 @@ import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHel
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
class KubernetesCreateApplicationController {
/* @ngInject */
@ -80,6 +81,16 @@ class KubernetesCreateApplicationController {
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
}
/**
* AUTO SCALER UI MANAGEMENT
*/
unselectAutoScaler() {
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) {
this.formValues.AutoScaler.IsUsed = false;
}
}
/**
* CONFIGURATION UI MANAGEMENT
*/
@ -319,6 +330,24 @@ class KubernetesCreateApplicationController {
return false;
}
autoScalerOverflow() {
const instances = this.formValues.AutoScaler.MaxReplicas;
const cpu = this.formValues.CpuLimit;
const maxCpu = this.state.sliders.cpu.max;
const memory = this.formValues.MemoryLimit;
const maxMemory = this.state.sliders.memory.max;
if (cpu * instances > maxCpu) {
return true;
}
if (memory * instances > maxMemory) {
return true;
}
return false;
}
publishViaLoadBalancerEnabled() {
return this.state.useLoadBalancer;
}
@ -345,11 +374,12 @@ class KubernetesCreateApplicationController {
isDeployUpdateButtonDisabled() {
const overflow = this.resourceReservationsOverflow();
const autoScalerOverflow = this.autoScalerOverflow();
const inProgress = this.state.actionInProgress;
const invalid = !this.isValid();
const hasNoChanges = this.isEditAndNoChangesMade();
const nonScalable = this.isNonScalable();
const res = overflow || inProgress || invalid || hasNoChanges || nonScalable;
const res = overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable;
return res;
}
@ -549,6 +579,7 @@ class KubernetesCreateApplicationController {
this.state = {
actionInProgress: false,
useLoadBalancer: false,
useServerMetrics: false,
sliders: {
cpu: {
min: 0,
@ -580,6 +611,8 @@ class KubernetesCreateApplicationController {
},
};
this.isAdmin = this.Authentication.isAdmin();
this.editChanges = [];
if (this.$transition$.params().namespace && this.$transition$.params().name) {
@ -587,8 +620,10 @@ class KubernetesCreateApplicationController {
}
const endpoint = this.EndpointProvider.currentEndpoint();
this.endpoint = endpoint;
this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer;
this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics;
this.formValues = new KubernetesApplicationFormValues();
@ -611,6 +646,10 @@ class KubernetesCreateApplicationController {
this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims);
this.savedFormValues = angular.copy(this.formValues);
delete this.formValues.ApplicationType;
} else {
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler();
this.formValues.AutoScaler.MinReplicas = this.formValues.ReplicaCount;
this.formValues.AutoScaler.MaxReplicas = this.formValues.ReplicaCount;
}
await this.updateSliders();

View file

@ -355,7 +355,7 @@
<tr>
<td>{{ ctrl.application.AutoScaler.MinReplicas }}</td>
<td>{{ ctrl.application.AutoScaler.MaxReplicas }}</td>
<td>{{ ctrl.application.AutoScaler.TargetCPUUtilizationPercentage }}%</td>
<td>{{ ctrl.application.AutoScaler.TargetCPUUtilization }}%</td>
</tr>
</tbody>
</table>

View file

@ -13,10 +13,11 @@
<div class="col-sm-12 form-section-title">
Expose applications over external IP addresses
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Enabling this feature will allow users to expose application they deploy over an external IP address assigned by cloud provider.
<p>
<p style="margin-top: 2px;">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs.
</p>
@ -31,6 +32,30 @@
</div>
</div>
<div class="col-sm-12 form-section-title">
Metrics
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Enabling this feature will allow users to use specific features that leverage the server metrics component.
<p style="margin-top: 2px;">
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Ensure that <a href="https://kubernetes.io/docs/tasks/debug-application-cluster/resource-metrics-pipeline/#metrics-server" target="_blank">server metrics</a> is
running inside your cluster.
</p>
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Enable features using server metrics
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="ctrl.formValues.UseServerMetrics" /><i></i> </label>
</div>
</div>
<div class="col-sm-12 form-section-title">
Available storage options
</div>

View file

@ -50,12 +50,14 @@ class KubernetesConfigureController {
this.endpoint.Kubernetes.Configuration.StorageClasses = classes;
this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
this.endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
const endpoints = this.EndpointProvider.endpoints();
const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id);
if (modifiedEndpoint) {
modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes;
modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
modifiedEndpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
this.EndpointProvider.setEndpoints(endpoints);
}
this.Notifications.success('Configuration successfully applied');
@ -80,6 +82,7 @@ class KubernetesConfigureController {
this.formValues = {
UseLoadBalancer: false,
UseServerMetrics: false,
};
try {
@ -100,6 +103,7 @@ class KubernetesConfigureController {
});
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve storage classes');
} finally {