687 lines
31 KiB
JavaScript
687 lines
31 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-dialog.js');
|
|
require('./kube-client-cockpit');
|
|
require('./utils');
|
|
|
|
require('../views/auth-form.html');
|
|
require('../views/auth-rejected-cert.html');
|
|
require('../views/container-page.html');
|
|
require('../views/containers-page.html');
|
|
require('../views/containers-listing.html');
|
|
require('../views/container-page-inline.html');
|
|
require('../views/container-body.html');
|
|
require('../views/pod-body.html');
|
|
|
|
angular.module('kubernetes.connection', [
|
|
'ui.cockpit',
|
|
'kubeClient',
|
|
'kubeClient.cockpit',
|
|
'kubeUtils',
|
|
])
|
|
|
|
.factory("sessionCertificates", [
|
|
"cockpitKubectlConfig",
|
|
function (kubectl) {
|
|
var trustedCerts = {};
|
|
|
|
function trustCert(cluster, pem) {
|
|
var options = kubectl.generateKubeOptions(cluster);
|
|
var address = options.address || "localhost";
|
|
trustedCerts[address] = pem;
|
|
}
|
|
|
|
function getCert(address) {
|
|
address = address || "localhost";
|
|
return trustedCerts[address];
|
|
}
|
|
|
|
return {
|
|
getCert: getCert,
|
|
trustCert: trustCert
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("connectionActions", [
|
|
"$q",
|
|
"cockpitKubeDiscover",
|
|
"cockpitKubectlConfig",
|
|
"cockpitRunCommand",
|
|
"cockpitConnectionInfo",
|
|
"CockpitKubeRequest",
|
|
"KubeMapNamedArray",
|
|
"CockpitTranslate",
|
|
"sessionCertificates",
|
|
function($q, cockpitKubeDiscover, kubectl, runCommand,
|
|
cockpitConnectionInfo, CockpitKubeRequest,
|
|
mapNamedArray, translate, sessionCertificates) {
|
|
var DEFAULT_ADDRESS = "http://localhost:8080";
|
|
const _ = translate.gettext;
|
|
|
|
function kubectlError(ex) {
|
|
// Because angular warns about all throws
|
|
console.warn("Unexpected Kubectl Error", ex);
|
|
return $q.reject(new Error(_("Error writing kubectl config")));
|
|
}
|
|
|
|
function kubectlData() {
|
|
return kubectl.read().then(function(data) {
|
|
var config;
|
|
try {
|
|
config = JSON.parse(data);
|
|
} catch (ex) {
|
|
console.warn("received invalid kubectl config", ex);
|
|
config = {};
|
|
}
|
|
return config;
|
|
});
|
|
}
|
|
|
|
function loadConfigData(config, useConfig) {
|
|
var user, cluster, context;
|
|
var contexts = mapNamedArray(config.contexts);
|
|
var users = mapNamedArray(config.users);
|
|
var clusters = mapNamedArray(config.clusters);
|
|
|
|
if (useConfig)
|
|
context = contexts[useConfig["current-context"]];
|
|
else
|
|
context = contexts[config["current-context"]];
|
|
|
|
if (context && context.context) {
|
|
if (context.context.user)
|
|
user = users[context.context.user];
|
|
if (context.context.cluster)
|
|
cluster = clusters[context.context.cluster];
|
|
}
|
|
|
|
return {
|
|
users: users,
|
|
clusters: clusters,
|
|
contexts: contexts,
|
|
currentUser: user,
|
|
currentCluster: cluster,
|
|
currentContext: context,
|
|
config: config
|
|
};
|
|
}
|
|
|
|
function load(error) {
|
|
var defer = $q.defer();
|
|
var promise;
|
|
|
|
var result = {
|
|
haveKubectl: false,
|
|
defaultAddress: DEFAULT_ADDRESS,
|
|
error: error,
|
|
users: undefined,
|
|
clusters: undefined,
|
|
contexts: undefined,
|
|
currentUser: undefined,
|
|
currentCluster: undefined,
|
|
currentContext: undefined,
|
|
config: undefined
|
|
};
|
|
|
|
if (!error) {
|
|
promise = cockpitKubeDiscover().then(function(options) {
|
|
var address = "localhost";
|
|
if (options.address)
|
|
address = options.address;
|
|
|
|
if (options.tls)
|
|
result.defaultAddress = "https://" + address + ":" + options.port;
|
|
else
|
|
result.defaultAddress = "http://" + address + ":" + options.port;
|
|
|
|
return kubectlData();
|
|
}, function(ex) {
|
|
result.error = ex;
|
|
return kubectlData();
|
|
});
|
|
} else {
|
|
promise = kubectlData();
|
|
}
|
|
|
|
promise.then(function(config) {
|
|
var useConfig;
|
|
|
|
result.haveKubectl = true;
|
|
|
|
if (cockpitConnectionInfo.type && cockpitConnectionInfo.type != "open")
|
|
useConfig = cockpitConnectionInfo.kubeConfig;
|
|
|
|
angular.extend(result, loadConfigData(config, useConfig));
|
|
defer.resolve(result);
|
|
}, function(ex) {
|
|
if (cockpitConnectionInfo.kubeConfig)
|
|
angular.extend(result, loadConfigData(cockpitConnectionInfo.kubeConfig));
|
|
defer.resolve(result);
|
|
});
|
|
return defer.promise;
|
|
}
|
|
|
|
function prepareData(config, cluster, user) {
|
|
var i;
|
|
var contexts, context;
|
|
var default_cluster = {
|
|
cluster: { server: DEFAULT_ADDRESS }
|
|
};
|
|
|
|
config = config || {};
|
|
cluster = cluster || default_cluster;
|
|
|
|
function ensureValid(name, options) {
|
|
var added;
|
|
var chars = "abcdefghijklmnopqrstuvwxyz";
|
|
name = name.toLowerCase().replace(/[^a-z0-9:/-]/g, "-");
|
|
while (options[name]) {
|
|
var length = 0;
|
|
if (!added)
|
|
name = name + "/";
|
|
|
|
added = "";
|
|
|
|
while (length < 4) {
|
|
added += chars[Math.floor(Math.random() * chars.length)];
|
|
length++;
|
|
}
|
|
|
|
name = name + added;
|
|
}
|
|
return name;
|
|
}
|
|
|
|
function generateClusterName(cluster) {
|
|
var a = document.createElement("a");
|
|
a.href = cluster.cluster.server;
|
|
|
|
var name = a.hostname;
|
|
|
|
if (a.port)
|
|
name = name + ":" + a.port;
|
|
|
|
return ensureValid(name, mapNamedArray(config.clusters));
|
|
}
|
|
|
|
function generateUserName(user, clusterName) {
|
|
var name;
|
|
if (user.user && user.user.username)
|
|
name = user.user.username;
|
|
else
|
|
name = "user";
|
|
name = name + "/" + clusterName;
|
|
return ensureValid(name, mapNamedArray(config.users));
|
|
}
|
|
|
|
function generateContextName(userName, clusterName) {
|
|
var name = clusterName + "/" + userName;
|
|
return ensureValid(name, mapNamedArray(config.contexts));
|
|
}
|
|
|
|
if (!cluster.name)
|
|
cluster.name = generateClusterName(cluster);
|
|
|
|
if (user && !user.name)
|
|
user.name = generateUserName(user, cluster.name);
|
|
|
|
contexts = config.contexts || [];
|
|
for (i = 0; i < contexts.length; i++) {
|
|
var c = contexts[i];
|
|
if (c.context) {
|
|
var inner = c.context || {};
|
|
if (inner.cluster == cluster.name &&
|
|
((user && inner.user == user.name) || (!user && !inner.user))) {
|
|
context = { name: c.name };
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!context) {
|
|
context = {
|
|
name: generateContextName(user ? user.name : "noauth", cluster.name),
|
|
context: {
|
|
user: user ? user.name : undefined,
|
|
cluster: cluster.name,
|
|
}
|
|
};
|
|
}
|
|
|
|
return {
|
|
cluster: cluster,
|
|
user: user,
|
|
context: context
|
|
};
|
|
}
|
|
|
|
/* Openshift returns token information in the hash of a Location URL */
|
|
function parseBearerToken(url) {
|
|
var token = null;
|
|
var parser = document.createElement('a');
|
|
parser.href = url;
|
|
var hash = parser.hash;
|
|
if (hash[0] == "#")
|
|
hash = hash.substr(1);
|
|
hash.split("&").forEach(function(part) {
|
|
var item = part.split("=");
|
|
if (item.shift() == "access_token")
|
|
token = item.join("=");
|
|
});
|
|
return token;
|
|
}
|
|
|
|
/* Retrieve a bearer token using basic auth if possible */
|
|
function populateBearerToken(cluster, user) {
|
|
/* The user data without any token */
|
|
var data = angular.extend({ }, user ? user.user : null);
|
|
|
|
/* If no password is set, just skip this step */
|
|
if (!data.password)
|
|
return $q.when();
|
|
|
|
delete data.token;
|
|
|
|
/* Build an Openshift OAuth WWW-Authenticate request */
|
|
var config = kubectl.generateKubeOptions(cluster.cluster, data);
|
|
var trust = sessionCertificates.getCert(config.address);
|
|
if (config.tls && trust)
|
|
config.tls["authority"] = { data: trust };
|
|
|
|
if (!config.headers)
|
|
config.headers = { };
|
|
config.headers["X-CSRF-Token"] = "1"; /* Any value will do */
|
|
var path = '/oauth/authorize?response_type=token&client_id=openshift-challenging-client';
|
|
var request = new CockpitKubeRequest("GET", path, "", config);
|
|
|
|
return request.then(function(response) {
|
|
/* Shouldn't return success. Not OAuth capable */
|
|
return "";
|
|
}, function(response) {
|
|
if (response.status == 302) {
|
|
var token;
|
|
var header = response.headers["Location"];
|
|
if (header) {
|
|
/*
|
|
* When OAuth is in play (ie: Openshift, Origin, Atomic, then
|
|
* user/password basic auth doesn't work for accessing the API.
|
|
*
|
|
* Unfortunately kubectl won't let us save both user/password and
|
|
* the token (if we wanted it for future use). So we have to remove
|
|
* the user and password data.
|
|
*/
|
|
token = parseBearerToken(header);
|
|
if (token) {
|
|
delete user.user.username;
|
|
delete user.user.password;
|
|
user.user.token = token;
|
|
}
|
|
}
|
|
return "";
|
|
} else if (response.status == 404) {
|
|
return ""; /* Not OAuth capable */
|
|
} else {
|
|
return $q.reject(response);
|
|
}
|
|
});
|
|
}
|
|
|
|
function writeKubectlConfig(cluster, user, context) {
|
|
var cluster_args, user_args, cmd_args;
|
|
var commands = [];
|
|
var promise;
|
|
|
|
// Everything here must run serially
|
|
if (user && user.user) {
|
|
user_args = [ "kubectl", "config", "set-credentials", user.name ];
|
|
|
|
if (user.user.username) {
|
|
user_args.push("--username=" + user.user.username);
|
|
user_args.push("--password=" + (user.user.password || ""));
|
|
}
|
|
|
|
if (user.user.token)
|
|
user_args.push("--token=" + user.user.token);
|
|
|
|
commands.push(user_args);
|
|
}
|
|
|
|
if (cluster && cluster.cluster) {
|
|
cluster_args = [ "kubectl", "config", "set-cluster",
|
|
cluster.name, "--server=" + cluster.cluster.server,
|
|
"--insecure-skip-tls-verify=" + !!cluster.cluster["insecure-skip-tls-verify"]
|
|
];
|
|
commands.push(cluster_args);
|
|
}
|
|
|
|
if (context) {
|
|
cmd_args = [ "kubectl", "config", "set-context", context.name ];
|
|
|
|
angular.forEach(["namespace", "user", "cluster"], function(value, key) {
|
|
if (context.context && context.context[value])
|
|
cmd_args.push("--" + value + "=" + context.context[value]);
|
|
});
|
|
commands.push(cmd_args);
|
|
commands.push([ "kubectl", "config", "use-context", context.name ]);
|
|
}
|
|
|
|
promise = $q.when();
|
|
angular.forEach(commands, function(command) {
|
|
promise = promise.then(function (result) {
|
|
return runCommand(command);
|
|
});
|
|
});
|
|
|
|
return promise.then(function () {
|
|
return kubectlData().then(function(data) {
|
|
return loadConfigData(data);
|
|
});
|
|
}).catch(kubectlError);
|
|
}
|
|
|
|
return {
|
|
prepareData: prepareData,
|
|
load: load,
|
|
writeKubectlConfig: writeKubectlConfig,
|
|
populateBearerToken: populateBearerToken
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller("ChangeAuthCtrl", [
|
|
"$q",
|
|
"$scope",
|
|
"$modalInstance",
|
|
"dialogData",
|
|
"connectionActions",
|
|
"CockpitKubeRequest",
|
|
"cockpitKubectlConfig",
|
|
"sessionCertificates",
|
|
function($q, $scope, instance, dialogData, connectionActions,
|
|
CockpitKubeRequest, kubectl, sessionCertificates) {
|
|
angular.extend($scope, dialogData);
|
|
|
|
function connect(data) {
|
|
var cluster = data.currentCluster ? data.currentCluster.cluster : null;
|
|
var user = data.currentUser ? data.currentUser.user : null;
|
|
|
|
var options = kubectl.generateKubeOptions(cluster, user);
|
|
var trust = sessionCertificates.getCert(options.address);
|
|
|
|
var force = $scope.haveKubectl ? "kubectl" : options;
|
|
if (options.tls && trust) {
|
|
options.tls["authority"] = { data: trust };
|
|
force = options;
|
|
}
|
|
|
|
var promise = new CockpitKubeRequest("GET", "/api", "", options);
|
|
return promise.then(function() {
|
|
return force;
|
|
}).catch(function (ex) {
|
|
data.error = ex;
|
|
angular.extend($scope, data);
|
|
$scope.$broadcast("loadData");
|
|
|
|
return $q.reject([]);
|
|
});
|
|
}
|
|
|
|
$scope.$on("selectUser", function (ev, user) {
|
|
var users = $scope.users || {};
|
|
if (user && user.name && !users[user.name])
|
|
delete user.name;
|
|
|
|
$scope.currentUser = user;
|
|
});
|
|
|
|
$scope.$on("selectCluster", function (ev, cluster) {
|
|
var clusters = $scope.clusters || {};
|
|
if (cluster && cluster.name && !clusters[cluster.name])
|
|
delete cluster.name;
|
|
|
|
$scope.currentCluster = cluster;
|
|
});
|
|
|
|
$scope.saveAndConnect = function (data) {
|
|
return connectionActions.populateBearerToken(data.cluster, data.user)
|
|
.then(function () {
|
|
if ($scope.haveKubectl) {
|
|
return connectionActions.writeKubectlConfig(data.cluster, data.user, data.context);
|
|
} else {
|
|
return $q.when({
|
|
currentUser: data.user,
|
|
currentCluster: data.cluster,
|
|
});
|
|
}
|
|
}, function (ex) {
|
|
$scope.currentUser = data.user;
|
|
$scope.currentCluster = data.cluster;
|
|
$scope.error = ex;
|
|
|
|
$scope.$broadcast("loadData");
|
|
return $q.reject([]);
|
|
})
|
|
.then(connect);
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive("authForm", [
|
|
"$q",
|
|
"connectionActions",
|
|
"CockpitTranslate",
|
|
"CockpitFormat",
|
|
function($q, connectionActions, translate, format) {
|
|
const _ = translate.gettext;
|
|
return {
|
|
restrict: "E",
|
|
scope: true,
|
|
link: function($scope, element, attrs) {
|
|
$scope.fields = {};
|
|
|
|
function loadData() {
|
|
if ($scope.error) {
|
|
var msg = $scope.error.statusText;
|
|
if (!msg)
|
|
msg = $scope.error.problem;
|
|
|
|
if (msg == "not-found")
|
|
msg = _("Couldn't find running API server");
|
|
|
|
$scope.failure(format.format(_("Connection Error: $0"), msg));
|
|
}
|
|
|
|
if ($scope.currentCluster)
|
|
$scope.selectCluster($scope.currentCluster);
|
|
else
|
|
$scope.fields.address = $scope.defaultAddress;
|
|
|
|
if ($scope.currentUser)
|
|
$scope.selectUser($scope.currentUser);
|
|
|
|
$scope.useAuth = !!$scope.currentUser;
|
|
}
|
|
|
|
function validate() {
|
|
var errors = [];
|
|
var ex;
|
|
var address_re = /^[a-z0-9:/.-]+$/i;
|
|
var address = $scope.fields.address;
|
|
var cluster = { cluster: {} };
|
|
var user;
|
|
|
|
if (!$scope.fields.address || !address_re.test(address.toLowerCase())) {
|
|
ex = new Error(_("Please provide a valid address"));
|
|
ex.target = "#kubernetes-address";
|
|
errors.push(ex);
|
|
ex = null;
|
|
} else if (address.indexOf("http://") !== 0 &&
|
|
address.indexOf("https://") !== 0) {
|
|
address = "http://" + address;
|
|
}
|
|
|
|
user = $scope.useAuth ? $scope.currentUser : null;
|
|
if (!user && $scope.useAuth)
|
|
user = { user: {} };
|
|
|
|
if (user && !$scope.fields.username &&
|
|
(!user.name || user.user.username)) {
|
|
ex = new Error(_("Please provide a username"));
|
|
ex.target = "#kubernetes-username";
|
|
errors.push(ex);
|
|
ex = null;
|
|
}
|
|
|
|
if ($scope.currentCluster)
|
|
cluster = $scope.currentCluster;
|
|
|
|
cluster.cluster.server = address;
|
|
cluster.cluster["insecure-skip-tls-verify"] = !!$scope.fields.skipVerify;
|
|
|
|
if (user) {
|
|
if ($scope.fields.username) {
|
|
user.user.username = $scope.fields.username;
|
|
user.user.password = $scope.fields.password;
|
|
} else {
|
|
delete user.user.username;
|
|
delete user.user.password;
|
|
}
|
|
if ($scope.fields.token)
|
|
user.user.token = $scope.fields.token;
|
|
else
|
|
delete user.user.token;
|
|
}
|
|
|
|
if (errors.length > 0)
|
|
return $q.reject(errors);
|
|
|
|
var data = connectionActions.prepareData($scope.config, cluster, user);
|
|
return $q.when(data);
|
|
}
|
|
|
|
$scope.selectCluster = function selectCluster(cluster) {
|
|
var inner = cluster && cluster.cluster ? cluster.cluster : {};
|
|
$scope.fields.address = inner.server;
|
|
$scope.fields.skipVerify = !!inner["insecure-skip-tls-verify"];
|
|
$scope.$emit("selectCluster", cluster);
|
|
};
|
|
|
|
$scope.selectUser = function selectUser(user) {
|
|
var inner = user && user.user ? user.user : {};
|
|
$scope.fields.username = inner.username;
|
|
$scope.fields.password = inner.password;
|
|
$scope.fields.token = inner.token;
|
|
$scope.$emit("selectUser", user);
|
|
};
|
|
|
|
$scope.hasCert = function hasCert(user) {
|
|
if (user && user.user) return user.user["client-key"] || user.user["client-key-data"];
|
|
return false;
|
|
};
|
|
|
|
$scope.toggleAuth = function toggleAuth() {
|
|
$scope.useAuth = !$scope.useAuth;
|
|
};
|
|
|
|
$scope.update = function() {
|
|
return validate().then(function (data) {
|
|
return $scope.saveAndConnect(data);
|
|
});
|
|
};
|
|
|
|
$scope.$on("loadData", loadData);
|
|
loadData();
|
|
},
|
|
templateUrl: "views/auth-form.html"
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive("authRejectedCert", [
|
|
"$q",
|
|
"connectionActions",
|
|
"sessionCertificates",
|
|
"cockpitRunCommand",
|
|
"CockpitTranslate",
|
|
"CockpitFormat",
|
|
function($q, connectionActions, sessionCertificates,
|
|
runCommand, translate, format) {
|
|
const _ = translate.gettext;
|
|
return {
|
|
restrict: "E",
|
|
scope: true,
|
|
link: function($scope, element, attrs) {
|
|
var pem = null;
|
|
$scope.address = null;
|
|
|
|
function getCertDetails() {
|
|
var options = $scope.error ? $scope.error.options : {};
|
|
var cmd = runCommand([ "openssl", "x509", "-noout", "-text" ]);
|
|
options = options || {};
|
|
pem = options["rejected-certificate"];
|
|
cmd.then(function(data) {
|
|
$scope.details = data;
|
|
}, function(ex) {
|
|
var msg = format.format(_("Error getting certificate details: $0"), ex.problem);
|
|
$scope.failure(msg);
|
|
});
|
|
cmd.send(pem);
|
|
cmd.send("\n\n");
|
|
}
|
|
|
|
function loadData() {
|
|
if ($scope.currentCluster)
|
|
$scope.address = $scope.currentCluster.cluster.server;
|
|
else
|
|
$scope.address = $scope.defaultAddress;
|
|
|
|
$scope.action = "skip";
|
|
$scope.details = null;
|
|
getCertDetails();
|
|
}
|
|
|
|
$scope.update = function update() {
|
|
var cluster = { cluster: { server: $scope.address } };
|
|
if ($scope.currentCluster)
|
|
cluster = $scope.currentCluster;
|
|
|
|
var data = connectionActions.prepareData($scope.config, cluster, $scope.currentUser);
|
|
if ($scope.action == "pem")
|
|
sessionCertificates.trustCert(data.cluster.cluster, pem);
|
|
|
|
if ($scope.action != "pem")
|
|
data.cluster.cluster['insecure-skip-tls-verify'] = true;
|
|
|
|
return $scope.saveAndConnect(data);
|
|
};
|
|
|
|
$scope.$on("loadData", loadData);
|
|
loadData();
|
|
},
|
|
templateUrl: "views/auth-rejected-cert.html"
|
|
};
|
|
}
|
|
]);
|
|
}());
|