515 lines
22 KiB
JavaScript
515 lines
22 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2015 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('./details');
|
|
require('./app');
|
|
require('./graphs');
|
|
require('./nodes');
|
|
require('./volumes');
|
|
|
|
require('../views/dashboard-page.html');
|
|
require('../views/deploy.html');
|
|
require('../views/file-button.html');
|
|
|
|
angular.module('kubernetes.dashboard', [
|
|
'ngRoute',
|
|
'kubernetes.details',
|
|
'kubernetes.app',
|
|
'kubernetes.graph',
|
|
'kubernetes.nodes'
|
|
])
|
|
|
|
.config(['$routeProvider', function($routeProvider) {
|
|
$routeProvider.when('/', {
|
|
templateUrl: 'views/dashboard-page.html',
|
|
controller: 'DashboardCtrl',
|
|
reloadOnSearch: false,
|
|
});
|
|
}])
|
|
|
|
.controller('DashboardCtrl', [
|
|
'$scope',
|
|
'kubeLoader',
|
|
'kubeSelect',
|
|
'dashboardData',
|
|
'dashboardActions',
|
|
'itemActions',
|
|
'nodeActions',
|
|
'nodeData',
|
|
'$location',
|
|
function($scope, loader, select, data, actions, itemActions,
|
|
nodeActions, nodeData, $location) {
|
|
loader.listen(function() {
|
|
$scope.services = select().kind("Service");
|
|
$scope.nodes = select().kind("Node");
|
|
$scope.pods = select().kind("Pod");
|
|
$scope.volumes = select().kind("PersistentVolume");
|
|
$scope.pvcs = select().kind("PersistentVolumeClaim");
|
|
|
|
$scope.status = {
|
|
pods: {
|
|
Pending: $scope.pods.statusPhase("Pending"),
|
|
Failed: $scope.pods.statusPhase("Failed"),
|
|
Unknown: $scope.pods.statusPhase("Unknown"),
|
|
},
|
|
nodes: {
|
|
Pending: $scope.nodes.statusPhase("Pending"),
|
|
Terminated: $scope.nodes.statusPhase("Terminated"),
|
|
NotReady: $scope.nodes.conditionNotTrue("Ready"),
|
|
OutOfDisk: $scope.nodes.conditionTrue("OutOfDisk"),
|
|
},
|
|
volumes: {
|
|
Pending: $scope.volumes.statusPhase("Pending"),
|
|
PendingClaims: $scope.pvcs.statusPhase("Pending"),
|
|
Available: $scope.volumes.statusPhase("Available"),
|
|
Released: $scope.volumes.statusPhase("Released"),
|
|
Failed: $scope.volumes.statusPhase("Failed"),
|
|
},
|
|
};
|
|
}, $scope);
|
|
|
|
loader.watch("Node", $scope);
|
|
loader.watch("Service", $scope);
|
|
loader.watch("ReplicationController", $scope);
|
|
loader.watch("Pod", $scope);
|
|
loader.watch("PersistentVolume", $scope);
|
|
loader.watch("PersistentVolumeClaim", $scope);
|
|
|
|
$scope.editServices = false;
|
|
$scope.toggleServiceChange = function toggleServiceChange() {
|
|
$scope.editServices = !$scope.editServices;
|
|
};
|
|
|
|
$scope.jumpService = function jumpService(ev, service) {
|
|
if ($scope.editServices)
|
|
return;
|
|
|
|
var meta = service.metadata || {};
|
|
var spec = service.spec || {};
|
|
if (spec.selector && !angular.equals({}, spec.selector) && meta.namespace)
|
|
$location.path("/pods/" + encodeURIComponent(meta.namespace)).search(spec.selector);
|
|
};
|
|
|
|
$scope.navigateNode = function(node) {
|
|
var meta = node.metadata || {};
|
|
if (meta.name)
|
|
$location.path("/nodes/" + encodeURIComponent(meta.name));
|
|
};
|
|
|
|
/* All the actions available on the $scope */
|
|
angular.extend($scope, actions);
|
|
angular.extend($scope, data);
|
|
angular.extend($scope, nodeData);
|
|
$scope.modifyService = itemActions.modifyService;
|
|
$scope.addNode = nodeActions.addNode;
|
|
|
|
/* Highlighting */
|
|
|
|
$scope.highlighted = null;
|
|
$scope.$on("highlight", function(ev, uid) {
|
|
$scope.highlighted = uid;
|
|
});
|
|
$scope.highlight = function highlight(uid) {
|
|
$scope.$broadcast("highlight", uid);
|
|
};
|
|
|
|
$scope.servicesState = function services_state() {
|
|
if ($scope.failure)
|
|
return 'failed';
|
|
var service;
|
|
for (service in $scope.services)
|
|
break;
|
|
return service ? 'ready' : 'empty';
|
|
};
|
|
}])
|
|
|
|
.directive('kubernetesAddress', function() {
|
|
return {
|
|
restrict: 'E',
|
|
link: function($scope, element, attributes) {
|
|
$scope.$watchGroup(["item.spec.clusterIP", "item.spec.ports"], function(values) {
|
|
var address = values[0];
|
|
var ports = values[1];
|
|
var href = null;
|
|
var text = null;
|
|
|
|
/* No ports */
|
|
if (!ports || !ports.length) {
|
|
text = address;
|
|
|
|
/* One single HTTP or HTTPS port */
|
|
} else if (ports.length == 1) {
|
|
text = address + ":" + ports[0].port;
|
|
if (ports[0].protocol === "TCP") {
|
|
if (ports[0].port === 80)
|
|
href = "http://" + encodeURIComponent(address);
|
|
else if (ports[0].port === 443)
|
|
href = "https://" + encodeURIComponent(address);
|
|
} else {
|
|
text += "/" + ports[0].protocol;
|
|
}
|
|
} else {
|
|
text = " " + address + " " + ports.map(function(p) {
|
|
if (p.protocol === "TCP")
|
|
return p.port;
|
|
else
|
|
return p.port + "/" + p.protocol;
|
|
}).join(" ");
|
|
}
|
|
|
|
var el;
|
|
element.empty();
|
|
if (href) {
|
|
el = angular.element("<a>")
|
|
.attr("href", href)
|
|
.attr("target", "_blank")
|
|
.on("click", function(ev) { ev.stopPropagation() });
|
|
element.append(el);
|
|
} else {
|
|
el = element;
|
|
}
|
|
el.text(text);
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
.factory('dashboardActions', [
|
|
'$modal',
|
|
function($modal) {
|
|
function deploy() {
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'DeployCtrl',
|
|
templateUrl: 'views/deploy.html',
|
|
resolve: {},
|
|
}).result;
|
|
}
|
|
|
|
return {
|
|
deploy: deploy,
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory('dashboardData', [
|
|
'kubeSelect',
|
|
function(select) {
|
|
function conditionDigest(arg, match) {
|
|
if (typeof arg == "string")
|
|
return [ arg ];
|
|
var conditions = (arg.status || { }).conditions || [ ];
|
|
var result = [ ];
|
|
conditions.forEach(function(condition) {
|
|
if ((match && condition.status == "True") ||
|
|
(!match && condition.status != "True")) {
|
|
result.push(condition.type);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
select.register({
|
|
name: "conditionTrue",
|
|
digests: function(arg) {
|
|
return conditionDigest(arg, true);
|
|
}
|
|
});
|
|
|
|
select.register({
|
|
name: "conditionNotTrue",
|
|
digests: function(arg) {
|
|
return conditionDigest(arg, false);
|
|
}
|
|
});
|
|
|
|
return {
|
|
nodeContainers: function nodeContainers(node) {
|
|
var count = 0;
|
|
var meta = node.metadata || { };
|
|
angular.forEach(select().kind("Pod")
|
|
.host(meta.name), function(pod) {
|
|
var spec = pod.spec || { };
|
|
var n = 1;
|
|
if (spec.containers)
|
|
n = spec.containers.length;
|
|
count += n;
|
|
});
|
|
return count;
|
|
},
|
|
|
|
serviceStatus: function serviceStatus(service) {
|
|
var spec = service.spec || { };
|
|
var meta = service.metadata || { };
|
|
var state = "";
|
|
|
|
var pods = select().kind("Pod")
|
|
.namespace(meta.namespace || "")
|
|
.label(spec.selector || {});
|
|
angular.forEach(pods, function(pod) {
|
|
if (!pod.status || !pod.status.phase)
|
|
return;
|
|
switch (pod.status.phase) {
|
|
case "Pending":
|
|
if (!state)
|
|
state = "wait";
|
|
break;
|
|
case "Running":
|
|
break;
|
|
case "Succeeded":
|
|
break;
|
|
case "Unknown":
|
|
break;
|
|
case "Failed":
|
|
/* falls through */
|
|
default: /* assume failed */
|
|
state = "fail";
|
|
break;
|
|
}
|
|
});
|
|
|
|
return state;
|
|
},
|
|
|
|
serviceContainers: function serviceContainers(service) {
|
|
var spec = service.spec || { };
|
|
var meta = service.metadata || {};
|
|
|
|
/* Calculate number of containers */
|
|
var x = 0;
|
|
var y = 0;
|
|
|
|
/*
|
|
* Calculate "x of y" containers, where x is the current
|
|
* number and y is the expected number. If x==y then only
|
|
* show x. The calculation is based on the statuses of the
|
|
* containers within the pod. Pod states: Pending,
|
|
* Running, Succeeded, Failed, and Unknown.
|
|
*/
|
|
var pods = select().kind("Pod")
|
|
.namespace(meta.namespace || "")
|
|
.label(spec.selector || {});
|
|
angular.forEach(pods, function(pod) {
|
|
if (!pod.status || !pod.status.phase)
|
|
return;
|
|
var spec = pod.spec || { };
|
|
var n = 1;
|
|
if (spec.containers)
|
|
n = spec.containers.length;
|
|
switch (pod.status.phase) {
|
|
case "Pending":
|
|
y += n;
|
|
break;
|
|
case "Running":
|
|
x += n;
|
|
y += n;
|
|
break;
|
|
case "Succeeded": // don't increment either counter
|
|
break;
|
|
case "Unknown":
|
|
y += n;
|
|
break;
|
|
case "Failed":
|
|
/* falls through */
|
|
default: /* assume failed */
|
|
y += n;
|
|
break;
|
|
}
|
|
});
|
|
|
|
if (x != y)
|
|
return x + " of " + y;
|
|
else
|
|
return "" + x;
|
|
}
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller("DeployCtrl", [
|
|
"$q",
|
|
"$scope",
|
|
"$timeout",
|
|
"$modalInstance",
|
|
"filterService",
|
|
"kubeMethods",
|
|
"KubeFormat",
|
|
"KubeTranslate",
|
|
function($q, $scope, $timeout, $instance, filter, methods, KubeFormat, translate) {
|
|
const _ = translate.gettext;
|
|
|
|
var file;
|
|
var fields = {
|
|
"filename": "",
|
|
"namespace" : filter.namespace(),
|
|
};
|
|
|
|
function validate_manifest() {
|
|
var defer = $q.defer();
|
|
var ex;
|
|
var fails = [];
|
|
|
|
var ns = fields.namespace;
|
|
if (!ns)
|
|
ex = new Error(_("Namespace cannot be empty."));
|
|
else if (!/^[a-z0-9]+$/i.test(ns))
|
|
ex = new Error(_("Please provide a valid namespace."));
|
|
if (ex) {
|
|
ex.target = "#deploy-app-namespace-group";
|
|
fails.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
if (!file)
|
|
ex = new Error(_("No metadata file was selected. Please select a Kubernetes metadata file."));
|
|
else if (file.type && !file.type.match("json.*"))
|
|
ex = new Error(_("The selected file is not a valid Kubernetes application manifest."));
|
|
if (ex) {
|
|
ex.target = "#deploy-app-manifest-file-button";
|
|
fails.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
var reader;
|
|
|
|
if (fails.length) {
|
|
defer.reject(fails);
|
|
} else {
|
|
reader = new window.FileReader();
|
|
reader.onerror = function(event) {
|
|
ex = new Error(KubeFormat.format(_("Unable to read the Kubernetes application manifest. Code $0."),
|
|
event.target.error.code));
|
|
ex.target = "#deploy-app-manifest-file-button";
|
|
defer.reject(ex);
|
|
};
|
|
reader.onload = function() {
|
|
try {
|
|
defer.resolve({
|
|
objects : JSON.parse(reader.result),
|
|
namespace : ns
|
|
});
|
|
} catch (err) {
|
|
ex = new Error(KubeFormat.format(_("Unable to decode Kubernetes application manifest.")));
|
|
ex.target = "#deploy-app-manifest-file-button";
|
|
defer.reject(ex);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
return defer.promise;
|
|
}
|
|
|
|
function deploy_manifest() {
|
|
var defer = $q.defer();
|
|
|
|
validate_manifest().then(function(data) {
|
|
methods.create(data.objects, data.namespace)
|
|
.then(function() {
|
|
if ($scope.namespace && data.namespace != $scope.namespace)
|
|
filter.namespace(data.namespace);
|
|
defer.resolve();
|
|
})
|
|
.catch(function(response) {
|
|
var ex;
|
|
var resp = response.data;
|
|
|
|
/* Interpret this code as a conflict, so suggest user creates a new namespace */
|
|
if (response && response.code === 409) {
|
|
ex = new Error(KubeFormat.format(_("Please create another namespace for $0 \"$1\""),
|
|
response.details.kind, response.details.id));
|
|
ex.target = "#deploy-app-namespace-field";
|
|
} else {
|
|
ex = resp || response;
|
|
}
|
|
|
|
defer.reject(ex);
|
|
});
|
|
}, function(ex) {
|
|
defer.reject(ex);
|
|
});
|
|
|
|
return defer.promise;
|
|
}
|
|
|
|
$scope.types = [
|
|
{
|
|
name: _("Manifest"),
|
|
type: "manifest",
|
|
}
|
|
];
|
|
|
|
$scope.selected = $scope.types[0];
|
|
$scope.fields = fields;
|
|
$scope.namespaces = filter.namespaces();
|
|
$scope.namespace = filter.namespace();
|
|
|
|
$scope.$on("file", function(ev, newFile) {
|
|
$scope.$applyAsync(function() {
|
|
file = newFile;
|
|
fields.filename = file ? file.name : "";
|
|
});
|
|
});
|
|
|
|
$scope.performDeploy = function performDeploy() {
|
|
if ($scope.selected.type == 'manifest') {
|
|
return deploy_manifest();
|
|
}
|
|
};
|
|
|
|
$scope.select = function(type) {
|
|
$scope.selected = type;
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('fileButton', function() {
|
|
return {
|
|
templateUrl: 'views/file-button.html',
|
|
restrict: 'A',
|
|
link: function($scope, element, attributes) {
|
|
var button, file_input;
|
|
if (element[0].children.length == 2) {
|
|
button = element[0].children[1];
|
|
file_input = element[0].children[0];
|
|
button.onclick = function () {
|
|
file_input.click();
|
|
};
|
|
file_input.onchange = function () {
|
|
var files = file_input.files || [];
|
|
$scope.$emit('file', files[0]);
|
|
};
|
|
}
|
|
|
|
element.on('$destroy', function() {
|
|
if (file_input)
|
|
file_input.onchange = null;
|
|
|
|
if (button)
|
|
button.onclick = null;
|
|
});
|
|
}
|
|
};
|
|
});
|
|
}());
|