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

refactor(app): backport technical changes (#4679)

* refactor(app): backport technical changes

* refactor(app): remove EE only features

* feat(app): small review changes to match EE codebase layout on some files

Co-authored-by: xAt0mZ <baron_l@epitech.eu>
This commit is contained in:
Alice Groux 2021-02-26 16:50:33 +01:00 committed by GitHub
parent 158bdae10e
commit ccf6babc02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 951 additions and 976 deletions

View file

@ -1,7 +1,7 @@
require('../../templates/advancedDeploymentPanel.html');
import angular from 'angular';
import * as _ from 'lodash-es';
import _ from 'lodash-es';
import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';

View file

@ -143,60 +143,72 @@
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="envVar in ctrl.formValues.EnvironmentVariables | orderBy: 'NameIndex'" style="margin-top: 2px;">
<div class="col-sm-4 input-group input-group-sm" style="vertical-align: top;">
<div class="input-group col-sm-12 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
<span class="input-group-addon">name</span>
<div style="margin-top: 2px;">
<div class="col-sm-4 input-group input-group-sm">
<div class="input-group col-sm-12 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
<span class="input-group-addon">name</span>
<input
type="text"
name="environment_variable_name_{{ $index }}"
class="form-control"
ng-model="envVar.Name"
ng-change="ctrl.onChangeEnvironmentName()"
ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/"
placeholder="foo"
ng-disabled="ctrl.formValues.Containers.length > 1"
required
/>
</div>
</div>
<div class="col-sm-4 input-group input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
<span class="input-group-addon">value</span>
<input
type="text"
name="environment_variable_name_{{ $index }}"
name="environment_variable_value_{{ $index }}"
class="form-control"
ng-model="envVar.Name"
ng-change="ctrl.onChangeEnvironmentName()"
ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/"
placeholder="foo"
ng-model="envVar.Value"
placeholder="bar"
ng-disabled="ctrl.formValues.Containers.length > 1"
required
/>
</div>
<div
class="small text-warning"
style="margin-top: 5px;"
ng-show="
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
<p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist alphanumeric characters, '-' or '_', start with an alphabetic
character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').</p
>
</ng-messages>
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This environment variable is already defined.</p
>
<div class="col-sm-2 input-group input-group-sm" ng-if="ctrl.formValues.Containers.length <= 1">
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable(envVar)">
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable(envVar)">
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="input-group col-sm-4 input-group-sm" ng-class="{ striked: envVar.NeedsDeletion }">
<span class="input-group-addon">value</span>
<input
type="text"
name="environment_variable_value_{{ $index }}"
class="form-control"
ng-model="envVar.Value"
placeholder="bar"
ng-disabled="ctrl.formValues.Containers.length > 1"
/>
</div>
<div class="input-group col-sm-2 input-group-sm" ng-if="ctrl.formValues.Containers.length <= 1">
<button ng-if="!envVar.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removeEnvironmentVariable(envVar)">
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
<button ng-if="envVar.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restoreEnvironmentVariable(envVar)">
<i class="fa fa-trash-restore" aria-hidden="true"></i>
</button>
<div
ng-show="
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
"
>
<div class="col-sm-4 input-group input-group-sm">
<div
class="small text-warning"
style="margin-top: 5px;"
ng-show="
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
<p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist alphanumeric characters, '-' or '_', start with an alphabetic
character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').</p
>
</ng-messages>
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This environment variable is already defined.</p
>
</div>
</div>
<div class="col-sm-4 input-group input-group-sm"></div>
<div class="col-sm-2 input-group input-group-sm"></div>
</div>
</div>
</div>
@ -272,54 +284,65 @@
<!-- has-override -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;" ng-if="config.Overriden">
<div ng-repeat="(keyIndex, overridenKey) in config.OverridenKeys" style="margin-top: 2px;">
<div class="col-md-1 col-sm-2" style="margin-left: 3px;" style="vertical-align: top;"></div>
<div class="input-group col-sm-3 input-group-sm" style="vertical-align: top;">
<span class="input-group-addon">configuration key</span>
<input type="text" class="form-control" ng-value="overridenKey.Key" disabled />
<div style="margin-top: 2px;">
<div class="col-sm-1 input-group input-group-sm" style="margin-left: 3px;"></div>
<div class="col-sm-3 input-group input-group-sm">
<span class="input-group-addon">configuration key</span>
<input type="text" class="form-control" ng-value="overridenKey.Key" disabled />
</div>
<div class="col-sm-3 input-group input-group-sm" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<div class="col-sm-12 input-group input-group-sm">
<span class="input-group-addon">path on disk</span>
<input
type="text"
class="form-control"
ng-model="overridenKey.Path"
placeholder="/etc/myapp/conf.d"
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
ng-disabled="ctrl.formValues.Containers.length > 1"
required
ng-change="ctrl.onChangeConfigurationPath()"
/>
</div>
</div>
<div class="input-group col-sm-4 btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT">
<i class="fa fa-list" aria-hidden="true"></i> Environment
</label>
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<i class="fa fa-file" aria-hidden="true"></i> Filesystem
</label>
</div>
</div>
<div
class="col-sm-3 input-group input-group-sm"
style="vertical-align: top;"
ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM"
ng-show="
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
"
>
<div class="input-group col-sm-12 input-group-sm">
<span class="input-group-addon">path on disk</span>
<input
type="text"
class="form-control"
ng-model="overridenKey.Path"
placeholder="/etc/myapp/conf.d"
name="overriden_key_path_{{ index }}_{{ keyIndex }}"
ng-disabled="ctrl.formValues.Containers.length > 1"
required
ng-change="ctrl.onChangeConfigurationPath()"
/>
</div>
<div
class="small text-warning"
style="margin-top: 5px;"
ng-show="
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
</ng-messages>
<p ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already used.</p
<div class="col-sm-1 input-group input-group-sm" style="margin-left: 3px;"></div>
<div class="col-sm-3 input-group input-group-sm"></div>
<div class="col-sm-3 input-group input-group-sm" ng-if="overridenKey.Type === ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<div
class="small text-warning"
style="margin-top: 5px;"
ng-show="
kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid ||
ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined
"
>
<ng-messages for="kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Path is required.</p>
</ng-messages>
<p ng-if="ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This path is already used.</p
>
</div>
</div>
</div>
<div class="input-group col-sm-3 btn-group btn-group-sm" style="vertical-align: top;">
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT">
<i class="fa fa-list" aria-hidden="true"></i> Environment
</label>
<label class="btn btn-primary" ng-model="overridenKey.Type" uib-btn-radio="ctrl.ApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM">
<i class="fa fa-file" aria-hidden="true"></i> Filesystem
</label>
<div class="col-sm-4 input-group input-group-sm"></div>
</div>
</div>
</div>
@ -342,12 +365,7 @@
<div class="form-group" ng-if="ctrl.storageClassAvailable()">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Persisted folders</label>
<span
class="label label-default interactive"
style="margin-left: 10px;"
ng-click="ctrl.addPersistedFolder()"
ng-if="!ctrl.isEditAndStatefulSet() && ctrl.formValues.Containers.length <= 1"
>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="ctrl.addPersistedFolder()" ng-if="ctrl.isAddPersistentFolderButtonShowed()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add persisted folder
</span>
</div>
@ -383,7 +401,7 @@
ng-model="persistedFolder.UseNewVolume"
uib-btn-radio="true"
ng-change="ctrl.useNewVolume($index)"
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
ng-disabled="ctrl.isNewVolumeButtonDisabled($index)"
>New volume</label
>
<label
@ -391,7 +409,7 @@
ng-model="persistedFolder.UseNewVolume"
uib-btn-radio="false"
ng-change="ctrl.useExistingVolume($index)"
ng-disabled="ctrl.availableVolumes.length === 0 || ctrl.application.ApplicationType === ctrl.ApplicationTypes.STATEFULSET"
ng-disabled="ctrl.isExistingVolumeButtonDisabled()"
>Existing volume</label
>
</span>
@ -419,12 +437,7 @@
</span>
</div>
<div
class="input-group col-sm-2 input-group-sm"
ng-class="{ striked: persistedFolder.NeedsDeletion }"
style="vertical-align: top;"
ng-if="persistedFolder.UseNewVolume"
>
<div class="input-group col-sm-2 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" ng-if="persistedFolder.UseNewVolume">
<span class="input-group-addon">storage</span>
<select
ng-if="ctrl.hasMultipleStorageClassesAvailable()"
@ -452,7 +465,7 @@
</div>
<div class="input-group col-sm-1 input-group-sm">
<div style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet() && !ctrl.state.useExistingVolume[$index] && ctrl.formValues.Containers.length <= 1">
<div ng-if="!ctrl.isEditAndStatefulSet() && !ctrl.state.useExistingVolume[$index] && ctrl.formValues.Containers.length <= 1">
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
@ -489,7 +502,7 @@
<div class="input-group col-sm-2 input-group-sm"></div>
<div class="input-group col-sm-2 input-group-sm">
<div class="input-group col-sm-5 input-group-sm">
<div class="small text-warning" style="margin-top: 5px;" ng-show="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid">
<ng-messages for="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Size is required.</p>
@ -509,8 +522,6 @@
</div>
</div>
<div class="input-group col-sm-3 input-group-sm"> </div>
<div class="input-group col-sm-1 input-group-sm"> </div>
</div>
</div>
@ -569,7 +580,7 @@
</div>
<div
ng-if="
(!ctrl.state.isEdit && !ctrl.state.PersistedFoldersUseExistingVolumes) ||
(!ctrl.state.isEdit && !ctrl.state.persistedFoldersUseExistingVolumes) ||
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED)
"
>
@ -590,7 +601,7 @@
</div>
<div
style="color: #767676;"
ng-if="(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.PersistedFoldersUseExistingVolumes"
ng-if="(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.persistedFoldersUseExistingVolumes"
>
<input type="radio" id="data_access_isolated" disabled />
<label
@ -679,7 +690,8 @@
<div class="col-sm-12 small text-warning">
<div ng-messages="kubernetesApplicationCreationForm.memory_limit.$error">
<p
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.state.sliders.memory.min }} and {{ ctrl.state.sliders.memory.max }}
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.state.sliders.memory.min }} and
{{ ctrl.state.sliders.memory.max }}
</p>
</div>
</div>
@ -786,7 +798,7 @@
style="margin-left: 20px;"
ng-model="ctrl.formValues.ReplicaCount"
ng-disabled="!ctrl.supportScalableReplicaDeployment()"
ng-change="ctrl.onChangeVolumeRequestedSize()"
ng-change="ctrl.enforceReplicaCountMinimum()"
required
/>
</div>
@ -807,7 +819,8 @@
>
<div class="col-sm-12 small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application will reserve the following resources: <b>{{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU</b> and
This application will reserve the following resources:
<b>{{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU</b> and
<b>{{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB</b> of memory.
</div>
</div>

View file

@ -1,5 +1,5 @@
import angular from 'angular';
import * as _ from 'lodash-es';
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import * as JsonPatch from 'fast-json-patch';
@ -20,7 +20,7 @@ import {
KubernetesApplicationPersistedFolderFormValue,
KubernetesApplicationPublishedPortFormValue,
KubernetesApplicationPlacementFormValue,
KubernetesFormValueDuplicate,
KubernetesFormValidationReferences,
} from 'Kubernetes/models/application/formValues';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
@ -75,15 +75,8 @@ class KubernetesCreateApplicationController {
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
this.ServiceTypes = KubernetesServiceTypes;
this.onInit = this.onInit.bind(this);
this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
this.updateSlidersAsync = this.updateSlidersAsync.bind(this);
this.refreshStacksAsync = this.refreshStacksAsync.bind(this);
this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this);
this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this);
this.refreshNamespaceDataAsync = this.refreshNamespaceDataAsync.bind(this);
this.getApplicationAsync = this.getApplicationAsync.bind(this);
}
/* #endregion */
@ -92,7 +85,7 @@ class KubernetesCreateApplicationController {
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);
}
/* #region AUTO SCLAER UI MANAGEMENT */
/* #region AUTO SCALER UI MANAGEMENT */
unselectAutoScaler() {
if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) {
this.formValues.AutoScaler.IsUsed = false;
@ -156,7 +149,7 @@ class KubernetesCreateApplicationController {
});
});
this.state.duplicates.configurationPaths.hasDuplicates = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0;
this.state.duplicates.configurationPaths.hasRefs = Object.keys(this.state.duplicates.configurationPaths.refs).length > 0;
}
/* #endregion */
@ -184,7 +177,7 @@ class KubernetesCreateApplicationController {
onChangeEnvironmentName() {
this.state.duplicates.environmentVariables.refs = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name'));
this.state.duplicates.environmentVariables.hasDuplicates = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0;
this.state.duplicates.environmentVariables.hasRefs = Object.keys(this.state.duplicates.environmentVariables.refs).length > 0;
}
/* #endregion */
@ -195,12 +188,14 @@ class KubernetesCreateApplicationController {
storageClass = this.storageClasses[0];
}
this.formValues.PersistedFolders.push(new KubernetesApplicationPersistedFolderFormValue(storageClass));
const newPf = new KubernetesApplicationPersistedFolderFormValue(storageClass);
this.formValues.PersistedFolders.push(newPf);
this.resetDeploymentType();
}
restorePersistedFolder(index) {
this.formValues.PersistedFolders[index].NeedsDeletion = false;
this.validatePersistedFolders();
}
resetPersistedFolders() {
@ -208,6 +203,7 @@ class KubernetesCreateApplicationController {
persistedFolder.ExistingVolume = null;
persistedFolder.UseNewVolume = true;
});
this.validatePersistedFolders();
}
removePersistedFolder(index) {
@ -216,6 +212,29 @@ class KubernetesCreateApplicationController {
} else {
this.formValues.PersistedFolders.splice(index, 1);
}
this.validatePersistedFolders();
}
useNewVolume(index) {
this.formValues.PersistedFolders[index].UseNewVolume = true;
this.formValues.PersistedFolders[index].ExistingVolume = null;
this.state.persistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true);
this.validatePersistedFolders();
}
useExistingVolume(index) {
this.formValues.PersistedFolders[index].UseNewVolume = false;
this.state.persistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false;
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
this.resetDeploymentType();
}
this.validatePersistedFolders();
}
/* #endregion */
/* #region PERSISTENT FOLDERS ON CHANGE VALIDATION */
validatePersistedFolders() {
this.onChangePersistedFolderPath();
this.onChangeExistingVolumeSelection();
}
@ -229,31 +248,19 @@ class KubernetesCreateApplicationController {
return persistedFolder.ContainerPath;
})
);
this.state.duplicates.persistedFolders.hasDuplicates = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
this.state.duplicates.persistedFolders.hasRefs = Object.keys(this.state.duplicates.persistedFolders.refs).length > 0;
}
onChangeExistingVolumeSelection() {
this.state.duplicates.existingVolumes.refs = KubernetesFormValidationHelper.getDuplicates(
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
if (persistedFolder.NeedsDeletion) {
return undefined;
}
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
})
);
this.state.duplicates.existingVolumes.hasDuplicates = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0;
}
useNewVolume(index) {
this.formValues.PersistedFolders[index].UseNewVolume = true;
this.formValues.PersistedFolders[index].ExistingVolume = null;
this.state.PersistedFoldersUseExistingVolumes = !_.reduce(this.formValues.PersistedFolders, (acc, pf) => acc && pf.UseNewVolume, true);
}
useExistingVolume(index) {
this.formValues.PersistedFolders[index].UseNewVolume = false;
this.state.PersistedFoldersUseExistingVolumes = _.find(this.formValues.PersistedFolders, { UseNewVolume: false }) ? true : false;
if (this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED) {
this.formValues.DataAccessPolicy = this.ApplicationDataAccessPolicies.SHARED;
this.resetDeploymentType();
}
this.state.duplicates.existingVolumes.hasRefs = Object.keys(this.state.duplicates.existingVolumes.refs).length > 0;
}
/* #endregion */
@ -296,7 +303,7 @@ class KubernetesCreateApplicationController {
const source = _.map(this.formValues.Placements, (p) => (p.NeedsDeletion ? undefined : p.Label.Key));
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
state.hasRefs = Object.keys(duplicates).length > 0;
}
/* #endregion */
@ -351,10 +358,10 @@ class KubernetesCreateApplicationController {
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.ContainerPort + p.Protocol));
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
state.hasRefs = Object.keys(duplicates).length > 0;
} else {
state.refs = {};
state.hasDuplicates = false;
state.hasRefs = false;
}
}
@ -364,10 +371,10 @@ class KubernetesCreateApplicationController {
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.NodePort));
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
state.hasRefs = Object.keys(duplicates).length > 0;
} else {
state.refs = {};
state.hasDuplicates = false;
state.hasRefs = false;
}
}
@ -382,9 +389,9 @@ class KubernetesCreateApplicationController {
const state = this.state.duplicates.publishedPorts.ingressRoutes;
if (this.formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined));
const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? (p.IngressHost || p.IngressName) + p.IngressRoute : undefined));
const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => (p.Host || i.Name) + p.Path));
const newRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.IsNew && p.IngressRoute ? `${p.IngressHost || p.IngressName}${p.IngressRoute}` : undefined));
const toDelRoutes = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion && p.IngressRoute ? `${p.IngressHost || p.IngressName}${p.IngressRoute}` : undefined));
const allRoutes = _.flatMap(this.ingresses, (i) => _.map(i.Paths, (p) => `${p.Host || i.Name}${p.Path}`));
const duplicates = KubernetesFormValidationHelper.getDuplicates(newRoutes);
_.forEach(newRoutes, (route, idx) => {
if (_.includes(allRoutes, route) && !_.includes(toDelRoutes, route)) {
@ -392,10 +399,10 @@ class KubernetesCreateApplicationController {
}
});
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
state.hasRefs = Object.keys(duplicates).length > 0;
} else {
state.refs = {};
state.hasDuplicates = false;
state.hasRefs = false;
}
}
@ -405,10 +412,10 @@ class KubernetesCreateApplicationController {
const source = _.map(this.formValues.PublishedPorts, (p) => (p.NeedsDeletion ? undefined : p.LoadBalancerPort));
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
state.hasRefs = Object.keys(duplicates).length > 0;
} else {
state.refs = {};
state.hasDuplicates = false;
state.hasRefs = false;
}
}
@ -428,14 +435,14 @@ class KubernetesCreateApplicationController {
isValid() {
return (
!this.state.alreadyExists &&
!this.state.duplicates.environmentVariables.hasDuplicates &&
!this.state.duplicates.persistedFolders.hasDuplicates &&
!this.state.duplicates.configurationPaths.hasDuplicates &&
!this.state.duplicates.existingVolumes.hasDuplicates &&
!this.state.duplicates.publishedPorts.containerPorts.hasDuplicates &&
!this.state.duplicates.publishedPorts.nodePorts.hasDuplicates &&
!this.state.duplicates.publishedPorts.ingressRoutes.hasDuplicates &&
!this.state.duplicates.publishedPorts.loadBalancerPorts.hasDuplicates
!this.state.duplicates.environmentVariables.hasRefs &&
!this.state.duplicates.persistedFolders.hasRefs &&
!this.state.duplicates.configurationPaths.hasRefs &&
!this.state.duplicates.existingVolumes.hasRefs &&
!this.state.duplicates.publishedPorts.containerPorts.hasRefs &&
!this.state.duplicates.publishedPorts.nodePorts.hasRefs &&
!this.state.duplicates.publishedPorts.ingressRoutes.hasRefs &&
!this.state.duplicates.publishedPorts.loadBalancerPorts.hasRefs
);
}
@ -501,12 +508,20 @@ class KubernetesCreateApplicationController {
if (folder.StorageClass && _.isEqual(folder.StorageClass.AccessModes, ['RWO'])) {
storageOptions.push(folder.StorageClass.Name);
} else {
storageOptions.push('<no storage option available>');
}
}
return _.uniq(storageOptions).join(', ');
}
enforceReplicaCountMinimum() {
if (this.formValues.ReplicaCount === null) {
this.formValues.ReplicaCount = 1;
}
}
resourceQuotaCapacityExceeded() {
return !this.state.sliders.memory.max || !this.state.sliders.cpu.max;
}
@ -562,9 +577,29 @@ class KubernetesCreateApplicationController {
return !this.editChanges.length;
}
/* #region PERSISTED FOLDERS */
/* #region BUTTONS STATES */
isAddPersistentFolderButtonShowed() {
return !this.isEditAndStatefulSet() && this.formValues.Containers.length <= 1;
}
isNewVolumeButtonDisabled(index) {
return this.isEditAndExistingPersistedFolder(index);
}
isExistingVolumeButtonDisabled() {
return !this.hasAvailableVolumes() || (this.isEdit && this.application.ApplicationType === this.ApplicationTypes.STATEFULSET);
}
/* #endregion */
hasAvailableVolumes() {
return this.availableVolumes.length > 0;
}
isEditAndExistingPersistedFolder(index) {
return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName;
}
/* #endregion */
isEditAndNotNewPublishedPort(index) {
return this.state.isEdit && !this.formValues.PublishedPorts[index].IsNew;
@ -639,126 +674,126 @@ class KubernetesCreateApplicationController {
/* #endregion */
/* #region DATA AUTO REFRESH */
async updateSlidersAsync() {
try {
const quota = this.formValues.ResourcePool.Quota;
let minCpu,
maxCpu,
minMemory,
maxMemory = 0;
if (quota) {
updateSliders() {
this.state.resourcePoolHasQuota = false;
const quota = this.formValues.ResourcePool.Quota;
let minCpu,
maxCpu,
minMemory,
maxMemory = 0;
if (quota) {
if (quota.CpuLimit) {
this.state.resourcePoolHasQuota = true;
if (quota.CpuLimit) {
minCpu = KubernetesApplicationQuotaDefaults.CpuLimit;
maxCpu = quota.CpuLimit - quota.CpuLimitUsed;
if (this.state.isEdit && this.savedFormValues.CpuLimit) {
maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount;
}
} else {
minCpu = 0;
maxCpu = this.state.nodes.cpu;
}
if (quota.MemoryLimit) {
minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit;
maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed;
if (this.state.isEdit && this.savedFormValues.MemoryLimit) {
maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount;
}
} else {
minMemory = 0;
maxMemory = this.state.nodes.memory;
minCpu = KubernetesApplicationQuotaDefaults.CpuLimit;
maxCpu = quota.CpuLimit - quota.CpuLimitUsed;
if (this.state.isEdit && this.savedFormValues.CpuLimit) {
maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount;
}
} else {
this.state.resourcePoolHasQuota = false;
minCpu = 0;
maxCpu = this.state.nodes.cpu;
}
if (quota.MemoryLimit) {
this.state.resourcePoolHasQuota = true;
minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit;
maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed;
if (this.state.isEdit && this.savedFormValues.MemoryLimit) {
maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount;
}
} else {
minMemory = 0;
maxMemory = this.state.nodes.memory;
}
this.state.sliders.memory.min = minMemory;
this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory);
this.state.sliders.cpu.min = minCpu;
this.state.sliders.cpu.max = _.round(maxCpu, 2);
if (!this.state.isEdit) {
this.formValues.CpuLimit = minCpu;
this.formValues.MemoryLimit = minMemory;
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to update resources selector');
} else {
minCpu = 0;
maxCpu = this.state.nodes.cpu;
minMemory = 0;
maxMemory = this.state.nodes.memory;
}
}
updateSliders() {
return this.$async(this.updateSlidersAsync);
}
async refreshStacksAsync(namespace) {
try {
this.stacks = await this.KubernetesStackService.get(namespace);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve stacks');
this.state.sliders.memory.min = minMemory;
this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory);
this.state.sliders.cpu.min = minCpu;
this.state.sliders.cpu.max = _.round(maxCpu, 2);
if (!this.state.isEdit) {
this.formValues.CpuLimit = minCpu;
this.formValues.MemoryLimit = minMemory;
}
}
refreshStacks(namespace) {
return this.$async(this.refreshStacksAsync, namespace);
}
async refreshConfigurationsAsync(namespace) {
try {
this.configurations = await this.KubernetesConfigurationService.get(namespace);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
}
return this.$async(async () => {
try {
this.stacks = await this.KubernetesStackService.get(namespace);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve stacks');
}
});
}
refreshConfigurations(namespace) {
return this.$async(this.refreshConfigurationsAsync, namespace);
}
async refreshApplicationsAsync(namespace) {
try {
this.applications = await this.KubernetesApplicationService.get(namespace);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
}
return this.$async(async () => {
try {
this.configurations = await this.KubernetesConfigurationService.get(namespace);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve configurations');
}
});
}
refreshApplications(namespace) {
return this.$async(this.refreshApplicationsAsync, namespace);
return this.$async(async () => {
try {
this.applications = await this.KubernetesApplicationService.get(namespace);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve applications');
}
});
}
refreshVolumes(namespace) {
const filteredVolumes = _.filter(this.volumes, (volume) => {
const isSameNamespace = volume.ResourcePool.Namespace.Name === namespace;
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
const isRWX = volume.PersistentVolumeClaim.StorageClass && _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX');
return isSameNamespace && (isUnused || isRWX);
return this.$async(async () => {
try {
const volumes = await this.KubernetesVolumeService.get(namespace);
_.forEach(volumes, (volume) => {
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, this.applications);
});
this.volumes = volumes;
const filteredVolumes = _.filter(this.volumes, (volume) => {
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
const isRWX = volume.PersistentVolumeClaim.StorageClass && _.includes(volume.PersistentVolumeClaim.StorageClass.AccessModes, 'RWX');
return isUnused || isRWX;
});
this.availableVolumes = filteredVolumes;
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve volumes');
}
});
this.availableVolumes = filteredVolumes;
}
refreshIngresses(namespace) {
this.filteredIngresses = _.filter(this.ingresses, { Namespace: namespace });
if (!this.publishViaIngressEnabled()) {
this.formValues.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL;
if (this.savedFormValues) {
this.formValues.PublishingType = this.savedFormValues.PublishingType;
} else {
this.formValues.PublishingType = this.ApplicationPublishingTypes.INTERNAL;
}
}
this.formValues.OriginalIngresses = this.filteredIngresses;
}
async refreshNamespaceDataAsync(namespace) {
await Promise.all([
this.refreshStacks(namespace),
this.refreshConfigurations(namespace),
this.refreshApplications(namespace),
this.refreshIngresses(namespace),
this.refreshVolumes(namespace),
]);
this.onChangeName();
}
refreshNamespaceData(namespace) {
return this.$async(this.refreshNamespaceDataAsync, namespace);
return this.$async(async () => {
await Promise.all([
this.refreshStacks(namespace),
this.refreshConfigurations(namespace),
this.refreshApplications(namespace),
this.refreshIngresses(namespace),
this.refreshVolumes(namespace),
]);
this.onChangeName();
});
}
resetFormValues() {
@ -768,10 +803,12 @@ class KubernetesCreateApplicationController {
}
onResourcePoolSelectionChange() {
const namespace = this.formValues.ResourcePool.Namespace.Name;
this.updateSliders();
this.refreshNamespaceData(namespace);
this.resetFormValues();
return this.$async(async () => {
const namespace = this.formValues.ResourcePool.Namespace.Name;
this.updateSliders();
await this.refreshNamespaceData(namespace);
this.resetFormValues();
});
}
/* #endregion */
@ -818,154 +855,143 @@ class KubernetesCreateApplicationController {
/* #endregion */
/* #region APPLICATION - used on edit context only */
async getApplicationAsync() {
try {
const namespace = this.state.params.namespace;
[this.application, this.persistentVolumeClaims] = await Promise.all([
this.KubernetesApplicationService.get(namespace, this.state.params.name),
this.KubernetesPersistentVolumeClaimService.get(namespace),
]);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
}
}
getApplication() {
return this.$async(this.getApplicationAsync);
return this.$async(async () => {
try {
const namespace = this.state.params.namespace;
[this.application, this.persistentVolumeClaims] = await Promise.all([
this.KubernetesApplicationService.get(namespace, this.state.params.name),
this.KubernetesPersistentVolumeClaimService.get(namespace),
]);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve application details');
}
});
}
/* #endregion */
/* #region ON INIT */
async onInit() {
try {
this.state = {
actionInProgress: false,
useLoadBalancer: false,
useServerMetrics: false,
sliders: {
cpu: {
min: 0,
max: 0,
},
memory: {
min: 0,
max: 0,
},
},
nodes: {
memory: 0,
cpu: 0,
},
resourcePoolHasQuota: false,
viewReady: false,
availableSizeUnits: ['MB', 'GB', 'TB'],
alreadyExists: false,
duplicates: {
environmentVariables: new KubernetesFormValueDuplicate(),
persistedFolders: new KubernetesFormValueDuplicate(),
configurationPaths: new KubernetesFormValueDuplicate(),
existingVolumes: new KubernetesFormValueDuplicate(),
publishedPorts: {
containerPorts: new KubernetesFormValueDuplicate(),
nodePorts: new KubernetesFormValueDuplicate(),
ingressRoutes: new KubernetesFormValueDuplicate(),
loadBalancerPorts: new KubernetesFormValueDuplicate(),
},
placements: new KubernetesFormValueDuplicate(),
},
isEdit: false,
params: {
namespace: this.$transition$.params().namespace,
name: this.$transition$.params().name,
},
PersistedFoldersUseExistingVolumes: false,
};
this.isAdmin = this.Authentication.isAdmin();
this.editChanges = [];
if (this.$transition$.params().namespace && this.$transition$.params().name) {
this.state.isEdit = true;
}
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();
const [resourcePools, nodes, ingresses] = await Promise.all([
this.KubernetesResourcePoolService.get(),
this.KubernetesNodeService.get(),
this.KubernetesIngressService.get(),
]);
this.ingresses = ingresses;
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.formValues.ResourcePool = this.resourcePools[0];
// TODO: refactor @Max
// Don't pull all volumes and applications across all namespaces
// Use refreshNamespaceData flow (triggered on Init + on Namespace change)
// and query only accross the selected namespace
if (this.storageClassAvailable()) {
const [applications, volumes] = await Promise.all([this.KubernetesApplicationService.get(), this.KubernetesVolumeService.get()]);
this.volumes = volumes;
_.forEach(this.volumes, (volume) => {
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
});
}
_.forEach(nodes, (item) => {
this.state.nodes.memory += filesizeParser(item.Memory);
this.state.nodes.cpu += item.CPU;
});
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
await this.refreshNamespaceData(namespace);
if (this.state.isEdit) {
await this.getApplication();
this.formValues = KubernetesApplicationConverter.applicationToFormValues(
this.application,
this.resourcePools,
this.configurations,
this.persistentVolumeClaims,
this.nodesLabels
);
this.formValues.OriginalIngresses = this.filteredIngresses;
this.savedFormValues = angular.copy(this.formValues);
delete this.formValues.ApplicationType;
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
const volume = _.find(this.availableVolumes, (vol) => vol.PersistentVolumeClaim.Name === persistedFolder.PersistentVolumeClaimName);
if (volume) {
persistedFolder.UseNewVolume = false;
persistedFolder.ExistingVolume = volume;
}
});
}
} else {
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
}
await this.updateSliders();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
}
$onInit() {
return this.$async(this.onInit);
return this.$async(async () => {
try {
this.state = {
actionInProgress: false,
useLoadBalancer: false,
useServerMetrics: false,
sliders: {
cpu: {
min: 0,
max: 0,
},
memory: {
min: 0,
max: 0,
},
},
nodes: {
memory: 0,
cpu: 0,
},
resourcePoolHasQuota: false,
viewReady: false,
availableSizeUnits: ['MB', 'GB', 'TB'],
alreadyExists: false,
duplicates: {
environmentVariables: new KubernetesFormValidationReferences(),
persistedFolders: new KubernetesFormValidationReferences(),
configurationPaths: new KubernetesFormValidationReferences(),
existingVolumes: new KubernetesFormValidationReferences(),
publishedPorts: {
containerPorts: new KubernetesFormValidationReferences(),
nodePorts: new KubernetesFormValidationReferences(),
ingressRoutes: new KubernetesFormValidationReferences(),
loadBalancerPorts: new KubernetesFormValidationReferences(),
},
placements: new KubernetesFormValidationReferences(),
},
isEdit: false,
params: {
namespace: this.$transition$.params().namespace,
name: this.$transition$.params().name,
},
persistedFoldersUseExistingVolumes: false,
};
this.isAdmin = this.Authentication.isAdmin();
this.editChanges = [];
if (this.state.params.namespace && this.state.params.name) {
this.state.isEdit = true;
}
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();
const [resourcePools, nodes, ingresses] = await Promise.all([
this.KubernetesResourcePoolService.get(),
this.KubernetesNodeService.get(),
this.KubernetesIngressService.get(),
]);
this.ingresses = ingresses;
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.formValues.ResourcePool = this.resourcePools[0];
if (!this.formValues.ResourcePool) {
return;
}
_.forEach(nodes, (item) => {
this.state.nodes.memory += filesizeParser(item.Memory);
this.state.nodes.cpu += item.CPU;
});
this.nodesLabels = KubernetesNodeHelper.generateNodeLabelsFromNodes(nodes);
const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name;
await this.refreshNamespaceData(namespace);
if (this.state.isEdit) {
await this.getApplication();
this.formValues = KubernetesApplicationConverter.applicationToFormValues(
this.application,
this.resourcePools,
this.configurations,
this.persistentVolumeClaims,
this.nodesLabels
);
this.formValues.OriginalIngresses = this.filteredIngresses;
this.savedFormValues = angular.copy(this.formValues);
delete this.formValues.ApplicationType;
if (this.application.ApplicationType !== KubernetesApplicationTypes.STATEFULSET) {
_.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
const volume = _.find(this.availableVolumes, ['PersistentVolumeClaim.Name', persistedFolder.PersistentVolumeClaimName]);
if (volume) {
persistedFolder.UseNewVolume = false;
persistedFolder.ExistingVolume = volume;
}
});
}
await this.refreshNamespaceData(namespace);
} else {
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(null, this.formValues.ReplicaCount);
this.formValues.OriginalIngressClasses = angular.copy(this.ingresses);
}
this.updateSliders();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
});
}
/* #endregion */
}

View file

@ -1,5 +1,5 @@
import angular from 'angular';
import * as _ from 'lodash-es';
import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch';
import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
@ -67,8 +67,8 @@ function computeAffinities(nodes, application) {
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST && !exists) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN && isIn) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN && !isIn) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key]) > parseInt(e.values[0])) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key]) < parseInt(e.values[0]))
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key], 10) > parseInt(e.values[0], 10)) ||
(e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key], 10) < parseInt(e.values[0], 10))
) {
return;
}

