mirror of https://github.com/portainer/portainer
feat(application): Add the ability to use existing volumes when creating an application (#4044)
* feat(applications): update UI to use existing volumes * feat(application): Add the ability to use existing volumes when creating an application * feat(application): Existing persisted folders should default to associated volumes * feat(application): add form validation to existing volume * feat(application): remove the ability to use an existing volume with statefulset application * feat(k8s/applications): minor UI update * feat(k8s/application): minor UI update * feat(volume): allow to increase volume size and few other things * feat(volumes): add the ability to allow volume expansion * fix(storage): fix the storage patch request * fix(k8s/applications): remove conflict leftover * feat(k8s/configure): minor UI update * feat(k8s/volume): minor UI update * fix(storage): change few things Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>pull/4173/head
parent
b9c2bf487b
commit
61f97469ab
|
@ -344,9 +344,10 @@ type (
|
||||||
|
|
||||||
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
||||||
KubernetesStorageClassConfig struct {
|
KubernetesStorageClassConfig struct {
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
AccessModes []string `json:"AccessModes"`
|
AccessModes []string `json:"AccessModes"`
|
||||||
Provisioner string `json:"Provisioner"`
|
Provisioner string `json:"Provisioner"`
|
||||||
|
AllowVolumeExpansion bool `json:"AllowVolumeExpansion"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server
|
// LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server
|
||||||
|
|
|
@ -28,16 +28,28 @@ class KubernetesPersistentVolumeClaimConverter {
|
||||||
_.remove(formValues.PersistedFolders, (item) => item.NeedsDeletion);
|
_.remove(formValues.PersistedFolders, (item) => item.NeedsDeletion);
|
||||||
const res = _.map(formValues.PersistedFolders, (item) => {
|
const res = _.map(formValues.PersistedFolders, (item) => {
|
||||||
const pvc = new KubernetesPersistentVolumeClaim();
|
const pvc = new KubernetesPersistentVolumeClaim();
|
||||||
if (item.PersistentVolumeClaimName) {
|
if (!_.isEmpty(item.ExistingVolume)) {
|
||||||
pvc.Name = item.PersistentVolumeClaimName;
|
const existantPVC = item.ExistingVolume.PersistentVolumeClaim;
|
||||||
pvc.PreviousName = item.PersistentVolumeClaimName;
|
pvc.Name = existantPVC.Name;
|
||||||
|
if (item.PersistentVolumeClaimName) {
|
||||||
|
pvc.PreviousName = item.PersistentVolumeClaimName;
|
||||||
|
}
|
||||||
|
pvc.StorageClass = existantPVC.StorageClass;
|
||||||
|
pvc.Storage = existantPVC.Storage.charAt(0) + 'i';
|
||||||
|
pvc.CreationDate = existantPVC.CreationDate;
|
||||||
|
pvc.Id = existantPVC.Id;
|
||||||
} else {
|
} else {
|
||||||
pvc.Name = formValues.Name + '-' + pvc.Name;
|
if (item.PersistentVolumeClaimName) {
|
||||||
|
pvc.Name = item.PersistentVolumeClaimName;
|
||||||
|
pvc.PreviousName = item.PersistentVolumeClaimName;
|
||||||
|
} else {
|
||||||
|
pvc.Name = formValues.Name + '-' + pvc.Name;
|
||||||
|
}
|
||||||
|
pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i';
|
||||||
|
pvc.StorageClass = item.StorageClass;
|
||||||
}
|
}
|
||||||
pvc.MountPath = item.ContainerPath;
|
pvc.MountPath = item.ContainerPath;
|
||||||
pvc.Namespace = formValues.ResourcePool.Namespace.Name;
|
pvc.Namespace = formValues.ResourcePool.Namespace.Name;
|
||||||
pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i';
|
|
||||||
pvc.StorageClass = item.StorageClass;
|
|
||||||
pvc.ApplicationOwner = formValues.ApplicationOwner;
|
pvc.ApplicationOwner = formValues.ApplicationOwner;
|
||||||
pvc.ApplicationName = formValues.Name;
|
pvc.ApplicationName = formValues.Name;
|
||||||
return pvc;
|
return pvc;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { KubernetesStorageClass } from 'Kubernetes/models/storage-class/models';
|
import { KubernetesStorageClass } from 'Kubernetes/models/storage-class/models';
|
||||||
|
import { KubernetesStorageClassCreatePayload } from 'Kubernetes/models/storage-class/payload';
|
||||||
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
|
|
||||||
class KubernetesStorageClassConverter {
|
class KubernetesStorageClassConverter {
|
||||||
/**
|
/**
|
||||||
|
@ -8,8 +10,24 @@ class KubernetesStorageClassConverter {
|
||||||
const res = new KubernetesStorageClass();
|
const res = new KubernetesStorageClass();
|
||||||
res.Name = data.metadata.name;
|
res.Name = data.metadata.name;
|
||||||
res.Provisioner = data.provisioner;
|
res.Provisioner = data.provisioner;
|
||||||
|
res.AllowVolumeExpansion = data.allowVolumeExpansion;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static createPayload(storageClass) {
|
||||||
|
const res = new KubernetesStorageClassCreatePayload();
|
||||||
|
res.metadata.name = storageClass.Name;
|
||||||
|
res.provisioner = storageClass.Provisioner;
|
||||||
|
res.allowVolumeExpansion = storageClass.AllowVolumeExpansion;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static patchPayload(oldStorageClass, newStorageClass) {
|
||||||
|
const oldPayload = KubernetesStorageClassConverter.createPayload(oldStorageClass);
|
||||||
|
const newPayload = KubernetesStorageClassConverter.createPayload(newStorageClass);
|
||||||
|
const payload = JsonPatch.compare(oldPayload, newPayload);
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KubernetesStorageClassConverter;
|
export default KubernetesStorageClassConverter;
|
||||||
|
|
|
@ -92,6 +92,8 @@ const _KubernetesApplicationPersistedFolderFormValue = Object.freeze({
|
||||||
Size: '',
|
Size: '',
|
||||||
SizeUnit: 'GB',
|
SizeUnit: 'GB',
|
||||||
StorageClass: {},
|
StorageClass: {},
|
||||||
|
ExistingVolume: null,
|
||||||
|
UseNewVolume: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesApplicationPersistedFolderFormValue {
|
export class KubernetesApplicationPersistedFolderFormValue {
|
||||||
|
|
|
@ -25,6 +25,7 @@ const _KubernetesStorageClass = Object.freeze({
|
||||||
Name: '',
|
Name: '',
|
||||||
AccessModes: [],
|
AccessModes: [],
|
||||||
Provisioner: '',
|
Provisioner: '',
|
||||||
|
AllowVolumeExpansion: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesStorageClass {
|
export class KubernetesStorageClass {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KubernetesStorageClassCreatePayload Model
|
||||||
|
*/
|
||||||
|
const _KubernetesStorageClassCreatePayload = Object.freeze({
|
||||||
|
metadata: new KubernetesCommonMetadataPayload(),
|
||||||
|
provisioner: '',
|
||||||
|
allowVolumeExpansion: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class KubernetesStorageClassCreatePayload {
|
||||||
|
constructor() {
|
||||||
|
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStorageClassCreatePayload)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,12 @@ angular.module('portainer.kubernetes').factory('KubernetesStorage', [
|
||||||
},
|
},
|
||||||
create: { method: 'POST' },
|
create: { method: 'POST' },
|
||||||
update: { method: 'PUT' },
|
update: { method: 'PUT' },
|
||||||
|
patch: {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json-patch+json',
|
||||||
|
},
|
||||||
|
},
|
||||||
delete: { method: 'DELETE' },
|
delete: { method: 'DELETE' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -209,7 +209,7 @@ class KubernetesApplicationService {
|
||||||
app.ServiceName = headlessService.metadata.name;
|
app.ServiceName = headlessService.metadata.name;
|
||||||
} else {
|
} else {
|
||||||
const claimPromises = _.map(claims, (item) => {
|
const claimPromises = _.map(claims, (item) => {
|
||||||
if (!item.PreviousName) {
|
if (!item.PreviousName && !item.Id) {
|
||||||
return this.KubernetesPersistentVolumeClaimService.create(item);
|
return this.KubernetesPersistentVolumeClaimService.create(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -255,11 +255,12 @@ class KubernetesApplicationService {
|
||||||
await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService);
|
await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService);
|
||||||
} else {
|
} else {
|
||||||
const claimPromises = _.map(newClaims, (newClaim) => {
|
const claimPromises = _.map(newClaims, (newClaim) => {
|
||||||
if (!newClaim.PreviousName) {
|
if (!newClaim.PreviousName && !newClaim.Id) {
|
||||||
return this.KubernetesPersistentVolumeClaimService.create(newClaim);
|
return this.KubernetesPersistentVolumeClaimService.create(newClaim);
|
||||||
|
} else if (!newClaim.Id) {
|
||||||
|
const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName });
|
||||||
|
return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim);
|
||||||
}
|
}
|
||||||
const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName });
|
|
||||||
return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim);
|
|
||||||
});
|
});
|
||||||
await Promise.all(claimPromises);
|
await Promise.all(claimPromises);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import PortainerError from 'Portainer/error';
|
import PortainerError from 'Portainer/error';
|
||||||
import KubernetesStorageClassConverter from 'Kubernetes/converters/storageClass';
|
import KubernetesStorageClassConverter from 'Kubernetes/converters/storageClass';
|
||||||
|
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
|
||||||
|
|
||||||
class KubernetesStorageService {
|
class KubernetesStorageService {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -10,6 +11,7 @@ class KubernetesStorageService {
|
||||||
this.KubernetesStorage = KubernetesStorage;
|
this.KubernetesStorage = KubernetesStorage;
|
||||||
|
|
||||||
this.getAsync = this.getAsync.bind(this);
|
this.getAsync = this.getAsync.bind(this);
|
||||||
|
this.patchAsync = this.patchAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,6 +33,24 @@ class KubernetesStorageService {
|
||||||
get(endpointId) {
|
get(endpointId) {
|
||||||
return this.$async(this.getAsync, endpointId);
|
return this.$async(this.getAsync, endpointId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH
|
||||||
|
*/
|
||||||
|
async patchAsync(oldStorageClass, newStorageClass) {
|
||||||
|
try {
|
||||||
|
const params = new KubernetesCommonParams();
|
||||||
|
params.id = newStorageClass.Name;
|
||||||
|
const payload = KubernetesStorageClassConverter.patchPayload(oldStorageClass, newStorageClass);
|
||||||
|
await this.KubernetesStorage().patch(params, payload).$promise;
|
||||||
|
} catch (err) {
|
||||||
|
throw new PortainerError('Unable to patch storage class', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(oldStorageClass, newStorageClass) {
|
||||||
|
return this.$async(this.patchAsync, oldStorageClass, newStorageClass);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default KubernetesStorageService;
|
export default KubernetesStorageService;
|
||||||
|
|
|
@ -319,8 +319,8 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
<div class="col-sm-12 form-inline" style="margin-top: 10px;" ng-repeat="persistedFolder in ctrl.formValues.PersistedFolders">
|
||||||
<div ng-repeat-start="persistedFolder in ctrl.formValues.PersistedFolders" style="margin-top: 2px;">
|
<div style="margin-top: 2px;">
|
||||||
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
||||||
<span class="input-group-addon">path in container</span>
|
<span class="input-group-addon">path in container</span>
|
||||||
<input
|
<input
|
||||||
|
@ -329,12 +329,38 @@
|
||||||
name="persisted_folder_path_{{ $index }}"
|
name="persisted_folder_path_{{ $index }}"
|
||||||
ng-model="persistedFolder.ContainerPath"
|
ng-model="persistedFolder.ContainerPath"
|
||||||
ng-change="ctrl.onChangePersistedFolderPath($index)"
|
ng-change="ctrl.onChangePersistedFolderPath($index)"
|
||||||
|
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
||||||
placeholder="/data"
|
placeholder="/data"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
<div class="input-group col-sm-2 input-group-sm">
|
||||||
|
<span
|
||||||
|
class="btn-group btn-group-sm"
|
||||||
|
ng-class="{ striked: persistedFolder.NeedsDeletion }"
|
||||||
|
ng-if="!ctrl.isEditAndExistingPersistedFolder($index) && ctrl.application.ApplicationType !== ctrl.ApplicationTypes.STATEFULSET"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="btn btn-primary"
|
||||||
|
ng-model="persistedFolder.UseNewVolume"
|
||||||
|
uib-btn-radio="true"
|
||||||
|
ng-change="ctrl.useNewVolume($index)"
|
||||||
|
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
||||||
|
>New volume</label
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="btn btn-primary"
|
||||||
|
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"
|
||||||
|
>Use an existing volume</label
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group col-sm-2 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" ng-if="persistedFolder.UseNewVolume">
|
||||||
<span class="input-group-addon">requested size</span>
|
<span class="input-group-addon">requested size</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
@ -356,7 +382,12 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }" style="vertical-align: top;">
|
<div
|
||||||
|
class="input-group col-sm-3 input-group-sm"
|
||||||
|
ng-class="{ striked: persistedFolder.NeedsDeletion }"
|
||||||
|
style="vertical-align: top;"
|
||||||
|
ng-if="persistedFolder.UseNewVolume"
|
||||||
|
>
|
||||||
<span class="input-group-addon">storage</span>
|
<span class="input-group-addon">storage</span>
|
||||||
<select
|
<select
|
||||||
ng-if="ctrl.hasMultipleStorageClassesAvailable()"
|
ng-if="ctrl.hasMultipleStorageClassesAvailable()"
|
||||||
|
@ -368,22 +399,40 @@
|
||||||
<input ng-if="!ctrl.hasMultipleStorageClassesAvailable()" type="text" class="form-control" disabled ng-model="persistedFolder.StorageClass.Name" />
|
<input ng-if="!ctrl.hasMultipleStorageClassesAvailable()" type="text" class="form-control" disabled ng-model="persistedFolder.StorageClass.Name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-1 input-group-sm" style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet()">
|
<div class="input-group col-sm-5 input-group-sm" ng-if="!persistedFolder.UseNewVolume" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
||||||
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
|
<span class="input-group-addon">volume</span>
|
||||||
<i class="fa fa-times" aria-hidden="true"></i>
|
<select
|
||||||
</button>
|
class="form-control"
|
||||||
<button ng-if="persistedFolder.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePersistedFolder($index)">
|
name="existing_volumes_{{ $index }}"
|
||||||
Restore
|
ng-model="ctrl.formValues.PersistedFolders[$index].ExistingVolume"
|
||||||
</button>
|
ng-options="vol as vol.PersistentVolumeClaim.Name for vol in ctrl.availableVolumes"
|
||||||
|
ng-change="ctrl.onChangeExistingVolumeSelection()"
|
||||||
|
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option selected disabled hidden value="">Select a volume</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
|
<div style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet()" ng-if="!ctrl.state.useExistingVolume[$index]">
|
||||||
|
<button ng-if="!persistedFolder.NeedsDeletion" class="btn btn-sm btn-danger" type="button" ng-click="ctrl.removePersistedFolder($index)">
|
||||||
|
<i class="fa fa-times" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button ng-if="persistedFolder.NeedsDeletion" class="btn btn-sm btn-primary" type="button" ng-click="ctrl.restorePersistedFolder($index)">
|
||||||
|
Restore
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ng-repeat-end
|
|
||||||
ng-show="
|
ng-show="
|
||||||
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
|
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
|
||||||
ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined ||
|
ctrl.state.duplicatePersistedFolderPaths[$index] !== undefined ||
|
||||||
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid
|
kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid ||
|
||||||
|
kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid ||
|
||||||
|
ctrl.state.duplicateExistingVolumes[$index] !== undefined
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="input-group col-sm-3 input-group-sm">
|
<div class="input-group col-sm-3 input-group-sm">
|
||||||
|
@ -401,13 +450,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-3 input-group-sm">
|
<div class="input-group col-sm-2 input-group-sm"></div>
|
||||||
|
|
||||||
|
<div class="input-group col-sm-2 input-group-sm">
|
||||||
<div class="small text-warning" style="margin-top: 5px;" ng-show="kubernetesApplicationCreationForm['persisted_folder_size_' + $index].$invalid">
|
<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">
|
<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>
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Size is required.</p>
|
||||||
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This value must be greater than zero.</p>
|
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This value must be greater than zero.</p>
|
||||||
</ng-messages>
|
</ng-messages>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="small text-warning"
|
||||||
|
ng-show="kubernetesApplicationCreationForm['existing_volumes_' + $index].$invalid || ctrl.state.duplicateExistingVolumes[$index] !== undefined"
|
||||||
|
>
|
||||||
|
<ng-messages for="kubernetesApplicationCreationForm['existing_volumes_' + $index].$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Volume is required.</p>
|
||||||
|
</ng-messages>
|
||||||
|
<p ng-if="ctrl.state.duplicateExistingVolumes[$index] !== undefined"
|
||||||
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This volume is already used.</p
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group col-sm-3 input-group-sm"> </div>
|
<div class="input-group col-sm-3 input-group-sm"> </div>
|
||||||
|
@ -467,7 +529,12 @@
|
||||||
<p>All the instances of this application will use the same data</p>
|
<p>All the instances of this application will use the same data</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="!ctrl.state.isEdit || (ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED)">
|
<div
|
||||||
|
ng-if="
|
||||||
|
(!ctrl.state.isEdit && !ctrl.state.PersistedFoldersUseExistingVolumes) ||
|
||||||
|
(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED)
|
||||||
|
"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
id="data_access_isolated"
|
id="data_access_isolated"
|
||||||
|
@ -483,7 +550,10 @@
|
||||||
<p>Every instance of this application will use their own data</p>
|
<p>Every instance of this application will use their own data</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div style="color: #767676;" ng-if="ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED">
|
<div
|
||||||
|
style="color: #767676;"
|
||||||
|
ng-if="(ctrl.state.isEdit && ctrl.formValues.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED) || ctrl.state.PersistedFoldersUseExistingVolumes"
|
||||||
|
>
|
||||||
<input type="radio" id="data_access_isolated" disabled />
|
<input type="radio" id="data_access_isolated" disabled />
|
||||||
<label
|
<label
|
||||||
for="data_access_isolated"
|
for="data_access_isolated"
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
KubernetesApplicationDeploymentTypes,
|
KubernetesApplicationDeploymentTypes,
|
||||||
KubernetesApplicationPublishingTypes,
|
KubernetesApplicationPublishingTypes,
|
||||||
KubernetesApplicationQuotaDefaults,
|
KubernetesApplicationQuotaDefaults,
|
||||||
|
KubernetesApplicationTypes,
|
||||||
} from 'Kubernetes/models/application/models';
|
} from 'Kubernetes/models/application/models';
|
||||||
import {
|
import {
|
||||||
KubernetesApplicationConfigurationFormValue,
|
KubernetesApplicationConfigurationFormValue,
|
||||||
|
@ -23,6 +24,7 @@ import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
|
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
|
||||||
|
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||||
|
|
||||||
class KubernetesCreateApplicationController {
|
class KubernetesCreateApplicationController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -39,7 +41,8 @@ class KubernetesCreateApplicationController {
|
||||||
KubernetesConfigurationService,
|
KubernetesConfigurationService,
|
||||||
KubernetesNodeService,
|
KubernetesNodeService,
|
||||||
KubernetesPersistentVolumeClaimService,
|
KubernetesPersistentVolumeClaimService,
|
||||||
KubernetesNamespaceHelper
|
KubernetesNamespaceHelper,
|
||||||
|
KubernetesVolumeService
|
||||||
) {
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
|
@ -52,12 +55,14 @@ class KubernetesCreateApplicationController {
|
||||||
this.KubernetesStackService = KubernetesStackService;
|
this.KubernetesStackService = KubernetesStackService;
|
||||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||||
this.KubernetesNodeService = KubernetesNodeService;
|
this.KubernetesNodeService = KubernetesNodeService;
|
||||||
|
this.KubernetesVolumeService = KubernetesVolumeService;
|
||||||
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
||||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||||
|
|
||||||
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||||
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
||||||
|
this.ApplicationTypes = KubernetesApplicationTypes;
|
||||||
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
|
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
|
||||||
this.ServiceTypes = KubernetesServiceTypes;
|
this.ServiceTypes = KubernetesServiceTypes;
|
||||||
|
|
||||||
|
@ -73,7 +78,13 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid() {
|
isValid() {
|
||||||
return !this.state.alreadyExists && !this.state.hasDuplicateEnvironmentVariables && !this.state.hasDuplicatePersistedFolderPaths && !this.state.hasDuplicateConfigurationPaths;
|
return (
|
||||||
|
!this.state.alreadyExists &&
|
||||||
|
!this.state.hasDuplicateEnvironmentVariables &&
|
||||||
|
!this.state.hasDuplicatePersistedFolderPaths &&
|
||||||
|
!this.state.hasDuplicateConfigurationPaths &&
|
||||||
|
!this.state.hasDuplicateExistingVolumes
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeName() {
|
onChangeName() {
|
||||||
|
@ -197,7 +208,14 @@ class KubernetesCreateApplicationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangePersistedFolderPath() {
|
onChangePersistedFolderPath() {
|
||||||
this.state.duplicatePersistedFolderPaths = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.PersistedFolders, 'ContainerPath'));
|
this.state.duplicatePersistedFolderPaths = KubernetesFormValidationHelper.getDuplicates(
|
||||||
|
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
|
if (persistedFolder.NeedsDeletion) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return persistedFolder.ContainerPath;
|
||||||
|
})
|
||||||
|
);
|
||||||
this.state.hasDuplicatePersistedFolderPaths = Object.keys(this.state.duplicatePersistedFolderPaths).length > 0;
|
this.state.hasDuplicatePersistedFolderPaths = Object.keys(this.state.duplicatePersistedFolderPaths).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +230,47 @@ class KubernetesCreateApplicationController {
|
||||||
this.formValues.PersistedFolders.splice(index, 1);
|
this.formValues.PersistedFolders.splice(index, 1);
|
||||||
}
|
}
|
||||||
this.onChangePersistedFolderPath();
|
this.onChangePersistedFolderPath();
|
||||||
|
this.onChangeExistingVolumeSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeExistingVolume(index) {
|
||||||
|
if (this.formValues.PersistedFolders[index].UseNewVolume) {
|
||||||
|
this.formValues.PersistedFolders[index].ExistingVolume = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeExistingVolumeSelection() {
|
||||||
|
this.state.duplicateExistingVolumes = KubernetesFormValidationHelper.getDuplicates(
|
||||||
|
_.map(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
|
return persistedFolder.ExistingVolume ? persistedFolder.ExistingVolume.PersistentVolumeClaim.Name : '';
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.state.hasDuplicateExistingVolumes = Object.keys(this.state.duplicateExistingVolumes).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterAvailableVolumes() {
|
||||||
|
const filteredVolumes = _.filter(this.volumes, (volume) => {
|
||||||
|
const isSameNamespace = volume.ResourcePool.Namespace.Name === this.formValues.ResourcePool.Namespace.Name;
|
||||||
|
const isUnused = !KubernetesVolumeHelper.isUsed(volume);
|
||||||
|
const isRWX = _.find(volume.PersistentVolumeClaim.StorageClass.AccessModes, (am) => am === 'RWX');
|
||||||
|
return isSameNamespace && (isUnused || isRWX);
|
||||||
|
});
|
||||||
|
this.availableVolumes = filteredVolumes;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* !PERSISTENT FOLDERS UI MANAGEMENT
|
* !PERSISTENT FOLDERS UI MANAGEMENT
|
||||||
|
@ -500,7 +559,12 @@ class KubernetesCreateApplicationController {
|
||||||
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
||||||
this.updateSliders();
|
this.updateSliders();
|
||||||
this.refreshStacksConfigsApps(namespace);
|
this.refreshStacksConfigsApps(namespace);
|
||||||
|
this.filterAvailableVolumes();
|
||||||
this.formValues.Configurations = [];
|
this.formValues.Configurations = [];
|
||||||
|
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||||
|
persistedFolder.ExistingVolume = null;
|
||||||
|
persistedFolder.UseNewVolume = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* !DATA AUTO REFRESH
|
* !DATA AUTO REFRESH
|
||||||
|
@ -604,11 +668,14 @@ class KubernetesCreateApplicationController {
|
||||||
hasDuplicatePersistedFolderPaths: false,
|
hasDuplicatePersistedFolderPaths: false,
|
||||||
duplicateConfigurationPaths: {},
|
duplicateConfigurationPaths: {},
|
||||||
hasDuplicateConfigurationPaths: false,
|
hasDuplicateConfigurationPaths: false,
|
||||||
|
duplicateExistingVolumes: {},
|
||||||
|
hasDuplicateExistingVolumes: false,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
params: {
|
params: {
|
||||||
namespace: this.$transition$.params().namespace,
|
namespace: this.$transition$.params().namespace,
|
||||||
name: this.$transition$.params().name,
|
name: this.$transition$.params().name,
|
||||||
},
|
},
|
||||||
|
PersistedFoldersUseExistingVolumes: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isAdmin = this.Authentication.isAdmin();
|
this.isAdmin = this.Authentication.isAdmin();
|
||||||
|
@ -627,11 +694,20 @@ class KubernetesCreateApplicationController {
|
||||||
|
|
||||||
this.formValues = new KubernetesApplicationFormValues();
|
this.formValues = new KubernetesApplicationFormValues();
|
||||||
|
|
||||||
const [resourcePools, nodes] = await Promise.all([this.KubernetesResourcePoolService.get(), this.KubernetesNodeService.get()]);
|
const [resourcePools, nodes, volumes, applications] = await Promise.all([
|
||||||
|
this.KubernetesResourcePoolService.get(),
|
||||||
|
this.KubernetesNodeService.get(),
|
||||||
|
this.KubernetesVolumeService.get(),
|
||||||
|
this.KubernetesApplicationService.get(),
|
||||||
|
]);
|
||||||
|
|
||||||
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
||||||
|
|
||||||
this.formValues.ResourcePool = this.resourcePools[0];
|
this.formValues.ResourcePool = this.resourcePools[0];
|
||||||
|
this.volumes = volumes;
|
||||||
|
_.forEach(this.volumes, (volume) => {
|
||||||
|
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
||||||
|
});
|
||||||
|
this.filterAvailableVolumes();
|
||||||
|
|
||||||
_.forEach(nodes, (item) => {
|
_.forEach(nodes, (item) => {
|
||||||
this.state.nodes.memory += filesizeParser(item.Memory);
|
this.state.nodes.memory += filesizeParser(item.Memory);
|
||||||
|
@ -646,6 +722,16 @@ class KubernetesCreateApplicationController {
|
||||||
this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims);
|
this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims);
|
||||||
this.savedFormValues = angular.copy(this.formValues);
|
this.savedFormValues = angular.copy(this.formValues);
|
||||||
delete this.formValues.ApplicationType;
|
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 {
|
} else {
|
||||||
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler();
|
this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler();
|
||||||
this.formValues.AutoScaler.MinReplicas = this.formValues.ReplicaCount;
|
this.formValues.AutoScaler.MinReplicas = this.formValues.ReplicaCount;
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
<span class="col-sm-12 text-muted small">
|
<span class="col-sm-12 text-muted small">
|
||||||
<p>
|
<p>
|
||||||
Select which storage options will be available for use when deploying applications. Have a look at your storage driver documentation to figure out which access
|
Select which storage options will be available for use when deploying applications. Have a look at your storage driver documentation to figure out which access
|
||||||
policy to configure.
|
policy to configure and if the volume expansion capability is supported.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can find more information about access modes
|
You can find more information about access modes
|
||||||
|
@ -87,6 +87,7 @@
|
||||||
<tr class="text-muted">
|
<tr class="text-muted">
|
||||||
<td>Storage</td>
|
<td>Storage</td>
|
||||||
<td>Shared access policy</td>
|
<td>Shared access policy</td>
|
||||||
|
<td>Volume expansion</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-repeat="class in ctrl.StorageClasses">
|
<tr ng-repeat="class in ctrl.StorageClasses">
|
||||||
<td>
|
<td>
|
||||||
|
@ -109,6 +110,11 @@
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="margin: 5px;">
|
||||||
|
<label class="switch"><input type="checkbox" ng-model="class.AllowVolumeExpansion" /><i></i> </label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -42,6 +42,7 @@ class KubernetesConfigureController {
|
||||||
res.Name = item.Name;
|
res.Name = item.Name;
|
||||||
res.AccessModes = _.map(item.AccessModes, 'Name');
|
res.AccessModes = _.map(item.AccessModes, 'Name');
|
||||||
res.Provisioner = item.Provisioner;
|
res.Provisioner = item.Provisioner;
|
||||||
|
res.AllowVolumeExpansion = item.AllowVolumeExpansion;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -52,6 +53,16 @@ class KubernetesConfigureController {
|
||||||
this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer;
|
||||||
this.endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
this.endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics;
|
||||||
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint);
|
||||||
|
|
||||||
|
const storagePromises = _.map(classes, (storageClass) => {
|
||||||
|
const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name });
|
||||||
|
if (oldStorageClass) {
|
||||||
|
return this.KubernetesStorageService.patch(oldStorageClass, storageClass);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(storagePromises);
|
||||||
|
|
||||||
const endpoints = this.EndpointProvider.endpoints();
|
const endpoints = this.EndpointProvider.endpoints();
|
||||||
const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id);
|
const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id);
|
||||||
if (modifiedEndpoint) {
|
if (modifiedEndpoint) {
|
||||||
|
@ -102,6 +113,8 @@ class KubernetesConfigureController {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.oldStorageClasses = angular.copy(this.StorageClasses);
|
||||||
|
|
||||||
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer;
|
||||||
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -41,14 +41,66 @@
|
||||||
<td>Provisioner</td>
|
<td>Provisioner</td>
|
||||||
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner ? ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner : '-' }}</td>
|
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner ? ctrl.volume.PersistentVolumeClaim.StorageClass.Provisioner : '-' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>Size</td>
|
|
||||||
<td>{{ ctrl.volume.PersistentVolumeClaim.Storage }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Creation date</td>
|
<td>Creation date</td>
|
||||||
<td>{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}</td>
|
<td>{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Size</td>
|
||||||
|
<td ng-if="!ctrl.state.increaseSize">
|
||||||
|
{{ ctrl.volume.PersistentVolumeClaim.Storage }}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
ng-click="ctrl.state.increaseSize = true"
|
||||||
|
ng-if="ctrl.volume.PersistentVolumeClaim.StorageClass.AllowVolumeExpansion"
|
||||||
|
>Increase size</button
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td ng-if="ctrl.state.increaseSize">
|
||||||
|
<form name="kubernetesVolumeUpdateForm">
|
||||||
|
<div class="form-inline">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
name="size"
|
||||||
|
ng-model="ctrl.state.volumeSize"
|
||||||
|
placeholder="20"
|
||||||
|
ng-min="0"
|
||||||
|
ng-change="ctrl.onChangeSize()"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="input-group-addon" style="padding: 0;">
|
||||||
|
<select
|
||||||
|
ng-model="ctrl.state.volumeSizeUnit"
|
||||||
|
ng-change="ctrl.onChangeSize()"
|
||||||
|
ng-options="unit for unit in ctrl.state.availableSizeUnits"
|
||||||
|
style="width: 100%; height: 100%;"
|
||||||
|
></select>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-primary" ng-disabled="!ctrl.sizeIsValid()" ng-click="ctrl.updateVolume()">
|
||||||
|
Update size
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-default" ng-click="ctrl.state.increaseSize = false">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-inline">
|
||||||
|
<div class="small text-warning" style="margin-top: 5px;" ng-show="ctrl.state.volumeSizeError || 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"
|
||||||
|
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> The new size must be greater than the actual size.</p
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,10 +2,23 @@ import angular from 'angular';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||||
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
|
||||||
|
import filesizeParser from 'filesize-parser';
|
||||||
|
|
||||||
class KubernetesVolumeController {
|
class KubernetesVolumeController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($async, $state, Notifications, LocalStorage, KubernetesVolumeService, KubernetesEventService, KubernetesNamespaceHelper, KubernetesApplicationService) {
|
constructor(
|
||||||
|
$async,
|
||||||
|
$state,
|
||||||
|
Notifications,
|
||||||
|
LocalStorage,
|
||||||
|
KubernetesVolumeService,
|
||||||
|
KubernetesEventService,
|
||||||
|
KubernetesNamespaceHelper,
|
||||||
|
KubernetesApplicationService,
|
||||||
|
KubernetesPersistentVolumeClaimService,
|
||||||
|
ModalService,
|
||||||
|
KubernetesPodService
|
||||||
|
) {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
|
@ -15,10 +28,14 @@ class KubernetesVolumeController {
|
||||||
this.KubernetesEventService = KubernetesEventService;
|
this.KubernetesEventService = KubernetesEventService;
|
||||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||||
|
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
||||||
|
this.ModalService = ModalService;
|
||||||
|
this.KubernetesPodService = KubernetesPodService;
|
||||||
|
|
||||||
this.onInit = this.onInit.bind(this);
|
this.onInit = this.onInit.bind(this);
|
||||||
this.getVolume = this.getVolume.bind(this);
|
this.getVolume = this.getVolume.bind(this);
|
||||||
this.getVolumeAsync = this.getVolumeAsync.bind(this);
|
this.getVolumeAsync = this.getVolumeAsync.bind(this);
|
||||||
|
this.updateVolumeAsync = this.updateVolumeAsync.bind(this);
|
||||||
this.getEvents = this.getEvents.bind(this);
|
this.getEvents = this.getEvents.bind(this);
|
||||||
this.getEventsAsync = this.getEventsAsync.bind(this);
|
this.getEventsAsync = this.getEventsAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -44,9 +61,57 @@ class KubernetesVolumeController {
|
||||||
return KubernetesVolumeHelper.isUsed(this.volume);
|
return KubernetesVolumeHelper.isUsed(this.volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeSize() {
|
||||||
|
if (this.state.volumeSize) {
|
||||||
|
const size = filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit);
|
||||||
|
if (this.state.oldVolumeSize > size) {
|
||||||
|
this.state.volumeSizeError = true;
|
||||||
|
} else {
|
||||||
|
this.volume.PersistentVolumeClaim.Storage = size;
|
||||||
|
this.state.volumeSizeError = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeIsValid() {
|
||||||
|
return !this.state.volumeSizeError && this.state.oldVolumeSize !== this.volume.PersistentVolumeClaim.Storage;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VOLUME
|
* VOLUME
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
async updateVolumeAsync(redeploy) {
|
||||||
|
try {
|
||||||
|
this.volume.PersistentVolumeClaim.Storage = this.state.volumeSize + this.state.volumeSizeUnit.charAt(0) + 'i';
|
||||||
|
await this.KubernetesPersistentVolumeClaimService.patch(this.oldVolume.PersistentVolumeClaim, this.volume.PersistentVolumeClaim);
|
||||||
|
this.Notifications.success('Volume successfully updated');
|
||||||
|
|
||||||
|
if (redeploy) {
|
||||||
|
const promises = _.flatten(
|
||||||
|
_.map(this.volume.Applications, (app) => {
|
||||||
|
return _.map(app.Pods, (item) => this.KubernetesPodService.delete(item));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.all(promises);
|
||||||
|
this.Notifications.success('Applications successfully redeployed');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$state.reload();
|
||||||
|
} catch (err) {
|
||||||
|
this.Notifications.error('Failure', err, 'Unable to update volume.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVolume() {
|
||||||
|
this.ModalService.confirmRedeploy(
|
||||||
|
'One or multiple applications are currently using this volume.</br> For the change to be taken into account these applications will need to be redeployed. Do you want us to reschedule it now?',
|
||||||
|
(redeploy) => {
|
||||||
|
return this.$async(this.updateVolumeAsync, redeploy);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getVolumeAsync() {
|
async getVolumeAsync() {
|
||||||
try {
|
try {
|
||||||
const [volume, applications] = await Promise.all([
|
const [volume, applications] = await Promise.all([
|
||||||
|
@ -55,6 +120,10 @@ class KubernetesVolumeController {
|
||||||
]);
|
]);
|
||||||
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
||||||
this.volume = volume;
|
this.volume = volume;
|
||||||
|
this.oldVolume = angular.copy(volume);
|
||||||
|
this.state.volumeSize = parseInt(volume.PersistentVolumeClaim.Storage.slice(0, -2));
|
||||||
|
this.state.volumeSizeUnit = volume.PersistentVolumeClaim.Storage.slice(-2);
|
||||||
|
this.state.oldVolumeSize = filesizeParser(volume.PersistentVolumeClaim.Storage);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve volume');
|
this.Notifications.error('Failure', err, 'Unable to retrieve volume');
|
||||||
}
|
}
|
||||||
|
@ -101,6 +170,11 @@ class KubernetesVolumeController {
|
||||||
namespace: this.$transition$.params().namespace,
|
namespace: this.$transition$.params().namespace,
|
||||||
name: this.$transition$.params().name,
|
name: this.$transition$.params().name,
|
||||||
eventWarningCount: 0,
|
eventWarningCount: 0,
|
||||||
|
availableSizeUnits: ['MB', 'GB', 'TB'],
|
||||||
|
increaseSize: false,
|
||||||
|
volumeSize: 0,
|
||||||
|
volumeSizeUnit: 'GB',
|
||||||
|
volumeSizeError: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state.activeTab = this.LocalStorage.getActiveTab('volume');
|
this.state.activeTab = this.LocalStorage.getActiveTab('volume');
|
||||||
|
|
|
@ -150,6 +150,24 @@ angular.module('portainer.app').factory('ModalService', [
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.confirmRedeploy = function (message, callback) {
|
||||||
|
message = $sanitize(message);
|
||||||
|
service.confirm({
|
||||||
|
title: '',
|
||||||
|
message: message,
|
||||||
|
buttons: {
|
||||||
|
confirm: {
|
||||||
|
label: 'Redeploy the applications',
|
||||||
|
className: 'btn-primary',
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: "I'll do it later",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
callback: callback,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
service.confirmDeletionAsync = function confirmDeletionAsync(message) {
|
service.confirmDeletionAsync = function confirmDeletionAsync(message) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
service.confirmDeletion(message, (confirmed) => resolve(confirmed));
|
service.confirmDeletion(message, (confirmed) => resolve(confirmed));
|
||||||
|
|
Loading…
Reference in New Issue