1043 lines
41 KiB
JavaScript
1043 lines
41 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2016 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
(function() {
|
|
var angular = require('angular');
|
|
require('angular-route');
|
|
require('angular-dialog.js');
|
|
|
|
require('./date');
|
|
require('./listing');
|
|
require('./kube-client');
|
|
require('./utils');
|
|
|
|
require('../views/volumes-page.html');
|
|
require('../views/pv-page.html');
|
|
require('../views/pv-body.html');
|
|
require('../views/pvc-body.html');
|
|
require('../views/pv-claim.html');
|
|
require('../views/volume-body.html');
|
|
require('../views/pvc-delete.html');
|
|
require('../views/pv-delete.html');
|
|
require('../views/pv-modify.html');
|
|
|
|
var VOLUME_FACTORY_SUFFIX = "VolumeFields";
|
|
|
|
angular.module('kubernetes.volumes', [
|
|
'ngRoute',
|
|
'kubeClient',
|
|
'kubernetes.date',
|
|
'kubernetes.listing',
|
|
'ui.cockpit',
|
|
])
|
|
|
|
.config([
|
|
'$routeProvider',
|
|
function($routeProvider) {
|
|
$routeProvider
|
|
.when('/volumes', {
|
|
templateUrl: 'views/volumes-page.html',
|
|
controller: 'VolumeCtrl'
|
|
})
|
|
|
|
.when('/volumes/:target', {
|
|
controller: 'VolumeCtrl',
|
|
templateUrl: 'views/pv-page.html'
|
|
});
|
|
}
|
|
])
|
|
|
|
/*
|
|
* The controller for the volumes view.
|
|
*/
|
|
.controller('VolumeCtrl', [
|
|
'$scope',
|
|
'kubeLoader',
|
|
'kubeSelect',
|
|
'ListingState',
|
|
'filterService',
|
|
'$routeParams',
|
|
'$location',
|
|
'volumeActions',
|
|
'$timeout',
|
|
function($scope, loader, select, ListingState, filterService,
|
|
$routeParams, $location, actions, $timeout) {
|
|
var target = $routeParams["target"] || "";
|
|
$scope.target = target;
|
|
$scope.pvFailure = null;
|
|
|
|
loader.listen(function() {
|
|
$scope.pending = select().kind("PersistentVolumeClaim")
|
|
.statusPhase("Pending");
|
|
$scope.pvs = select().kind("PersistentVolume");
|
|
if (target)
|
|
$scope.item = select().kind("PersistentVolume")
|
|
.name(target)
|
|
.one();
|
|
}, $scope);
|
|
|
|
loader.watch("PersistentVolume", $scope)
|
|
.catch(function (ex) {
|
|
$scope.pvFailure = ex.message;
|
|
});
|
|
|
|
loader.watch("PersistentVolumeClaim", $scope);
|
|
loader.watch("Endpoints", $scope);
|
|
loader.watch("Pod", $scope);
|
|
|
|
$scope.listing = new ListingState($scope);
|
|
|
|
/* All the actions available on the $scope */
|
|
angular.extend($scope, actions);
|
|
|
|
/* Redirect after a delete */
|
|
$scope.deletePV = function(item) {
|
|
var promise = actions.deletePV(item);
|
|
|
|
/* If the promise is successful, redirect to another page */
|
|
promise.then(function() {
|
|
if ($scope.target)
|
|
$location.path('/volumes/');
|
|
});
|
|
|
|
return promise;
|
|
};
|
|
|
|
$scope.$on("activate", function(ev, id) {
|
|
ev.preventDefault();
|
|
$location.path('/volumes/' + id);
|
|
});
|
|
}
|
|
])
|
|
|
|
.directive('pvBody',
|
|
function() {
|
|
return {
|
|
restrict: 'A',
|
|
templateUrl: 'views/pv-body.html'
|
|
};
|
|
}
|
|
)
|
|
|
|
.directive('pvcBody',
|
|
function() {
|
|
return {
|
|
restrict: 'A',
|
|
templateUrl: 'views/pvc-body.html',
|
|
scope: {
|
|
item: '=item',
|
|
settings: '=settings'
|
|
},
|
|
};
|
|
}
|
|
)
|
|
|
|
.directive('pvClaim', [
|
|
'volumeData',
|
|
'kubeLoader',
|
|
function(volumeData, loader) {
|
|
return {
|
|
restrict: 'A',
|
|
templateUrl: 'views/pv-claim.html',
|
|
link: function(scope, element, attrs) {
|
|
loader.listen(function() {
|
|
scope.pvc = volumeData.claimForVolume(scope.item);
|
|
scope.pods = volumeData.podsForClaim(scope.pvc);
|
|
}, scope);
|
|
|
|
loader.watch("PersistentVolume", scope);
|
|
loader.watch("PersistentVolumeClaim", scope);
|
|
loader.watch("Pod", scope);
|
|
},
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('volumeBody',
|
|
function() {
|
|
return {
|
|
restrict: 'A',
|
|
templateUrl: 'views/volume-body.html',
|
|
scope: {
|
|
volume: '=volume'
|
|
},
|
|
};
|
|
}
|
|
)
|
|
|
|
.factory("volumeData", [
|
|
'kubeSelect',
|
|
"KubeTranslate",
|
|
"KubeMapNamedArray",
|
|
function (select, translate, mapNamedArray) {
|
|
const _ = translate.gettext;
|
|
|
|
var KNOWN_VOLUME_TYPES = {
|
|
"gcePersistentDisk" : _("GCE Persistent Disk"),
|
|
"awsElasticBlockStore" : _("AWS Elastic Block Store"),
|
|
"gitRepo" : _("Git Repository"),
|
|
"secret" : _("Secret"),
|
|
"emptyDir" : _("Empty Directory"),
|
|
"hostPath" : _("Host Path"),
|
|
"glusterfs" : _("Gluster FS"),
|
|
"nfs" : _("NFS Mount"),
|
|
"rbd" : _("Rados Block Device"),
|
|
"iscsi" : _("ISCSI"),
|
|
"cinder" : _("Cinder"),
|
|
"cephfs" : _("Ceph Filesystem Mount"),
|
|
"fc" : _("Fibre Channel"),
|
|
"flocker" : _("Flocker"),
|
|
"flexVolume" : _("Flex"),
|
|
"azureFile" : _("Azure")
|
|
};
|
|
|
|
var ACCESS_MODES = {
|
|
"ReadWriteOnce" : _("Read and write from a single node"),
|
|
"ReadOnlyMany" : _("Read only from multiple nodes"),
|
|
"ReadWriteMany" : _("Read and write from multiple nodes"),
|
|
};
|
|
|
|
var RECLAIM_POLICIES = {
|
|
"Retain" : _("Retain"),
|
|
"Delete" : _("Delete"),
|
|
"Recycle" : _("Recycle")
|
|
};
|
|
|
|
select.register({
|
|
name: "volumeName",
|
|
digest: function(arg) {
|
|
if (typeof arg === "string")
|
|
return arg;
|
|
|
|
var spec = arg.spec || {};
|
|
return spec.volumeName;
|
|
}
|
|
});
|
|
|
|
select.register({
|
|
name: "claim",
|
|
digests: function(arg) {
|
|
if (typeof arg === "string")
|
|
return ["claimName=" + arg];
|
|
|
|
var i;
|
|
var ret = [];
|
|
var spec = arg.spec || {};
|
|
var vols = spec.volumes || [];
|
|
for (i in vols) {
|
|
var claim = vols[i].persistentVolumeClaim || {};
|
|
if (claim.claimName)
|
|
ret.push("claimName=" + claim.claimName);
|
|
}
|
|
return ret;
|
|
}
|
|
});
|
|
|
|
function getVolumeType(volume) {
|
|
var keys = Object.keys(KNOWN_VOLUME_TYPES);
|
|
var i;
|
|
volume = volume || {};
|
|
for (i = 0; i < keys.length; i++) {
|
|
var key = keys[i];
|
|
if (volume[key])
|
|
return key;
|
|
}
|
|
}
|
|
|
|
function getVolumeLabel(volume) {
|
|
var type = getVolumeType(volume);
|
|
if (type)
|
|
return KNOWN_VOLUME_TYPES[type];
|
|
return _("Unknown");
|
|
}
|
|
|
|
function claimForVolume(volume) {
|
|
var uid = "";
|
|
if (volume && volume.spec.claimRef)
|
|
uid = volume.spec.claimRef.uid || "";
|
|
|
|
return select().kind("PersistentVolumeClaim")
|
|
.uid(uid)
|
|
.one();
|
|
}
|
|
|
|
function claimFromVolumeSource(source, namespace) {
|
|
source = source || {};
|
|
return select().kind("PersistentVolumeClaim")
|
|
.namespace(namespace || "")
|
|
.name(source.claimName || "")
|
|
.one();
|
|
}
|
|
|
|
function podsForClaim(claim) {
|
|
var meta;
|
|
claim = claim || {};
|
|
meta = claim.metadata || {};
|
|
return select().kind("Pod")
|
|
.namespace(meta.namespace || "")
|
|
.claim(meta.name || "");
|
|
}
|
|
|
|
function volumesForPod(item) {
|
|
var volumes;
|
|
var i, j, container, volumeMounts, name;
|
|
if (item && !item.volumes) {
|
|
if (item.spec)
|
|
volumes = mapNamedArray(item.spec.volumes);
|
|
else
|
|
volumes = { };
|
|
|
|
if (item.spec && item.spec.containers) {
|
|
for (i = 0; i < item.spec.containers.length; i++) {
|
|
container = item.spec.containers[i];
|
|
volumeMounts = container.volumeMounts || [];
|
|
for (j = 0; j < volumeMounts.length; j++) {
|
|
name = volumeMounts[j].name;
|
|
if (!volumes[name])
|
|
volumes[name] = {};
|
|
|
|
if (!volumes[name]['mounts'])
|
|
volumes[name]['mounts'] = {};
|
|
|
|
volumes[name]['mounts'][container.name] = volumeMounts[j];
|
|
}
|
|
}
|
|
}
|
|
|
|
item.volumes = volumes;
|
|
}
|
|
return item ? item.volumes : { };
|
|
}
|
|
|
|
return {
|
|
podsForClaim: podsForClaim,
|
|
volumesForPod: volumesForPod,
|
|
claimFromVolumeSource: claimFromVolumeSource,
|
|
claimForVolume: claimForVolume,
|
|
getVolumeType: getVolumeType,
|
|
getVolumeLabel: getVolumeLabel,
|
|
reclaimPolicies: RECLAIM_POLICIES,
|
|
accessModes: ACCESS_MODES,
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory('volumeActions', [
|
|
'$modal',
|
|
'$injector',
|
|
'volumeData',
|
|
function($modal, $injector, volumeData) {
|
|
function canEdit(item) {
|
|
var spec = item ? item.spec : {};
|
|
var type = volumeData.getVolumeType(spec);
|
|
if (type)
|
|
return $injector.has(type + VOLUME_FACTORY_SUFFIX);
|
|
return true;
|
|
}
|
|
|
|
function deleteItem(item, resolve, template, ev) {
|
|
if (ev)
|
|
ev.stopPropagation();
|
|
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'VolumeDeleteCtrl',
|
|
templateUrl: template,
|
|
resolve: resolve,
|
|
}).result;
|
|
}
|
|
|
|
function deletePVC(item, ev) {
|
|
var resolve = {
|
|
dialogData: function() {
|
|
return { item: item,
|
|
pvc: item,
|
|
pods: volumeData.podsForClaim(item) };
|
|
}
|
|
};
|
|
return deleteItem(item, resolve, 'views/pvc-delete.html', ev);
|
|
}
|
|
|
|
function deletePV(item, ev) {
|
|
var resolve = {
|
|
dialogData: function() {
|
|
return { item: item };
|
|
}
|
|
};
|
|
return deleteItem(item, resolve, 'views/pv-delete.html', ev);
|
|
}
|
|
|
|
function createPV(item) {
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'PVModifyCtrl',
|
|
templateUrl: 'views/pv-modify.html',
|
|
resolve: {
|
|
dialogData: function() {
|
|
return { pvc : item };
|
|
}
|
|
},
|
|
}).result;
|
|
}
|
|
|
|
function modifyPV(item) {
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'PVModifyCtrl',
|
|
templateUrl: 'views/pv-modify.html',
|
|
resolve: {
|
|
dialogData: function() {
|
|
return { item: item };
|
|
}
|
|
},
|
|
}).result;
|
|
}
|
|
|
|
return {
|
|
modifyPV: modifyPV,
|
|
createPV: createPV,
|
|
deletePV: deletePV,
|
|
deletePVC: deletePVC,
|
|
canEdit: canEdit,
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("defaultVolumeFields", [
|
|
"volumeData",
|
|
"KubeStringToBytes",
|
|
"KubeTranslate",
|
|
"KUBE_NAME_RE",
|
|
function (volumeData, stringToBytes, translate, NAME_RE) {
|
|
const _ = translate.gettext;
|
|
|
|
function build (item, type) {
|
|
if (!item)
|
|
item = {};
|
|
|
|
var spec = item.spec || {};
|
|
var requests, storage;
|
|
|
|
if (item.kind == "PersistentVolumeClaim") {
|
|
requests = spec.resources ? spec.resources.requests : {};
|
|
storage = requests ? requests.storage : "";
|
|
} else {
|
|
storage = spec.capacity ? spec.capacity.storage : "";
|
|
}
|
|
|
|
var fields = {
|
|
"capacity" : storage || "",
|
|
"policy" : spec.persistentVolumeReclaimPolicy || "Retain",
|
|
"accessModes": volumeData.accessModes,
|
|
"reclaimPolicies": volumeData.reclaimPolicies,
|
|
};
|
|
|
|
var i;
|
|
for (i in spec.accessModes || []) {
|
|
fields[spec.accessModes[i]] = true;
|
|
}
|
|
|
|
return fields;
|
|
}
|
|
|
|
function validate (item, fields) {
|
|
var ex, spec, name, capacity, policy, i, validModes;
|
|
var accessModes = [];
|
|
var ret = {
|
|
errors: [],
|
|
data: null,
|
|
};
|
|
|
|
validModes = Object.keys(fields.accessModes || {});
|
|
for (i = 0; i < validModes.length; i++) {
|
|
var mode = validModes[i];
|
|
if (fields[mode])
|
|
accessModes.push(mode);
|
|
}
|
|
|
|
if (accessModes.length < 1) {
|
|
ex = new Error(_("Please select a valid access mode"));
|
|
ex.target = "#last-access";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
name = fields.name ? fields.name.trim() : fields.name;
|
|
if (!item && (!name || !NAME_RE.test(name))) {
|
|
ex = new Error(_("Please provide a valid name"));
|
|
ex.target = "#modify-name";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
capacity = fields.capacity ? fields.capacity.trim() : fields.capacity;
|
|
if (!item && (!capacity || !stringToBytes(capacity))) {
|
|
ex = new Error(_("Please provide a valid storage capacity."));
|
|
ex.target = "#modify-capacity";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
policy = fields.policy ? fields.policy.trim() : fields.policy;
|
|
if (!fields.reclaimPolicies || !fields.reclaimPolicies[policy]) {
|
|
ex = new Error(_("Please select a valid policy option."));
|
|
ex.target = "#last-policy";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
if (ret.errors.length < 1) {
|
|
spec = {
|
|
"accessModes" : accessModes,
|
|
"capacity" : { "storage" : capacity },
|
|
"persistentVolumeReclaimPolicy" : policy,
|
|
};
|
|
|
|
if (item) {
|
|
ret.data = {
|
|
spec: spec
|
|
};
|
|
} else {
|
|
ret.data = {
|
|
kind: "PersistentVolume",
|
|
metadata: {
|
|
name: fields.name.trim()
|
|
},
|
|
spec: spec
|
|
};
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
return {
|
|
build: build,
|
|
validate: validate,
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("glusterfs" + VOLUME_FACTORY_SUFFIX, [
|
|
"volumeData",
|
|
"KubeTranslate",
|
|
"kubeSelect",
|
|
function (volumeData, translate, select) {
|
|
const _ = translate.gettext;
|
|
|
|
function build(item) {
|
|
if (!item)
|
|
item = {};
|
|
|
|
var spec = item.spec || {};
|
|
var glusterfs = spec.glusterfs || {};
|
|
|
|
return {
|
|
endpoint: glusterfs.endpoints,
|
|
endpointOptions: select().kind("Endpoints"),
|
|
glusterfsPath: glusterfs.path,
|
|
readOnly: glusterfs.readOnly,
|
|
reclaimPolicies: {
|
|
"Retain" : volumeData.reclaimPolicies["Retain"]
|
|
},
|
|
};
|
|
}
|
|
|
|
function validate (item, fields) {
|
|
var ex, endpoint, path;
|
|
var ret = {
|
|
errors: [],
|
|
data: null,
|
|
};
|
|
|
|
endpoint = fields.endpoint ? fields.endpoint.trim() : fields.endpoint;
|
|
if (!select().kind("Endpoints")
|
|
.name(endpoint)
|
|
.one()) {
|
|
ex = new Error(_("Please select a valid endpoint"));
|
|
ex.target = "#modify-endpoint";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
// The API calls glusterfs volume name, path.
|
|
path = fields.glusterfsPath ? fields.glusterfsPath.trim() : fields.glusterfsPath;
|
|
if (!path) {
|
|
ex = new Error(_("Please provide a GlusterFS volume name"));
|
|
ex.target = "#modify-glusterfs-path";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
if (ret.errors.length < 1) {
|
|
ret.data = {
|
|
endpoints: endpoint,
|
|
path: path,
|
|
readOnly: !!fields.readOnly
|
|
};
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
return {
|
|
build: build,
|
|
validate: validate,
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("nfs" + VOLUME_FACTORY_SUFFIX, [
|
|
"volumeData",
|
|
"KubeTranslate",
|
|
function (volumeData, translate) {
|
|
const _ = translate.gettext;
|
|
|
|
function build(item) {
|
|
if (!item)
|
|
item = {};
|
|
|
|
var spec = item.spec || {};
|
|
var nfs = spec.nfs || {};
|
|
return {
|
|
server: nfs.server,
|
|
path: nfs.path,
|
|
readOnly: nfs.readOnly,
|
|
reclaimPolicies: {
|
|
"Recycle" : volumeData.reclaimPolicies["Recycle"],
|
|
"Retain" : volumeData.reclaimPolicies["Retain"],
|
|
},
|
|
};
|
|
}
|
|
|
|
function validate (item, fields) {
|
|
var regex = /^[a-z0-9.:-]+$/i;
|
|
|
|
var ex, server, path;
|
|
var ret = {
|
|
errors: [],
|
|
data: null,
|
|
};
|
|
|
|
server = fields.server ? fields.server.trim() : fields.server;
|
|
if (!server || !regex.test(server)) {
|
|
ex = new Error(_("Please provide a valid NFS server"));
|
|
ex.target = "#nfs-modify-server";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
path = fields.path ? fields.path.trim() : fields.path;
|
|
if (!path || path.search("/") !== 0) {
|
|
ex = new Error(_("Please provide a valid path"));
|
|
ex.target = "#modify-path";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
if (ret.errors.length < 1) {
|
|
ret.data = {
|
|
server: server,
|
|
path: path,
|
|
readOnly: !!fields.readOnly
|
|
};
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
return {
|
|
build: build,
|
|
validate: validate,
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("hostPath" + VOLUME_FACTORY_SUFFIX, [
|
|
"volumeData",
|
|
"KubeTranslate",
|
|
function (volumeData, translate) {
|
|
const _ = translate.gettext;
|
|
|
|
function build(item) {
|
|
if (!item)
|
|
item = {};
|
|
|
|
var spec = item.spec || {};
|
|
var hp = spec.hostPath || {};
|
|
return {
|
|
path: hp.path,
|
|
readOnly: hp.readOnly,
|
|
reclaimPolicies: {
|
|
"Recycle" : volumeData.reclaimPolicies["Recycle"],
|
|
"Retain" : volumeData.reclaimPolicies["Retain"],
|
|
},
|
|
};
|
|
}
|
|
|
|
function validate (item, fields) {
|
|
var ex, path;
|
|
var ret = {
|
|
errors: [],
|
|
data: null,
|
|
};
|
|
|
|
path = fields.path ? fields.path.trim() : fields.path;
|
|
if (!path || path.search("/") !== 0) {
|
|
ex = new Error(_("Please provide a valid path"));
|
|
ex.target = "#modify-path";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
if (ret.errors.length < 1) {
|
|
ret.data = {
|
|
path: path,
|
|
};
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
return {
|
|
build: build,
|
|
validate: validate,
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller("VolumeDeleteCtrl", [
|
|
"$scope",
|
|
"$modalInstance",
|
|
"dialogData",
|
|
"volumeData",
|
|
"kubeMethods",
|
|
"kubeLoader",
|
|
function($scope, $instance, dialogData, volumeData, methods, loader) {
|
|
angular.extend($scope, dialogData);
|
|
|
|
loader.listen(function() {
|
|
if ($scope.pvc)
|
|
$scope.pods = volumeData.podsForClaim($scope.pvc);
|
|
}, $scope);
|
|
|
|
$scope.performDelete = function performDelete() {
|
|
return methods.delete($scope.item);
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("iscsi" + VOLUME_FACTORY_SUFFIX, [
|
|
"volumeData",
|
|
"KubeTranslate",
|
|
function (volumeData, translate) {
|
|
const _ = translate.gettext;
|
|
|
|
function build(item) {
|
|
if (!item)
|
|
item = {};
|
|
|
|
var spec = item.spec || {};
|
|
var source = spec.iscsi || {};
|
|
return {
|
|
target: source.targetPortal,
|
|
fstype: source.fsType || "ext4",
|
|
iqn: source.iqn,
|
|
lun: source.lun || 0,
|
|
iface: source.iscsiInterface,
|
|
readOnly: source.readOnly,
|
|
ReadWriteOnce: true,
|
|
accessModes: {
|
|
"ReadWriteOnce": volumeData.accessModes["ReadWriteOnce"],
|
|
},
|
|
reclaimPolicies: {
|
|
"Retain": volumeData.reclaimPolicies["Retain"],
|
|
},
|
|
};
|
|
}
|
|
|
|
function validate (item, fields) {
|
|
var ex, lun, target, iqn, fs, iface;
|
|
var regex = /^[a-z0-9.:-]+$/i;
|
|
|
|
var ret = {
|
|
errors: [],
|
|
data: null,
|
|
};
|
|
|
|
target = fields.target ? fields.target.trim() : fields.target;
|
|
if (!target || !regex.test(target)) {
|
|
ex = new Error(_("Please provide a valid target"));
|
|
ex.target = "#modify-iscsi-target";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
iqn = fields.iqn ? fields.iqn.trim() : fields.iqn;
|
|
if (!iqn || !regex.test(iqn)) {
|
|
ex = new Error(_("Please provide a valid qualified name"));
|
|
ex.target = "#modify-iscsi-iqn";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
lun = parseInt(fields.lun ? fields.lun.trim() : fields.lun, 10);
|
|
if (isNaN(lun)) {
|
|
ex = new Error(_("Please provide a valid logical unit number"));
|
|
ex.target = "#modify-iscsi-lun";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
fs = fields.fstype ? fields.fstype.trim() : "ext4";
|
|
if (!regex.test(fs)) {
|
|
ex = new Error(_("Please provide a valid filesystem type"));
|
|
ex.target = "#modify-fstype";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
iface = fields.iface ? fields.iface.trim() : undefined;
|
|
if (iface && !regex.test(iface)) {
|
|
ex = new Error(_("Please provide a valid interface"));
|
|
ex.target = "#modify-iscsi-iface";
|
|
ret.errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
if (ret.errors.length < 1) {
|
|
ret.data = {
|
|
iqn: iqn,
|
|
lun: lun,
|
|
fsType: fs,
|
|
targetPortal: target,
|
|
readOnly: !!fields.readOnly
|
|
};
|
|
if (iface)
|
|
ret.data["iscsiInterface"] = iface;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
return {
|
|
build: build,
|
|
validate: validate,
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller("PVModifyCtrl", [
|
|
"$q",
|
|
"$scope",
|
|
"$injector",
|
|
"$modalInstance",
|
|
"dialogData",
|
|
"volumeData",
|
|
"defaultVolumeFields",
|
|
"kubeMethods",
|
|
"KubeTranslate",
|
|
function($q, $scope, $injector, $instance, dialogData, volumeData,
|
|
defaultVolumeFields, methods, translate) {
|
|
const _ = translate.gettext;
|
|
var volumeFields, valName;
|
|
|
|
angular.extend($scope, dialogData);
|
|
|
|
$scope.types = [
|
|
{
|
|
name: _("NFS"),
|
|
type: "nfs",
|
|
},
|
|
{
|
|
name: _("Host Path"),
|
|
type: "hostPath",
|
|
},
|
|
{
|
|
name: _("ISCSI"),
|
|
type: "iscsi",
|
|
},
|
|
{
|
|
name: _("GlusterFS"),
|
|
type: "glusterfs",
|
|
},
|
|
];
|
|
|
|
function selectType(type) {
|
|
$scope.current_type = type;
|
|
valName = $scope.current_type + VOLUME_FACTORY_SUFFIX;
|
|
$scope.fields = defaultVolumeFields.build($scope.item || $scope.pvc);
|
|
if ($injector.has(valName)) {
|
|
volumeFields = $injector.get(valName, "PVModifyCtrl");
|
|
angular.extend($scope.fields, volumeFields.build($scope.item || $scope.pvc));
|
|
} else {
|
|
$scope.$applyAsync(function () {
|
|
$scope.$dismiss();
|
|
});
|
|
}
|
|
}
|
|
|
|
if ($scope.item) {
|
|
$scope.current_type = volumeData.getVolumeType($scope.item.spec);
|
|
selectType(volumeData.getVolumeType($scope.item.spec));
|
|
} else {
|
|
$scope.selected = $scope.types[0];
|
|
selectType($scope.selected.type);
|
|
}
|
|
|
|
function validate() {
|
|
var defer = $q.defer();
|
|
var resp, main_resp;
|
|
var errors = [];
|
|
|
|
if (!$scope.item) {
|
|
valName = $scope.current_type + VOLUME_FACTORY_SUFFIX;
|
|
if ($injector.has(valName))
|
|
volumeFields = $injector.get(valName, "PVModifyCtrl");
|
|
else
|
|
errors.push(new Error(_("Sorry, I don't know how to modify this volume")));
|
|
}
|
|
|
|
if (volumeFields) {
|
|
resp = volumeFields.validate($scope.item, $scope.fields);
|
|
errors = resp.errors;
|
|
}
|
|
|
|
main_resp = defaultVolumeFields.validate($scope.item, $scope.fields);
|
|
errors = errors.concat(main_resp.errors);
|
|
|
|
if (errors.length > 0) {
|
|
defer.reject(errors);
|
|
} else {
|
|
main_resp.data.spec[$scope.current_type] = resp ? resp.data : null;
|
|
defer.resolve(main_resp.data);
|
|
}
|
|
|
|
return defer.promise;
|
|
}
|
|
|
|
function updatePVC() {
|
|
/*
|
|
* HACK: https://github.com/kubernetes/kubernetes/issues/21498
|
|
*
|
|
* Until the API gets a way to manually
|
|
* trigger the process that matches a volume with a claim
|
|
* we change something else on the claim to try to kick things
|
|
* off sooner. Otherwise in the worse case
|
|
* the user will need to wait for around 10 mins.
|
|
*/
|
|
if ($scope.pvc) {
|
|
return methods.patch($scope.pvc, {
|
|
"metadata" : {
|
|
"annotations" : {
|
|
"claimTrigger": "on"
|
|
}
|
|
}
|
|
})
|
|
.then(function () {
|
|
methods.patch($scope.pvc, {
|
|
"metadata" : {
|
|
"annotations" : {
|
|
"claimTrigger": null
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
$scope.select = function(type) {
|
|
$scope.selected = type;
|
|
selectType(type.type);
|
|
};
|
|
|
|
$scope.setField = function(name, value) {
|
|
$scope.fields[name] = value;
|
|
};
|
|
|
|
$scope.hasField = function(name) {
|
|
return $scope.fields.hasOwnProperty(name);
|
|
};
|
|
|
|
$scope.performModify = function performModify() {
|
|
return validate().then(function(data) {
|
|
var res;
|
|
if (!$scope.item)
|
|
res = methods.create(data, null);
|
|
else
|
|
res = methods.patch($scope.item, data);
|
|
return res.then(updatePVC);
|
|
});
|
|
};
|
|
}
|
|
])
|
|
|
|
.filter("formatReadOnly", [
|
|
"KubeTranslate",
|
|
function(translate) {
|
|
return function(readOnly) {
|
|
if (readOnly)
|
|
return translate.gettext("Yes");
|
|
else
|
|
return translate.gettext("No");
|
|
};
|
|
}
|
|
])
|
|
|
|
.filter("formatPartitionNumber", [
|
|
function() {
|
|
return function(partition) {
|
|
if (!partition)
|
|
return 0;
|
|
else
|
|
return partition;
|
|
};
|
|
}
|
|
])
|
|
|
|
.filter("formatVolumeType", [
|
|
'volumeData',
|
|
function(volumeData) {
|
|
return function(volume) {
|
|
return volumeData.getVolumeLabel(volume || {});
|
|
};
|
|
}
|
|
])
|
|
|
|
.filter("reclaimLabel", [
|
|
'volumeData',
|
|
function(volumeData) {
|
|
return function(policy) {
|
|
var label = volumeData.reclaimPolicies[policy || ""];
|
|
return label || policy;
|
|
};
|
|
}
|
|
])
|
|
|
|
.filter("accessModeLabel", [
|
|
'volumeData',
|
|
function(volumeData) {
|
|
return function(mode) {
|
|
var label = volumeData.accessModes[mode || ""];
|
|
return label || mode;
|
|
};
|
|
}
|
|
]);
|
|
}());
|