View file

@ -1,4 +1,4 @@
import * as _ from 'lodash-es';
import _ from 'lodash-es';
angular.module('portainer.docker').controller('KubernetesApplicationPlacementsDatatableController', function ($scope, $controller, DatatableService, Authentication) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));

View file

@ -56,7 +56,6 @@
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
></select>
</div>
</div>

View file

@ -1,7 +1,7 @@
import * as _ from 'lodash-es';
import _ from 'lodash-es';
import angular from 'angular';
import { KubernetesStorageClass, KubernetesStorageClassAccessPolicies } from 'Kubernetes/models/storage-class/models';
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
import { KubernetesIngressClass } from 'Kubernetes/ingress/models';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
@ -82,7 +82,7 @@ class KubernetesConfigureController {
const source = _.map(this.formValues.IngressClasses, (ic) => (ic.NeedsDeletion ? undefined : ic.Name));
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
state.hasRefs = Object.keys(duplicates).length > 0;
}
onChangeIngressClassName(index) {
@ -212,7 +212,7 @@ class KubernetesConfigureController {
viewReady: false,
endpointId: this.$stateParams.id,
duplicates: {
ingressClasses: new KubernetesFormValueDuplicate(),
ingressClasses: new KubernetesFormValidationReferences(),
},
};

View file

@ -6,7 +6,7 @@ import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceRese
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues';
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
class KubernetesCreateResourcePoolController {
@ -44,7 +44,7 @@ class KubernetesCreateResourcePoolController {
}
});
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
state.hasRefs = Object.keys(duplicates).length > 0;
}
/* #region ANNOTATIONS MANAGEMENT */
@ -58,7 +58,7 @@ class KubernetesCreateResourcePoolController {
/* #endregion */
isCreateButtonDisabled() {
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.isAlreadyExist || this.state.duplicates.ingressHosts.hasDuplicates;
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.isAlreadyExist || this.state.duplicates.ingressHosts.hasRefs;
}
onChangeName() {
@ -109,13 +109,10 @@ class KubernetesCreateResourcePoolController {
/* #region GET INGRESSES */
async getIngressesAsync() {
this.state.ingressesLoading = true;
try {
this.allIngresses = await this.KubernetesIngressService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
} finally {
this.state.ingressesLoading = false;
}
}
@ -154,7 +151,7 @@ class KubernetesCreateResourcePoolController {
isAlreadyExist: false,
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
duplicates: {
ingressHosts: new KubernetesFormValueDuplicate(),
ingressHosts: new KubernetesFormValidationReferences(),
},
};

View file

@ -36,6 +36,17 @@
<p> <i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 2px;"></i> At least a single limit must be set for the quota to be valid. </p>
</span>
</div>
<div ng-if="ctrl.formValues.HasQuota">
<kubernetes-resource-reservation
ng-if="ctrl.pool.Quota"
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
cpu="ctrl.state.cpuUsed"
memory="ctrl.state.memoryUsed"
cpu-limit="ctrl.formValues.CpuLimit"
memory-limit="ctrl.formValues.MemoryLimit"
>
</kubernetes-resource-reservation>
</div>
<!-- !quotas-switch -->
<div ng-if="ctrl.formValues.HasQuota && ctrl.isAdmin && ctrl.isEditable">
<div class="col-sm-12 form-section-title">
@ -50,7 +61,7 @@
<div class="col-sm-3">
<slider
model="ctrl.formValues.MemoryLimit"
floor="ctrl.defaults.MemoryLimit"
floor="ctrl.ResourceQuotaDefaults.MemoryLimit"
ceil="ctrl.state.sliderMaxMemory"
step="128"
ng-if="ctrl.state.sliderMaxMemory"
@ -60,7 +71,7 @@
<input
name="memory_limit"
type="number"
min="{{ ctrl.defaults.MemoryLimit }}"
min="{{ ctrl.ResourceQuotaDefaults.MemoryLimit }}"
max="{{ ctrl.state.sliderMaxMemory }}"
class="form-control"
ng-model="ctrl.formValues.MemoryLimit"
@ -78,7 +89,7 @@
<div class="col-sm-12 small text-warning">
<div ng-messages="resourcePoolEditForm.pool_name.$error">
<p
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.defaults.MemoryLimit }} and
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Value must be between {{ ctrl.ResourceQuotaDefaults.MemoryLimit }} and
{{ ctrl.state.sliderMaxMemory }}</p
>
</div>
@ -93,7 +104,7 @@
<div class="col-sm-5">
<slider
model="ctrl.formValues.CpuLimit"
floor="ctrl.defaults.CpuLimit"
floor="ctrl.ResourceQuotaDefaults.CpuLimit"
ceil="ctrl.state.sliderMaxCpu"
step="0.1"
precision="2"
@ -109,17 +120,31 @@
<!-- !cpu-limit-input -->
</div>
</div>
<div ng-if="ctrl.formValues.HasQuota">
<kubernetes-resource-reservation
ng-if="ctrl.pool.Quota"
description="Resource reservation represents the total amount of resource assigned to all the applications deployed inside this resource pool."
cpu="ctrl.state.cpuUsed"
memory="ctrl.state.memoryUsed"
cpu-limit="ctrl.formValues.CpuLimit"
memory-limit="ctrl.formValues.MemoryLimit"
>
</kubernetes-resource-reservation>
<!-- #region LOADBALANCERS -->
<div class="col-sm-12 form-section-title">
Load balancers
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use
of load balancers in this resource pool.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Load Balancer quota
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
</span>
</div>
</div>
<!-- #endregion -->
<div ng-if="ctrl.isAdmin && ctrl.isEditable && ctrl.state.canUseIngress">
<div class="col-sm-12 form-section-title">
Ingresses
@ -250,33 +275,7 @@
</div>
<!-- #endregion -->
</div>
<!-- #region LOAD-BALANCERS -->
<div class="col-sm-12 form-section-title">
Load balancers
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
You can set a quota on the amount of external load balancers that can be created inside this resource pool. Set this quota to 0 to effectively disable the use
of load balancers in this resource pool.
</span>
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Load Balancer quota
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
</span>
</div>
</div>
<!-- #endregion -->
<!-- #region LOAD-BALANCERS -->
<!-- #region STORAGES -->
<div class="col-sm-12 form-section-title">
Storages
</div>

View file

@ -1,14 +1,15 @@
import angular from 'angular';
import * as _ from 'lodash-es';
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import { KubernetesResourcePoolFormValues, KubernetesResourcePoolIngressClassAnnotationFormValue } from 'Kubernetes/models/resource-pool/formValues';
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
import { KubernetesFormValueDuplicate } from 'Kubernetes/models/application/formValues';
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
class KubernetesResourcePoolController {
/* #region CONSTRUCTOR */
@ -28,7 +29,8 @@ class KubernetesResourcePoolController {
KubernetesPodService,
KubernetesApplicationService,
KubernetesNamespaceHelper,
KubernetesIngressService
KubernetesIngressService,
KubernetesVolumeService
) {
this.$async = $async;
this.$state = $state;
@ -46,18 +48,17 @@ class KubernetesResourcePoolController {
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesIngressService = KubernetesIngressService;
this.KubernetesVolumeService = KubernetesVolumeService;
this.IngressClassTypes = KubernetesIngressClassTypes;
this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults;
this.onInit = this.onInit.bind(this);
this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this);
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
this.getEvents = this.getEvents.bind(this);
this.getEventsAsync = this.getEventsAsync.bind(this);
this.getApplications = this.getApplications.bind(this);
this.getApplicationsAsync = this.getApplicationsAsync.bind(this);
this.getIngresses = this.getIngresses.bind(this);
this.getIngressesAsync = this.getIngressesAsync.bind(this);
}
/* #endregion */
@ -74,7 +75,7 @@ class KubernetesResourcePoolController {
}
});
state.refs = duplicates;
state.hasDuplicates = Object.keys(duplicates).length > 0;
state.hasRefs = Object.keys(duplicates).length > 0;
}
/* #region ANNOTATIONS MANAGEMENT */
@ -92,7 +93,7 @@ class KubernetesResourcePoolController {
}
isUpdateButtonDisabled() {
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.duplicates.ingressHosts.hasDuplicates;
return this.state.actionInProgress || (this.formValues.HasQuota && !this.isQuotaValid()) || this.state.duplicates.ingressHosts.hasRefs;
}
isQuotaValid() {
@ -107,11 +108,11 @@ class KubernetesResourcePoolController {
}
checkDefaults() {
if (this.formValues.CpuLimit < this.defaults.CpuLimit) {
this.formValues.CpuLimit = this.defaults.CpuLimit;
if (this.formValues.CpuLimit < KubernetesResourceQuotaDefaults.CpuLimit) {
this.formValues.CpuLimit = KubernetesResourceQuotaDefaults.CpuLimit;
}
if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit)) {
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit);
if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(KubernetesResourceQuotaDefaults.MemoryLimit)) {
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(KubernetesResourceQuotaDefaults.MemoryLimit);
}
}
@ -140,45 +141,12 @@ class KubernetesResourcePoolController {
return false;
}
/* #region UPDATE RESOURCE POOL */
async updateResourcePoolAsync() {
this.state.actionInProgress = true;
try {
this.checkDefaults();
const namespace = this.pool.Namespace.Name;
const cpuLimit = this.formValues.CpuLimit;
const memoryLimit = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit);
const owner = this.pool.Namespace.ResourcePoolOwner;
const quota = this.pool.Quota;
if (this.formValues.HasQuota) {
if (quota) {
quota.CpuLimit = cpuLimit;
quota.MemoryLimit = memoryLimit;
await this.KubernetesResourceQuotaService.update(quota);
} else {
await this.createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit);
}
} else if (quota) {
await this.KubernetesResourceQuotaService.delete(quota);
}
const promises = _.map(this.formValues.IngressClasses, (c) => {
c.Namespace = namespace;
if (c.WasSelected === false && c.Selected === true) {
const ingress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
return this.KubernetesIngressService.create(ingress);
} else if (c.WasSelected === true && c.Selected === false) {
return this.KubernetesIngressService.delete(c);
} else if (c.WasSelected === true && c.Selected === true) {
const oldIngress = _.find(this.ingresses, { Name: c.IngressClass.Name });
const newIngress = KubernetesIngressConverter.resourcePoolIngressClassFormValueToIngress(c);
newIngress.Paths = angular.copy(oldIngress.Paths);
newIngress.PreviousHost = oldIngress.Host;
return this.KubernetesIngressService.patch(oldIngress, newIngress);
}
});
await Promise.all(promises);
await this.KubernetesResourcePoolService.patch(this.savedFormValues, this.formValues);
this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name);
this.$state.reload();
} catch (err) {
@ -212,75 +180,70 @@ class KubernetesResourcePoolController {
return this.$async(this.updateResourcePoolAsync);
}
}
/* #endregion */
hasEventWarnings() {
return this.state.eventWarningCount;
}
/* #region GET EVENTS */
async getEventsAsync() {
try {
this.state.eventsLoading = true;
this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name);
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events');
} finally {
this.state.eventsLoading = false;
}
}
getEvents() {
return this.$async(this.getEventsAsync);
return this.$async(async () => {
try {
this.state.eventsLoading = true;
this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name);
this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events');
} finally {
this.state.eventsLoading = false;
}
});
}
/* #endregion */
/* #region GET APPLICATIONS */
async getApplicationsAsync() {
try {
this.state.applicationsLoading = true;
this.applications = await this.KubernetesApplicationService.get(this.pool.Namespace.Name);
this.applications = _.map(this.applications, (app) => {
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
app.CPU = resourceReservation.CPU;
app.Memory = resourceReservation.Memory;
return app;
});
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve applications.');
} finally {
this.state.applicationsLoading = false;
}
}
getApplications() {
return this.$async(this.getApplicationsAsync);
return this.$async(async () => {
try {
this.state.applicationsLoading = true;
this.applications = await this.KubernetesApplicationService.get(this.pool.Namespace.Name);
this.applications = _.map(this.applications, (app) => {
const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods);
app.CPU = resourceReservation.CPU;
app.Memory = resourceReservation.Memory;
return app;
});
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve applications.');
} finally {
this.state.applicationsLoading = false;
}
});
}
/* #endregion */
/* #region GET INGRESSES */
async getIngressesAsync() {
this.state.ingressesLoading = true;
try {
const namespace = this.pool.Namespace.Name;
this.allIngresses = await this.KubernetesIngressService.get();
this.ingresses = _.filter(this.allIngresses, { Namespace: namespace });
_.forEach(this.ingresses, (ing) => {
ing.Namespace = namespace;
_.forEach(ing.Paths, (path) => {
const application = _.find(this.applications, { ServiceName: path.ServiceName });
path.ApplicationName = application && application.Name ? application.Name : '-';
});
});
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
} finally {
this.state.ingressesLoading = false;
}
}
getIngresses() {
return this.$async(this.getIngressesAsync);
return this.$async(async () => {
this.state.ingressesLoading = true;
try {
const namespace = this.pool.Namespace.Name;
this.allIngresses = await this.KubernetesIngressService.get();
this.ingresses = _.filter(this.allIngresses, { Namespace: namespace });
_.forEach(this.ingresses, (ing) => {
ing.Namespace = namespace;
_.forEach(ing.Paths, (path) => {
const application = _.find(this.applications, { ServiceName: path.ServiceName });
path.ApplicationName = application && application.Name ? application.Name : '-';
});
});
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve ingresses.');
} finally {
this.state.ingressesLoading = false;
}
});
}
/* #endregion */
@ -290,9 +253,6 @@ class KubernetesResourcePoolController {
const endpoint = this.EndpointProvider.currentEndpoint();
this.endpoint = endpoint;
this.isAdmin = this.Authentication.isAdmin();
this.defaults = KubernetesResourceQuotaDefaults;
this.formValues = new KubernetesResourcePoolFormValues(this.defaults);
this.formValues.HasQuota = false;
this.state = {
actionInProgress: false,
@ -312,7 +272,7 @@ class KubernetesResourcePoolController {
eventWarningCount: 0,
canUseIngress: endpoint.Kubernetes.Configuration.IngressClasses.length,
duplicates: {
ingressHosts: new KubernetesFormValueDuplicate(),
ingressHosts: new KubernetesFormValidationReferences(),
},
};
@ -320,9 +280,11 @@ class KubernetesResourcePoolController {
const name = this.$transition$.params().id;
const [nodes, pool] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get(name)]);
const [nodes, pools] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get()]);
this.pool = pool;
this.pool = _.find(pools, { Namespace: { Name: name } });
this.formValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
this.formValues.Name = this.pool.Namespace.Name;
_.forEach(nodes, (item) => {
this.state.sliderMaxMemory += filesizeParser(item.Memory);
@ -330,13 +292,10 @@ class KubernetesResourcePoolController {
});
this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory);
const quota = pool.Quota;
const quota = this.pool.Quota;
if (quota) {
this.oldQuota = angular.copy(quota);
this.formValues.HasQuota = true;
this.formValues.CpuLimit = quota.CpuLimit;
this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit);
this.formValues = KubernetesResourceQuotaConverter.quotaToResourcePoolFormValues(quota);
this.state.cpuUsed = quota.CpuLimitUsed;
this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed);
}
@ -354,6 +313,7 @@ class KubernetesResourcePoolController {
const ingressClasses = endpoint.Kubernetes.Configuration.IngressClasses;
this.formValues.IngressClasses = KubernetesIngressConverter.ingressClassesToFormValues(ingressClasses, this.ingresses);
}
this.savedFormValues = angular.copy(this.formValues);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {

View file

@ -79,6 +79,7 @@
ng-model="ctrl.state.volumeSize"
placeholder="20"
ng-min="0"
min="0"
ng-change="ctrl.onChangeSize()"
required
/>
@ -100,11 +101,11 @@
</div>
<div class="form-inline">
<div class="small text-warning" style="margin-top: 5px;" ng-show="ctrl.state.volumeSizeError || kubernetesVolumeUpdateForm.size.$invalid">
<div class="small text-warning" style="margin-top: 5px;" ng-show="ctrl.state.errors.volumeSize || kubernetesVolumeUpdateForm.size.$invalid">
<div ng-messages="kubernetesVolumeUpdateForm.size.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
<p ng-show="ctrl.state.volumeSizeError && !kubernetesVolumeUpdateForm.size.$invalid"
<p ng-show="ctrl.state.errors.volumeSize && !kubernetesVolumeUpdateForm.size.$invalid"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> The new size must be greater than the actual size.</p
>
</div>

View file

@ -64,18 +64,17 @@ class KubernetesVolumeController {
onChangeSize() {
if (this.state.volumeSize) {
const size = filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit);
const size = filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit, { base: 10 });
if (this.state.oldVolumeSize > size) {
this.state.volumeSizeError = true;
this.state.errors.volumeSize = true;
} else {
this.volume.PersistentVolumeClaim.Storage = size;
this.state.volumeSizeError = false;
this.state.errors.volumeSize = false;
}
}
}
sizeIsValid() {
return !this.state.volumeSizeError && this.state.oldVolumeSize !== this.volume.PersistentVolumeClaim.Storage;
return !this.state.errors.volumeSize && this.state.volumeSize && this.state.oldVolumeSize !== filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit, { base: 10 });
}
/**
@ -84,7 +83,7 @@ class KubernetesVolumeController {
async updateVolumeAsync(redeploy) {
try {
this.volume.PersistentVolumeClaim.Storage = this.state.volumeSize + this.state.volumeSizeUnit.charAt(0) + 'i';
this.volume.PersistentVolumeClaim.Storage = this.state.volumeSize + this.state.volumeSizeUnit.charAt(0);
await this.KubernetesPersistentVolumeClaimService.patch(this.oldVolume.PersistentVolumeClaim, this.volume.PersistentVolumeClaim);
this.Notifications.success('Volume successfully updated');
@ -126,9 +125,9 @@ class KubernetesVolumeController {
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
this.volume = volume;
this.oldVolume = angular.copy(volume);
this.state.volumeSize = parseInt(volume.PersistentVolumeClaim.Storage.slice(0, -2));
this.state.volumeSize = parseInt(volume.PersistentVolumeClaim.Storage.slice(0, -2), 10);
this.state.volumeSizeUnit = volume.PersistentVolumeClaim.Storage.slice(-2);
this.state.oldVolumeSize = filesizeParser(volume.PersistentVolumeClaim.Storage);
this.state.oldVolumeSize = filesizeParser(volume.PersistentVolumeClaim.Storage, { base: 10 });
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve volume');
}
@ -179,9 +178,11 @@ class KubernetesVolumeController {
increaseSize: false,
volumeSize: 0,
volumeSizeUnit: 'GB',
volumeSizeError: false,
volumeSharedAccessPolicy: '',
volumeSharedAccessPolicyTooltip: '',
errors: {
volumeSize: false,
},
};
this.state.activeTab = this.LocalStorage.getActiveTab('volume');

View file

@ -1,9 +1,10 @@
require('../../templates/advancedDeploymentPanel.html');
import * as _ from 'lodash-es';
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import angular from 'angular';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper';
function buildStorages(storages, volumes) {
_.forEach(storages, (s) => {
@ -15,28 +16,9 @@ function buildStorages(storages, volumes) {
}
function computeSize(volumes) {
let hasT,
hasG,
hasM = false;
const size = _.sumBy(volumes, (v) => {
const storage = v.PersistentVolumeClaim.Storage;
if (!hasT && _.endsWith(storage, 'TB')) {
hasT = true;
} else if (!hasG && _.endsWith(storage, 'GB')) {
hasG = true;
} else if (!hasM && _.endsWith(storage, 'MB')) {
hasM = true;
}
return filesizeParser(storage, { base: 10 });
});
if (hasT) {
return size / 1000 / 1000 / 1000 / 1000 + 'TB';
} else if (hasG) {
return size / 1000 / 1000 / 1000 + 'GB';
} else if (hasM) {
return size / 1000 / 1000 + 'MB';
}
return size;
const size = _.sumBy(volumes, (v) => filesizeParser(v.PersistentVolumeClaim.Storage, { base: 10 }));
const format = KubernetesResourceQuotaHelper.formatBytes(size);
return `${format.Size}${format.SizeUnit}`;
}
class KubernetesVolumesController {