cockpit/pkg/kubernetes/scripts/dashboard.js

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;
});
}
};
});
}());