Compare commits
5 Commits
fix/EE-634
...
feat/GH/44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23a2e598d | ||
|
|
d8e97ed84d | ||
|
|
99da75d5ee | ||
|
|
42eea33b45 | ||
|
|
97d08059a6 |
@@ -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);
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
19
app/kubernetes/models/volume/formValues.js
Normal file
19
app/kubernetes/models/volume/formValues.js
Normal 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',
|
||||
};
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
45
app/kubernetes/persistent-volume/converter.js
Normal file
45
app/kubernetes/persistent-volume/converter.js
Normal 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;
|
||||
20
app/kubernetes/persistent-volume/payloads.js
Normal file
20
app/kubernetes/persistent-volume/payloads.js
Normal 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: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
23
app/kubernetes/persistent-volume/rest.js
Normal file
23
app/kubernetes/persistent-volume/rest.js
Normal 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' },
|
||||
}
|
||||
);
|
||||
};
|
||||
},
|
||||
]);
|
||||
90
app/kubernetes/persistent-volume/service.js
Normal file
90
app/kubernetes/persistent-volume/service.js
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
138
app/kubernetes/views/volumes/create/createVolume.html
Normal file
138
app/kubernetes/views/volumes/create/createVolume.html
Normal 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> > 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>
|
||||
8
app/kubernetes/views/volumes/create/createVolume.js
Normal file
8
app/kubernetes/views/volumes/create/createVolume.js
Normal file
@@ -0,0 +1,8 @@
|
||||
angular.module('portainer.kubernetes').component('kubernetesCreateVolumeView', {
|
||||
templateUrl: './createVolume.html',
|
||||
controller: 'KubernetesCreateVolumeController',
|
||||
controllerAs: 'ctrl',
|
||||
bindings: {
|
||||
$transition$: '<',
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user