mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 12:25:22 +02:00
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>
This commit is contained in:
parent
b9c2bf487b
commit
61f97469ab
16 changed files with 436 additions and 40 deletions
|
@ -319,8 +319,8 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat-start="persistedFolder in ctrl.formValues.PersistedFolders" style="margin-top: 2px;">
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;" ng-repeat="persistedFolder in ctrl.formValues.PersistedFolders">
|
||||
<div style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-3 input-group-sm" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
||||
<span class="input-group-addon">path in container</span>
|
||||
<input
|
||||
|
@ -329,12 +329,38 @@
|
|||
name="persisted_folder_path_{{ $index }}"
|
||||
ng-model="persistedFolder.ContainerPath"
|
||||
ng-change="ctrl.onChangePersistedFolderPath($index)"
|
||||
ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)"
|
||||
placeholder="/data"
|
||||
required
|
||||
/>
|
||||
</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>
|
||||
<input
|
||||
type="number"
|
||||
|
@ -356,7 +382,12 @@
|
|||
</span>
|
||||
</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>
|
||||
<select
|
||||
ng-if="ctrl.hasMultipleStorageClassesAvailable()"
|
||||
|
@ -368,22 +399,40 @@
|
|||
<input ng-if="!ctrl.hasMultipleStorageClassesAvailable()" type="text" class="form-control" disabled ng-model="persistedFolder.StorageClass.Name" />
|
||||
</div>
|
||||
|
||||
<div class="input-group col-sm-1 input-group-sm" style="vertical-align: top;" ng-if="!ctrl.isEditAndStatefulSet()">
|
||||
<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 class="input-group col-sm-5 input-group-sm" ng-if="!persistedFolder.UseNewVolume" ng-class="{ striked: persistedFolder.NeedsDeletion }">
|
||||
<span class="input-group-addon">volume</span>
|
||||
<select
|
||||
class="form-control"
|
||||
name="existing_volumes_{{ $index }}"
|
||||
ng-model="ctrl.formValues.PersistedFolders[$index].ExistingVolume"
|
||||
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
|
||||
ng-repeat-end
|
||||
ng-show="
|
||||
kubernetesApplicationCreationForm['persisted_folder_path_' + $index].$invalid ||
|
||||
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">
|
||||
|
@ -401,13 +450,26 @@
|
|||
</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">
|
||||
<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="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This value must be greater than zero.</p>
|
||||
</ng-messages>
|
||||
</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 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>
|
||||
</label>
|
||||
</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
|
||||
type="radio"
|
||||
id="data_access_isolated"
|
||||
|
@ -483,7 +550,10 @@
|
|||
<p>Every instance of this application will use their own data</p>
|
||||
</label>
|
||||
</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 />
|
||||
<label
|
||||
for="data_access_isolated"
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
KubernetesApplicationDeploymentTypes,
|
||||
KubernetesApplicationPublishingTypes,
|
||||
KubernetesApplicationQuotaDefaults,
|
||||
KubernetesApplicationTypes,
|
||||
} from 'Kubernetes/models/application/models';
|
||||
import {
|
||||
KubernetesApplicationConfigurationFormValue,
|
||||
|
@ -23,6 +24,7 @@ import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
|||
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
|
||||
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
|
||||
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
|
||||
|
||||
class KubernetesCreateApplicationController {
|
||||
/* @ngInject */
|
||||
|
@ -39,7 +41,8 @@ class KubernetesCreateApplicationController {
|
|||
KubernetesConfigurationService,
|
||||
KubernetesNodeService,
|
||||
KubernetesPersistentVolumeClaimService,
|
||||
KubernetesNamespaceHelper
|
||||
KubernetesNamespaceHelper,
|
||||
KubernetesVolumeService
|
||||
) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
|
@ -52,12 +55,14 @@ class KubernetesCreateApplicationController {
|
|||
this.KubernetesStackService = KubernetesStackService;
|
||||
this.KubernetesConfigurationService = KubernetesConfigurationService;
|
||||
this.KubernetesNodeService = KubernetesNodeService;
|
||||
this.KubernetesVolumeService = KubernetesVolumeService;
|
||||
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
||||
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
|
||||
|
||||
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
|
||||
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
|
||||
this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
|
||||
this.ApplicationTypes = KubernetesApplicationTypes;
|
||||
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
|
||||
this.ServiceTypes = KubernetesServiceTypes;
|
||||
|
||||
|
@ -73,7 +78,13 @@ class KubernetesCreateApplicationController {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
@ -197,7 +208,14 @@ class KubernetesCreateApplicationController {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -212,6 +230,47 @@ class KubernetesCreateApplicationController {
|
|||
this.formValues.PersistedFolders.splice(index, 1);
|
||||
}
|
||||
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
|
||||
|
@ -500,7 +559,12 @@ class KubernetesCreateApplicationController {
|
|||
const namespace = this.formValues.ResourcePool.Namespace.Name;
|
||||
this.updateSliders();
|
||||
this.refreshStacksConfigsApps(namespace);
|
||||
this.filterAvailableVolumes();
|
||||
this.formValues.Configurations = [];
|
||||
this.formValues.PersistedFolders = _.forEach(this.formValues.PersistedFolders, (persistedFolder) => {
|
||||
persistedFolder.ExistingVolume = null;
|
||||
persistedFolder.UseNewVolume = true;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* !DATA AUTO REFRESH
|
||||
|
@ -604,11 +668,14 @@ class KubernetesCreateApplicationController {
|
|||
hasDuplicatePersistedFolderPaths: false,
|
||||
duplicateConfigurationPaths: {},
|
||||
hasDuplicateConfigurationPaths: false,
|
||||
duplicateExistingVolumes: {},
|
||||
hasDuplicateExistingVolumes: false,
|
||||
isEdit: false,
|
||||
params: {
|
||||
namespace: this.$transition$.params().namespace,
|
||||
name: this.$transition$.params().name,
|
||||
},
|
||||
PersistedFoldersUseExistingVolumes: false,
|
||||
};
|
||||
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
|
@ -627,11 +694,20 @@ class KubernetesCreateApplicationController {
|
|||
|
||||
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.formValues.ResourcePool = this.resourcePools[0];
|
||||
this.volumes = volumes;
|
||||
_.forEach(this.volumes, (volume) => {
|
||||
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
|
||||
});
|
||||
this.filterAvailableVolumes();
|
||||
|
||||
_.forEach(nodes, (item) => {
|
||||
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.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();
|
||||
this.formValues.AutoScaler.MinReplicas = this.formValues.ReplicaCount;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue