Compare commits

...

5 Commits

20 changed files with 588 additions and 12 deletions

View File

@@ -247,6 +247,16 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
},
};
const volumeCreation = {
name: 'kubernetes.volumes.new',
url: '/new',
views: {
'content@': {
component: 'kubernetesCreateVolumeView',
},
},
};
$stateRegistryProvider.register(kubernetes);
$stateRegistryProvider.register(applications);
$stateRegistryProvider.register(applicationCreation);
@@ -270,5 +280,6 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([
$stateRegistryProvider.register(resourcePoolAccess);
$stateRegistryProvider.register(volumes);
$stateRegistryProvider.register(volume);
$stateRegistryProvider.register(volumeCreation);
},
]);

View File

@@ -54,6 +54,7 @@
<button type="button" class="btn btn-sm btn-danger" ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="kubernetes.volumes.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add NFS volume </button>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
@@ -134,6 +135,7 @@
}}</a>
<span class="label label-info image-tag label-margins" ng-if="$ctrl.isSystemNamespace(item)">system</span>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isExternalVolume(item)">external</span>
<span class="label label-primary image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && $ctrl.isNFSVolume(item)">NFS</span>
<span class="label label-warning image-tag label-margins" ng-if="!$ctrl.isSystemNamespace(item) && !$ctrl.isUsed(item)">unused</span>
</td>
<td>
@@ -149,7 +151,7 @@
<span ng-if="!item.Applications.length">-</span>
</td>
<td>
{{ item.PersistentVolumeClaim.StorageClass.Name }}
{{ item.PersistentVolumeClaim.StorageClass.Name || '-' }}
</td>
<td>
{{ item.PersistentVolumeClaim.Storage }}

View File

@@ -4,12 +4,13 @@ import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
// TODO: review - refactor to use `extends GenericDatatableController`
class KubernetesVolumesDatatableController {
/* @ngInject */
constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) {
constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService, EndpointProvider) {
this.$async = $async;
this.$controller = $controller;
this.Authentication = Authentication;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.DatatableService = DatatableService;
this.EndpointProvider = EndpointProvider;
this.onInit = this.onInit.bind(this);
this.allowSelection = this.allowSelection.bind(this);
@@ -40,6 +41,10 @@ class KubernetesVolumesDatatableController {
return KubernetesVolumeHelper.isExternalVolume(item);
}
isNFSVolume(item) {
return KubernetesVolumeHelper.isNFSVolume(item);
}
allowSelection(item) {
return !this.disableRemove(item);
}
@@ -49,6 +54,8 @@ class KubernetesVolumesDatatableController {
this.prepareTableFromDataset();
this.isAdmin = this.Authentication.isAdmin();
this.settings.showSystem = false;
const endpoint = this.EndpointProvider.currentEndpoint();
this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses;
this.state.orderBy = this.orderBy;
var storedOrder = this.DatatableService.getDataTableOrder(this.tableKey);

View File

@@ -17,6 +17,7 @@ class KubernetesPersistentVolumeClaimConverter {
res.Yaml = yaml ? yaml.data : '';
res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : '';
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] : '';
res.PersistentVolumeName = data.spec.volumeName;
return res;
}
@@ -57,6 +58,20 @@ class KubernetesPersistentVolumeClaimConverter {
return res;
}
/**
* Generate KubernetesPersistentVolumeClaim from KubernetesVolumeFormValues
* @param {KubernetesVolumeFormValues} formValues
*/
static volumeFormValuesToVolumeClaim(formValues) {
const pvc = new KubernetesPersistentVolumeClaim();
pvc.Name = formValues.Name;
pvc.Namespace = formValues.ResourcePool.Namespace.Name;
pvc.Storage = '' + formValues.Size + formValues.SizeUnit.charAt(0) + 'i';
pvc.MountPath = formValues.NFSMountPoint;
pvc.ApplicationOwner = formValues.ApplicationOwner;
return pvc;
}
static createPayload(pvc) {
const res = new KubernetesPersistentVolumClaimCreatePayload();
res.metadata.name = pvc.Name;
@@ -66,6 +81,9 @@ class KubernetesPersistentVolumeClaimConverter {
res.metadata.labels.app = pvc.ApplicationName;
res.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pvc.ApplicationOwner;
res.metadata.labels[KubernetesPortainerApplicationNameLabel] = pvc.ApplicationName;
if (pvc.PersistentVolumeName) {
res.spec.volumeName = pvc.PersistentVolumeName;
}
return res;
}

View File

@@ -1,9 +1,10 @@
import { KubernetesVolume } from 'Kubernetes/models/volume/models';
class KubernetesVolumeConverter {
static pvcToVolume(claim, pool) {
static apiToVolume(pvc, pv, pool) {
const res = new KubernetesVolume();
res.PersistentVolumeClaim = claim;
res.PersistentVolumeClaim = pvc;
res.PersistentVolume = pv;
res.ResourcePool = pool;
return res;
}

View File

@@ -24,5 +24,37 @@ class KubernetesCommonHelper {
_.set(obj, path, value);
}
}
/**
* Format a string to be compliant with RFC 1123 - DNS subdomain specs
* https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
* contain no more than 253 characters
* contain only lowercase alphanumeric characters, '-' or '.'
* start with an alphanumeric character
* end with an alphanumeric character
* @param {String} str String to format
*/
static formatToDnsSubdomainName(str) {
let res = _.replace(str, /[^a-z0-9.-]/g, '.');
res = _.replace(res, /(^[-.]*)|([-.]*$)/g, '');
res = _.truncate(res, { length: 253, omission: '' });
return res;
}
/**
* Format a string to be compliant with RFC 1123 - DNS subdomain specs
* https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
* contain at most 63 characters
* contain only lowercase alphanumeric characters or '-'
* start with an alphanumeric character
* end with an alphanumeric character
* @param {String} str String to format
*/
static formatToDnsLabelName(str) {
let res = _.replace(str, /[^a-z0-9-]/g, '-');
res = _.replace(str, /(^[-]*)|([-]*$)/g, ''); // ensure alph on string start and end
res = _.truncate(res, { length: 63, omission: '' });
return res;
}
}
export default KubernetesCommonHelper;

View File

@@ -1,6 +1,7 @@
import _ from 'lodash-es';
import uuidv4 from 'uuid/v4';
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
import KubernetesCommonHelper from './commonHelper';
class KubernetesVolumeHelper {
// TODO: review
@@ -32,6 +33,15 @@ class KubernetesVolumeHelper {
static isExternalVolume(volume) {
return !volume.PersistentVolumeClaim.ApplicationOwner;
}
static isNFSVolume(volume) {
return volume.PersistentVolume.NFSAddress;
}
static generateVolumeName(name) {
const n = `${name}-${uuidv4()}`;
return KubernetesCommonHelper.formatToDnsSubdomainName(n);
}
}
export default KubernetesVolumeHelper;

View File

@@ -0,0 +1,19 @@
/**
* FormValues for CreateVolume view
*/
export function KubernetesVolumeFormValues() {
return {
Id: '',
Name: '',
ResourcePool: {}, // KubernetesResourcePool
Size: '',
SizeUnit: '',
NFSAddress: '',
NFSMountPoint: '',
};
}
export const KubernetesVolumeFormValuesDefaults = {
Size: '10',
SizeUnit: 'MB',
};

View File

@@ -8,11 +8,12 @@ const _KubernetesPersistentVolumeClaim = Object.freeze({
PreviousName: '',
Namespace: '',
Storage: 0,
StorageClass: {}, // KubernetesStorageClass
StorageClass: undefined, // KubernetesStorageClass
CreationDate: '',
ApplicationOwner: '',
ApplicationName: '',
MountPath: '', // used for Application creation from ApplicationFormValues | not used from API conversion
PersistentVolumeName: '', // Name of KubernetesPersistentVolume
Yaml: '',
});
@@ -23,12 +24,27 @@ export class KubernetesPersistentVolumeClaim {
}
}
/**
* KubernetesPersistentVolume Model
*/
export function KubernetesPersistentVolume() {
return {
Id: '',
Name: '',
StorageClass: {}, // KubernetesStorageClass
Size: '',
NFSAddress: '',
NFSMountPoint: '',
};
}
/**
* KubernetesVolume Model (Composite)
*/
const _KubernetesVolume = Object.freeze({
ResourcePool: {}, // KubernetesResourcePool
PersistentVolumeClaim: {}, // KubernetesPersistentVolumeClaim
PersistentVolume: undefined, // KubernetesPersistentVolume
Applications: [], // KubernetesApplication
});

View File

@@ -0,0 +1,45 @@
import _ from 'lodash-es';
import { KubernetesPersistentVolume } from 'Kubernetes/models/volume/models';
import { KubernetesPersistentVolumeCreatePayload } from 'Kubernetes/persistent-volume/payloads';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
class KubernetesPersistentVolumeConverter {
/**
* Converts KubernetesVolumeFormValues to KubernetesPersistentVolume
* @param {KubernetesVolumeFormValues} fv
*/
static formValuesToPersistentVolume(fv) {
const pv = new KubernetesPersistentVolume();
pv.Name = KubernetesVolumeHelper.generateVolumeName(fv.Name);
pv.Size = fv.Size + fv.SizeUnit;
pv.NFSAddress = fv.NFSAddress;
pv.NFSMountPoint = fv.NFSMountPoint;
return pv;
}
static apiToPersistentVolume(data, storageClasses) {
const pv = new KubernetesPersistentVolume();
pv.Id = data.metadata.id;
pv.Name = data.metadata.name;
pv.Size = data.spec.capacity.storage.replace('i', 'B');
if (data.spec.storageClassName) {
pv.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName });
}
if (data.spec.nfs) {
pv.NFSAddress = data.spec.nfs.server;
pv.NFSMountPoint = data.spec.nfs.path;
}
return pv;
}
static createPayload(pv) {
const res = new KubernetesPersistentVolumeCreatePayload();
res.metadata.name = pv.Name;
res.spec.capacity.storage = pv.Size.replace('B', 'i');
res.spec.nfs.path = pv.NFSMountPoint;
res.spec.nfs.server = pv.NFSAddress;
return res;
}
}
export default KubernetesPersistentVolumeConverter;

View File

@@ -0,0 +1,20 @@
import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads';
/**
* KubernetesPersistentVolumeCreatePayload Model
*/
export function KubernetesPersistentVolumeCreatePayload() {
return {
metadata: new KubernetesCommonMetadataPayload(),
spec: {
accessModes: ['ReadWriteOnce'],
capacity: {
storage: '',
},
nfs: {
path: '',
server: '',
},
},
};
}

View File

@@ -0,0 +1,23 @@
angular.module('portainer.kubernetes').factory('KubernetesPersistentVolume', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function KubernetesPersistentVolume($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return function (namespace) {
const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/persistentvolumes/:id/:action';
return $resource(
url,
{
endpointId: EndpointProvider.endpointID,
namespace: namespace,
},
{
get: { method: 'GET' },
create: { method: 'POST' },
delete: { method: 'DELETE' },
}
);
};
},
]);

View File

@@ -0,0 +1,90 @@
import angular from 'angular';
import _ from 'lodash-es';
import PortainerError from 'Portainer/error';
import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import KubernetesPersistentVolumeConverter from 'Kubernetes/persistent-volume/converter';
class KubernetesPersistentVolumeService {
/* @ngInject */
constructor($async, KubernetesPersistentVolume, EndpointProvider) {
this.$async = $async;
this.KubernetesPersistentVolume = KubernetesPersistentVolume;
this.EndpointProvider = EndpointProvider;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
this.createAsync = this.createAsync.bind(this);
this.deleteAsync = this.deleteAsync.bind(this);
}
/**
* GET
*/
async getAsync(name) {
try {
const params = new KubernetesCommonParams();
params.id = name;
const data = await this.KubernetesPersistentVolume().get(params).$promise;
const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses;
return KubernetesPersistentVolumeConverter.apiToPersistentVolume(data, storageClasses);
} catch (err) {
throw new PortainerError('Unable to retrieve persistent volume', err);
}
}
async getAllAsync() {
try {
const data = await this.KubernetesPersistentVolume().get().$promise;
const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses;
const res = _.map(data.items, (item) => KubernetesPersistentVolumeConverter.apiToPersistentVolume(item, storageClasses));
return res;
} catch (err) {
throw new PortainerError('Unable to retrieve persistent volumes', err);
}
}
get(name) {
if (name) {
return this.$async(this.getAsync, name);
}
return this.$async(this.getAllAsync);
}
/**
* CREATE
*/
async createAsync(pv) {
try {
const payload = KubernetesPersistentVolumeConverter.createPayload(pv);
const data = await this.KubernetesPersistentVolume().create(payload).$promise;
return data;
} catch (err) {
throw new PortainerError('Unable to create persistent volume', err);
}
}
create(pv) {
return this.$async(this.createAsync, pv);
}
/**
* DELETE
*/
async deleteAsync(pv) {
try {
const params = new KubernetesCommonParams();
params.id = pv.Name;
await this.KubernetesPersistentVolume().delete(params).$promise;
} catch (err) {
throw new PortainerError('Unable to delete persistent volume', err);
}
}
delete(pv) {
return this.$async(this.deleteAsync, pv);
}
}
export default KubernetesPersistentVolumeService;
angular.module('portainer.kubernetes').service('KubernetesPersistentVolumeService', KubernetesPersistentVolumeService);

View File

@@ -2,14 +2,18 @@ import angular from 'angular';
import _ from 'lodash-es';
import KubernetesVolumeConverter from 'Kubernetes/converters/volume';
import KubernetesPersistentVolumeConverter from 'Kubernetes/persistent-volume/converter';
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper';
class KubernetesVolumeService {
/* @ngInject */
constructor($async, KubernetesResourcePoolService, KubernetesApplicationService, KubernetesPersistentVolumeClaimService) {
constructor($async, KubernetesResourcePoolService, KubernetesApplicationService, KubernetesPersistentVolumeClaimService, KubernetesPersistentVolumeService) {
this.$async = $async;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesApplicationService = KubernetesApplicationService;
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
this.KubernetesPersistentVolumeService = KubernetesPersistentVolumeService;
this.getAsync = this.getAsync.bind(this);
this.getAllAsync = this.getAllAsync.bind(this);
@@ -22,7 +26,11 @@ class KubernetesVolumeService {
async getAsync(namespace, name) {
try {
const [pvc, pool] = await Promise.all([this.KubernetesPersistentVolumeClaimService.get(namespace, name), this.KubernetesResourcePoolService.get(namespace)]);
return KubernetesVolumeConverter.pvcToVolume(pvc, pool);
let pv = undefined;
if (pvc.PersistentVolumeName) {
pv = await this.KubernetesPersistentVolumeService.get(pvc.PersistentVolumeName);
}
return KubernetesVolumeConverter.apiToVolume(pvc, pv, pool);
} catch (err) {
throw err;
}
@@ -35,7 +43,18 @@ class KubernetesVolumeService {
const res = await Promise.all(
_.map(pools, async (pool) => {
const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name);
return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool));
const pvs = [];
await Promise.all(
_.map(pvcs, async (pvc) => {
if (pvc.PersistentVolumeName) {
pvs.push(await this.KubernetesPersistentVolumeService.get(pvc.PersistentVolumeName));
}
})
);
return _.map(pvcs, (pvc) => {
const pv = pvc.PersistentVolumeName ? _.find(pvs, { Name: pvc.PersistentVolumeName }) : undefined;
return KubernetesVolumeConverter.apiToVolume(pvc, pv, pool);
});
})
);
return _.flatten(res);
@@ -51,12 +70,34 @@ class KubernetesVolumeService {
return this.$async(this.getAllAsync, namespace);
}
/**
* CREATE all KubernetesVolume composite elements (but the ResourcePool)
* @param {KubernetesVolumeFormValues} fv
*/
create(fv) {
return this.$async(async () => {
try {
fv.ApplicationOwner = KubernetesCommonHelper.ownerToLabel(fv.ApplicationOwner);
const pv = KubernetesPersistentVolumeConverter.formValuesToPersistentVolume(fv);
const data = await this.KubernetesPersistentVolumeService.create(pv);
const pvc = KubernetesPersistentVolumeClaimConverter.volumeFormValuesToVolumeClaim(fv);
pvc.PersistentVolumeName = data.metadata.name;
await this.KubernetesPersistentVolumeClaimService.create(pvc);
} catch (err) {
throw err;
}
});
}
/**
* DELETE
*/
async deleteAsync(volume) {
try {
await this.KubernetesPersistentVolumeClaimService.delete(volume.PersistentVolumeClaim);
if (volume.PersistentVolume) {
await this.KubernetesPersistentVolumeService.delete(volume.PersistentVolume);
}
} catch (err) {
throw err;
}

View File

@@ -0,0 +1,138 @@
<kubernetes-view-header ng-if="!ctrl.state.isEdit" title="Create volume" state="kubernetes.volumes.new" view-ready="ctrl.state.viewReady">
<a ui-sref="kubernetes.volumes">Volumes</a> &gt; Create a volume
</kubernetes-view-header>
<kubernetes-view-loading view-ready="ctrl.state.viewReady"></kubernetes-view-loading>
<div ng-if="ctrl.state.viewReady">
<div class="row">
<div class="col-xs-12">
<rd-widget>
<rd-widget-body>
<form class="form-horizontal" name="kubernetesVolumeCreationForm" autocomplete="off">
<!-- #region NAME FIELD -->
<div class="form-group">
<label for="volume_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
name="volume_name"
ng-model="ctrl.formValues.Name"
ng-change="ctrl.onNameChange()"
placeholder="my-volume"
ng-pattern="/^[a-z]([-a-z0-9]*[a-z0-9])?$/"
auto-focus
required
/>
</div>
</div>
<div class="form-group" ng-show="kubernetesVolumeCreationForm.volume_name.$invalid || ctrl.state.alreadyExists">
<div class="col-sm-12 small text-warning">
<div ng-messages="kubernetesVolumeCreationForm.volume_name.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of lower case alphanumeric characters or '-', start with an alphabetic
character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').</p
>
</div>
<p ng-if="ctrl.state.alreadyExists">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> A volume with the same name already exists in this resource pool.
</p>
</div>
</div>
<!-- #endregion -->
<!-- #region RESOURCE POOL -->
<div class="col-sm-12 form-section-title">
Resource pool
</div>
<div class="form-group">
<label for="resource-pool-selector" class="col-sm-1 control-label text-left">Resource pool</label>
<div class="col-sm-11">
<select
class="form-control"
id="resource-pool-selector"
ng-model="ctrl.formValues.ResourcePool"
ng-options="resourcePool.Namespace.Name for resourcePool in ctrl.resourcePools"
ng-change="ctrl.onResourcePoolSelectionChange()"
ng-disabled="ctrl.state.isEdit"
required
></select>
</div>
</div>
<!-- #endregion -->
<!-- #region NFS-SETTINGS -->
<div>
<div class="col-sm-12 form-section-title">
NFS Settings
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<i class="fa fa-info-circle blue-icon space-right" aria-hidden="true"></i>
Please ensure that your hosts are running either nfs-utils or nfs-common software before attempting to use NFS
</span>
</div>
<div class="form-group">
<label for="address" class="col-sm-1 control-label text-left">Address</label>
<div class="col-sm-11">
<input type="text" class="form-control" name="address" ng-model="ctrl.formValues.NFSAddress" placeholder="e.g. my.nfs-server.com OR xxx.xxx.xxx.xxx" required />
</div>
</div>
<div class="form-group" ng-show="kubernetesVolumeCreationForm.address.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="kubernetesVolumeCreationForm.address.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<div class="form-group">
<label for="mount_point" class="col-sm-1 control-label text-left">Mount point</label>
<div class="col-sm-11">
<input
type="text"
class="form-control"
name="mount_point"
ng-model="ctrl.formValues.NFSMountPoint"
placeholder="e.g. /export/share, :/export/share, /share or :/share"
required
/>
</div>
</div>
<div class="form-group" ng-show="kubernetesVolumeCreationForm.mount_point.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="kubernetesVolumeCreationForm.mount_point.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
</div>
<!-- #endregion -->
<!-- #region ACTIONS -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button
type="button"
class="btn btn-primary btn-sm"
ng-disabled="!kubernetesVolumeCreationForm.$valid"
button-spinner="ctrl.state.actionInProgress"
ng-click="ctrl.createVolume()"
>
<span ng-show="!ctrl.state.isEdit && !ctrl.state.actionInProgress">Create volume</span>
<span ng-show="!ctrl.state.isEdit && ctrl.state.actionInProgress">Creation in progress...</span>
</button>
</div>
</div>
<!-- #endregion -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
angular.module('portainer.kubernetes').component('kubernetesCreateVolumeView', {
templateUrl: './createVolume.html',
controller: 'KubernetesCreateVolumeController',
controllerAs: 'ctrl',
bindings: {
$transition$: '<',
},
});

View File

@@ -0,0 +1,83 @@
import angular from 'angular';
import * as _ from 'lodash-es';
import { KubernetesVolumeFormValues, KubernetesVolumeFormValuesDefaults } from 'Kubernetes/models/volume/formValues';
class KubernetesCreateVolumeController {
/* @ngInject */
constructor($async, $state, Notifications, Authentication, KubernetesNamespaceHelper, KubernetesResourcePoolService, KubernetesVolumeService, KubernetesPersistentVolumeService) {
this.$async = $async;
this.$state = $state;
this.Notifications = Notifications;
this.Authentication = Authentication;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
this.KubernetesVolumeService = KubernetesVolumeService;
this.KubernetesPersistentVolumeService = KubernetesPersistentVolumeService;
}
onNameChange() {
const existingVolume = _.find(this.volumes, ['PersistentVolumeClaim.Name', this.formValues.Name]);
this.state.alreadyExists = !!existingVolume;
}
refreshExistingVolumes() {
return this.$async(async () => {
try {
this.volumes = await this.KubernetesVolumeService.get(this.formValues.ResourcePool.Namespace.Name);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to refresh volumes for this Resource Pool');
}
});
}
onResourcePoolSelectionChange() {
return this.$async(async () => {
await this.refreshExistingVolumes();
this.onNameChange();
});
}
createVolume() {
return this.$async(async () => {
this.state.actionInProgress = true;
try {
await this.KubernetesVolumeService.create(this.formValues);
this.Notifications.success('Volume successfully created', this.formValues.Name);
this.$state.go('kubernetes.volumes');
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to create volume');
} finally {
this.state.actionInProgress = false;
}
});
}
$onInit() {
return this.$async(async () => {
try {
this.state = {
viewReady: false,
alreadyExists: false,
actionInProgress: false,
};
const resourcePools = await this.KubernetesResourcePoolService.get();
this.resourcePools = _.filter(resourcePools, (resourcePool) => !this.KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
this.formValues = new KubernetesVolumeFormValues();
this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username;
this.formValues.Size = KubernetesVolumeFormValuesDefaults.Size;
this.formValues.SizeUnit = KubernetesVolumeFormValuesDefaults.SizeUnit;
this.formValues.ResourcePool = this.resourcePools[0];
await this.onResourcePoolSelectionChange();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load view data');
} finally {
this.state.viewReady = true;
}
});
}
}
export default KubernetesCreateVolumeController;
angular.module('portainer.kubernetes').controller('KubernetesCreateVolumeController', KubernetesCreateVolumeController);

View File

@@ -23,6 +23,7 @@
<td>
{{ ctrl.volume.PersistentVolumeClaim.Name }}
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isExternalVolume()">external</span>
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isNFSVolume()">NFS</span>
<span class="label label-warning image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && !ctrl.isUsed()">unused</span>
</td>
</tr>
@@ -33,11 +34,11 @@
<span style="margin-left: 5px;" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace(item)">system</span>
</td>
</tr>
<tr>
<tr ng-if="ctrl.volume.PersistentVolumeClaim.StorageClass">
<td>Storage</td>
<td>{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}</td>
</tr>
<tr>
<tr ng-if="ctrl.volume.PersistentVolumeClaim.StorageClass">
<td>Shared Access Policy</td>
<td
>{{ ctrl.state.volumeSharedAccessPolicy }}
@@ -113,6 +114,14 @@
</form>
</td>
</tr>
<tr ng-if="ctrl.isNFSVolume()">
<td>NFS Address</td>
<td>{{ ctrl.volume.PersistentVolume.NFSAddress }}</td>
</tr>
<tr ng-if="ctrl.isNFSVolume()">
<td>NFS Mount point</td>
<td>{{ ctrl.volume.PersistentVolume.NFSMountPoint }}</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -54,6 +54,10 @@ class KubernetesVolumeController {
return KubernetesVolumeHelper.isExternalVolume(this.volume);
}
isNFSVolume() {
return KubernetesVolumeHelper.isNFSVolume(this.volume);
}
isSystemNamespace() {
return this.KubernetesNamespaceHelper.isSystemNamespace(this.volume.ResourcePool.Namespace.Name);
}

View File

@@ -91,14 +91,13 @@ class KubernetesVolumesController {
this.KubernetesApplicationService.get(),
this.KubernetesStorageService.get(this.state.endpointId),
]);
this.volumes = _.map(volumes, (volume) => {
volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications);
return volume;
});
this.storages = buildStorages(storages, volumes);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retreive resource pools');
this.Notifications.error('Failure', err, 'Unable to retrieve volumes');
}
}