1230 lines
52 KiB
JavaScript
1230 lines
52 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');
|
|
var cockpit = require('cockpit');
|
|
|
|
require('./kube-client');
|
|
|
|
function debug() {
|
|
if (window.debugging == "all" || window.debugging == "kube")
|
|
console.debug.apply(console, arguments);
|
|
}
|
|
|
|
function updateMessage(response) {
|
|
if (!response)
|
|
return;
|
|
|
|
var obj;
|
|
try {
|
|
obj = JSON.parse(response.data);
|
|
} catch (e) {
|
|
// Some kubernetes versions message up json reponses
|
|
if (response.data && response.headers &&
|
|
response.headers["Content-Type"] === "text/plain") {
|
|
obj = { message: response.data };
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (obj && obj.message)
|
|
response.message = response.statusText = obj.message;
|
|
}
|
|
|
|
/*
|
|
* Currently we assume that the certificates in the kube config
|
|
* file are:
|
|
*
|
|
* base64(PEM(data))
|
|
*
|
|
* Since our http-stream2 expects PEM certificates (although they're
|
|
* nasty, they're better than all the alternatives) so we strip out
|
|
* the outer base64 layer.
|
|
*/
|
|
|
|
function parseCertOption(object, option) {
|
|
var blob = object[option + "-data"];
|
|
if (blob !== undefined)
|
|
return { data: window.atob(blob) };
|
|
|
|
var file = object[option];
|
|
if (file !== undefined)
|
|
return { file: file };
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function basicToken(user, pass) {
|
|
return window.btoa(window.unescape(encodeURIComponent(user + ":" + pass)));
|
|
}
|
|
|
|
angular.module("kubeClient.cockpit", [
|
|
"kubeClient",
|
|
])
|
|
|
|
.factory("CockpitKubeWatch", [
|
|
"$q",
|
|
"KUBE_SCHEMA",
|
|
"cockpitKubeDiscover",
|
|
function($q, KUBE_SCHEMA, cockpitKubeDiscover) {
|
|
return function CockpitKubeWatch(path, callback) {
|
|
debug("creating watch:", path);
|
|
|
|
/* Used to track the last resource for restarting query */
|
|
var lastResource;
|
|
|
|
/* Whether close has been called */
|
|
var stopping = false;
|
|
|
|
/* The current HTTP request */
|
|
var channel = null;
|
|
|
|
/* The http options */
|
|
var http = null;
|
|
|
|
/* Waiting to do the next http request */
|
|
var wait = null;
|
|
|
|
/*
|
|
* Loading logic.
|
|
*
|
|
* For performance, we only use watches here. So we have
|
|
* to guess when loading is finished and when updates begin.
|
|
* There are several heuristics:
|
|
*
|
|
* 1) Receiving a MODIFY or DELETE means loading has finished.
|
|
* 2) A timeout after last ADDED
|
|
* 3) Error or connection closed.
|
|
*
|
|
* Remember that a watch object can restart its request for a number
|
|
* of reasons, and so the loading/loaded state may go back and forth.
|
|
*
|
|
* When transitioning from a loading to a loaded state, we have to:
|
|
* a) Notify the caller if not already done
|
|
* b) See if any objects present before load need to be removed.
|
|
*/
|
|
|
|
var defer = $q.defer();
|
|
var promise = defer.promise;
|
|
var loaded = false;
|
|
var objects = { };
|
|
var previous;
|
|
var loading;
|
|
|
|
function loadBegin(full) {
|
|
if (full) {
|
|
previous = objects;
|
|
objects = { };
|
|
} else {
|
|
previous = null;
|
|
}
|
|
loadPoke(true);
|
|
}
|
|
|
|
function loadPoke(force) {
|
|
if (force || loading !== undefined) {
|
|
window.clearTimeout(loading);
|
|
loading = window.setTimeout(loadFinish, 100);
|
|
}
|
|
}
|
|
|
|
function loadFinish(ex) {
|
|
if (loaded)
|
|
return;
|
|
loaded = true;
|
|
|
|
var key, prev;
|
|
var frames = [];
|
|
prev = previous;
|
|
previous = null;
|
|
for (key in prev) {
|
|
if (!(key in objects))
|
|
frames.push({ type: "DELETED", object: prev[key] });
|
|
}
|
|
|
|
/* Simulated delete frames */
|
|
if (frames.length)
|
|
callback(frames);
|
|
|
|
/* Tell callback to flush */
|
|
callback([]);
|
|
|
|
if (ex)
|
|
defer.reject(ex);
|
|
else
|
|
defer.resolve();
|
|
}
|
|
|
|
/*
|
|
* Each change is sent as an individual line from Kubernetes
|
|
* but they may not arrive exactly that way, so we buffer
|
|
* and split lines again.
|
|
*/
|
|
|
|
var buffer;
|
|
function handleWatch(data) {
|
|
if (buffer)
|
|
data = buffer + data;
|
|
|
|
var lines = data.split("\n");
|
|
var i;
|
|
var length = lines.length - 1;
|
|
|
|
/* Last line is incomplete save for later */
|
|
buffer = lines[length];
|
|
|
|
var frames = [];
|
|
|
|
/* Process all the others */
|
|
var frame, object;
|
|
for (i = 0; i < length; i++) {
|
|
try {
|
|
frame = JSON.parse(lines[i]);
|
|
} catch (ex) {
|
|
console.warn(lines[i], ex);
|
|
channel.close();
|
|
continue;
|
|
}
|
|
|
|
object = frame.object;
|
|
if (!object) {
|
|
console.warn("invalid watch without object", frame);
|
|
continue;
|
|
}
|
|
|
|
/* The watch failed, likely due to invalid resourceVersion */
|
|
if (frame.type == "ERROR") {
|
|
if (lastResource) {
|
|
lastResource = null;
|
|
startWatch();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
var meta = object.metadata;
|
|
if (!meta || (!meta.uid && object.kind !== "Project") || !object.kind) {
|
|
console.warn("invalid kube object: ", object);
|
|
continue;
|
|
}
|
|
|
|
lastResource = meta.resourceVersion;
|
|
|
|
/* We track objects here so we can restart watches */
|
|
var uid = meta.uid;
|
|
if (frame.type == "DELETED")
|
|
delete objects[uid];
|
|
else
|
|
objects[uid] = object;
|
|
|
|
debug(frame.type, object.kind, meta.uid);
|
|
frames.push(frame);
|
|
}
|
|
|
|
callback(frames);
|
|
loadPoke();
|
|
}
|
|
|
|
function startWatch() {
|
|
if (channel)
|
|
return;
|
|
|
|
var full = true;
|
|
var uri = path + "?watch=true";
|
|
|
|
/*
|
|
* If we have a last resource we can guarantee that we don't miss
|
|
* any objects or changes to objects. If we don't have one, then we
|
|
* have to list everything again. Still watch at the same time though.
|
|
*/
|
|
if (lastResource) {
|
|
uri += "&resourceVersion=" + encodeURIComponent(lastResource);
|
|
full = false;
|
|
}
|
|
|
|
/*
|
|
* As a precaution, watch must take at least 1 second
|
|
* to complete. Otherwise we could be in a tight loop here.
|
|
* eg: if the API of Kubernetes changes unpredictably.
|
|
*/
|
|
var blocked = false;
|
|
window.setTimeout(function() {
|
|
blocked = true;
|
|
}, 1000);
|
|
|
|
var opts = angular.extend({ }, http, {
|
|
path: uri,
|
|
method: "GET"
|
|
});
|
|
|
|
channel = cockpit.channel(opts);
|
|
|
|
var response = { };
|
|
var failed = false;
|
|
channel.addEventListener("control", function(ev, options) {
|
|
if (options.command == "response") {
|
|
response = options;
|
|
if (response.status > 299)
|
|
failed = true;
|
|
loadBegin(full);
|
|
}
|
|
});
|
|
|
|
channel.addEventListener("message", function(ev, payload) {
|
|
if (failed)
|
|
response.data = (response.data || "") + payload;
|
|
else
|
|
handleWatch(payload);
|
|
});
|
|
|
|
channel.addEventListener("close", function(ev, options) {
|
|
channel = null;
|
|
loading = false;
|
|
if (stopping)
|
|
return;
|
|
|
|
var msg = "watching " + path + " failed: ";
|
|
var problem = options.problem;
|
|
var status = response.status;
|
|
|
|
if (problem) {
|
|
msg += problem;
|
|
if (problem == "disconnected")
|
|
debug(msg);
|
|
else
|
|
console.warn(msg);
|
|
response.problem = problem;
|
|
response.status = 999;
|
|
loadFinish(response);
|
|
} else if (failed) {
|
|
updateMessage(response);
|
|
msg += (response.message || response.reason || status);
|
|
if (status === 404 || status === 410 || status === 403) {
|
|
debug(msg);
|
|
loadFinish(response);
|
|
return; /* don't try watch again if we get a 404/410/403 */
|
|
} else {
|
|
console.warn(msg);
|
|
}
|
|
loadFinish(response);
|
|
} else if (!blocked) {
|
|
console.warn("watching kube " + path + " didn't block");
|
|
} else {
|
|
startWatch();
|
|
return;
|
|
}
|
|
|
|
startWatchLater();
|
|
});
|
|
|
|
/* No http request body */
|
|
channel.control({ command: "done" });
|
|
}
|
|
|
|
function startWatchLater() {
|
|
if (!wait) {
|
|
wait = window.setTimeout(function() {
|
|
wait = null;
|
|
startWatch();
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
$q.when(cockpitKubeDiscover(), function(options) {
|
|
http = options;
|
|
startWatch();
|
|
}, function(resp) {
|
|
loadFinish(resp);
|
|
});
|
|
|
|
promise.cancel = function cancel(ex) {
|
|
stopping = true;
|
|
var problem;
|
|
if (channel) {
|
|
if (ex)
|
|
problem = ex.problem;
|
|
channel.close(problem || "disconnected");
|
|
channel = null;
|
|
}
|
|
window.clearTimeout(wait);
|
|
wait = null;
|
|
http = null;
|
|
loadFinish(ex);
|
|
};
|
|
|
|
return promise;
|
|
};
|
|
}
|
|
])
|
|
|
|
/*
|
|
* A WebSocket factory for the kubernetes-container-terminal
|
|
* that uses the kubectl command with a fake websocket
|
|
*/
|
|
.factory("CockpitKubectlWebSocket", [
|
|
function() {
|
|
function decode(href, options) {
|
|
var pos = href.indexOf('?');
|
|
var first = href;
|
|
var path;
|
|
if (pos === -1)
|
|
first = href;
|
|
else
|
|
first = href.substr(0, pos);
|
|
|
|
path = first.split('/').map(decodeURIComponent);
|
|
|
|
if (pos !== -1) {
|
|
angular.forEach(href.substring(pos + 1).split("&"), function(opt) {
|
|
var last;
|
|
var parts = opt.split('=');
|
|
var name = decodeURIComponent(parts[0]);
|
|
var value = decodeURIComponent(parts[1]);
|
|
if (options.hasOwnProperty(name)) {
|
|
last = options[name];
|
|
if (!angular.isArray(last))
|
|
last = options[name] = [ last ];
|
|
last.push(value);
|
|
} else {
|
|
options[name] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
function parser(url) {
|
|
var options = { };
|
|
var path = decode(url, options);
|
|
|
|
var command = [ ];
|
|
var args = [ ];
|
|
var namespace = "default";
|
|
var container = null;
|
|
var cmd = "log";
|
|
var pod = "";
|
|
|
|
var i, len;
|
|
for (i = 0, len = path.length; i < len; i++) {
|
|
if (path[i] === "namespaces") {
|
|
namespace = path[++i];
|
|
} else if (path[i] === "pods") {
|
|
pod = path[++i];
|
|
if (path[i + 1] == "exec")
|
|
cmd = "exec";
|
|
else if (path[i + 1] == "log")
|
|
cmd = "logs";
|
|
}
|
|
}
|
|
|
|
for (i in options) {
|
|
if (i == "container") {
|
|
container = options[i];
|
|
} else if (i == "command") {
|
|
if (angular.isArray(options[i]))
|
|
command = options[i];
|
|
else
|
|
command.push(options[i]);
|
|
} else if (i == "stdin" || i == "tty" || i == "follow") {
|
|
args.push("--" + i);
|
|
}
|
|
}
|
|
|
|
var ret = [ "kubectl", cmd, "--namespace=" + namespace ];
|
|
if (container)
|
|
ret.push("--container=" + container);
|
|
ret.push.apply(ret, args);
|
|
ret.push(pod, "--");
|
|
ret.push.apply(ret, command);
|
|
return ret;
|
|
}
|
|
|
|
return function KubeFakeWebSocket(url, protocols) {
|
|
var cmd = parser(url);
|
|
var base64 = false;
|
|
|
|
/* A fake WebSocket */
|
|
var channel;
|
|
var state = 0; /* CONNECTING */
|
|
var ws = { };
|
|
cockpit.event_target(ws);
|
|
|
|
function open() {
|
|
channel = cockpit.channel({
|
|
payload: "stream",
|
|
spawn: cmd,
|
|
pty: true
|
|
});
|
|
|
|
channel.addEventListener("close", function(ev, options) {
|
|
channel = null;
|
|
|
|
state = 3;
|
|
var cev = new Event("close", { bubbles: false, cancelable: false });
|
|
ws.dispatchEvent(cev);
|
|
});
|
|
|
|
channel.addEventListener("message", function(ev, data) {
|
|
if (base64)
|
|
data = "1" + window.btoa(data);
|
|
var mev = new window.MessageEvent('message', { data: data });
|
|
ws.dispatchEvent(mev);
|
|
});
|
|
|
|
state = 1;
|
|
var oev = new Event("open", { bubbles: false, cancelable: false });
|
|
ws.dispatchEvent(oev);
|
|
}
|
|
|
|
function fail() {
|
|
var ev = new Event("close", { bubbles: false, cancelable: false });
|
|
ws.dispatchEvent(ev);
|
|
}
|
|
|
|
function close(code, reason) {
|
|
if (channel)
|
|
channel.close(reason);
|
|
}
|
|
|
|
function send(data) {
|
|
if (base64) {
|
|
/*
|
|
* HACK: container-terminal sends Width/Height commands but
|
|
* many of the kubernetes implementations don't yet support
|
|
* that. So filter them out here for now.
|
|
*/
|
|
if (data[0] === "4")
|
|
return;
|
|
data = window.atob(data.slice(1));
|
|
}
|
|
|
|
if (channel)
|
|
channel.send(data);
|
|
}
|
|
|
|
var valid = true;
|
|
if (protocols) {
|
|
if (angular.isArray(protocols))
|
|
valid = base64 = protocols.indexOf("base64.channel.k8s.io") !== -1;
|
|
else
|
|
valid = base64 = protocols === "base64.channel.k8s.io";
|
|
}
|
|
|
|
/* A fake WebSocket */
|
|
Object.defineProperties(ws, {
|
|
binaryType: { value: "arraybuffer" },
|
|
bufferedAmount: { value: 0 },
|
|
extensions: { value: "" },
|
|
protocol: { value: base64 ? "base64.channel.k8s.io" : "" },
|
|
readyState: { get: function() { return state } },
|
|
url: { value: url },
|
|
close: { value: close },
|
|
send: { value: send },
|
|
});
|
|
|
|
if (valid) {
|
|
window.setTimeout(open);
|
|
} else {
|
|
console.warn("Unsupported kubernetes container WebSocket subprotocol: " + protocols);
|
|
window.setTimeout(fail);
|
|
}
|
|
|
|
return ws;
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("CockpitKubeSocket", [
|
|
"$q",
|
|
"$injector",
|
|
function($q, $injector) {
|
|
return function CockpitKubeSocket(url, config) {
|
|
var base64 = false;
|
|
var connect;
|
|
var state = 0; /* CONNECTING */
|
|
var ws = { };
|
|
var channel;
|
|
|
|
var protocols = [];
|
|
if (config && config.protocols) {
|
|
protocols = config.protocols;
|
|
if (!angular.isArray(protocols))
|
|
protocols = [ String(config.protocols) ];
|
|
}
|
|
|
|
/*
|
|
* If we're called with fully formed options, then don't do
|
|
* connect discovery stuff. Otherwise ask our connect service
|
|
* for connection info, and do discovery.
|
|
*/
|
|
if (config && config.port)
|
|
connect = config;
|
|
else
|
|
connect = $injector.get('cockpitKubeDiscover')();
|
|
|
|
function close(code, reason) {
|
|
if (channel)
|
|
channel.close(reason);
|
|
}
|
|
|
|
function send(data) {
|
|
/*
|
|
* HACK: container-terminal sends Width/Height commands but
|
|
* many of the kubernetes implementations don't yet support
|
|
* that. So filter them out here for now.
|
|
*/
|
|
if (base64 && data[0] === "4")
|
|
return;
|
|
|
|
if (channel)
|
|
channel.send(data);
|
|
}
|
|
|
|
/* A fake WebSocket */
|
|
Object.defineProperties(ws, {
|
|
binaryType: { value: "arraybuffer" },
|
|
bufferedAmount: { value: 0 },
|
|
extensions: { value: "" },
|
|
protocol: { value: protocols[0] },
|
|
readyState: { get: function() { return state } },
|
|
url: { value: url },
|
|
close: { value: close },
|
|
send: { value: send },
|
|
});
|
|
|
|
base64 = protocols.indexOf("base64.channel.k8s.io") !== -1;
|
|
|
|
$q.when(connect, function connected(options) {
|
|
cockpit.event_target(ws);
|
|
|
|
channel = cockpit.channel(angular.extend({ }, options, {
|
|
payload: "websocket-stream1",
|
|
path: url,
|
|
protocols: protocols.length > 0 ? protocols : undefined,
|
|
}));
|
|
|
|
channel.addEventListener("close", function(ev, options) {
|
|
channel = null;
|
|
|
|
state = 3;
|
|
var cev = new Event("close", { bubbles: false, cancelable: false });
|
|
ws.dispatchEvent(cev);
|
|
});
|
|
|
|
channel.addEventListener("message", function(ev, data) {
|
|
var mev = new window.MessageEvent('message', { data: data });
|
|
ws.dispatchEvent(mev);
|
|
});
|
|
|
|
state = 1;
|
|
var oev = new Event("open", { bubbles: false, cancelable: false });
|
|
ws.dispatchEvent(oev);
|
|
});
|
|
|
|
return ws;
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("CockpitKubeRequest", [
|
|
"$q",
|
|
"$injector",
|
|
function($q, $injector) {
|
|
var CONTENT_TYPE = "Content-Type";
|
|
var JSON_TYPE = "application/json";
|
|
return function CockpitKubeRequest(method, path, body, config) {
|
|
var defer = $q.defer();
|
|
var promise = defer.promise;
|
|
var connect, channel;
|
|
|
|
var heads = { };
|
|
if (body && typeof body == "object") {
|
|
body = JSON.stringify(body);
|
|
heads[CONTENT_TYPE] = JSON_TYPE;
|
|
}
|
|
|
|
if (!config)
|
|
config = { };
|
|
|
|
/*
|
|
* If we're called with fully formed options, then don't do
|
|
* connect discovery stuff. Otherwise ask our connect service
|
|
* for connection info, and do discovery.
|
|
*/
|
|
if (config && config.port)
|
|
connect = config;
|
|
else
|
|
connect = $injector.get('cockpitKubeDiscover')();
|
|
|
|
$q.when(connect, function connected(options) {
|
|
var opts = angular.extend({ }, config, options, {
|
|
path: path,
|
|
method: method,
|
|
payload: "http-stream2"
|
|
});
|
|
|
|
opts.headers = angular.extend(heads, config.headers || { }, options.headers || { });
|
|
|
|
debug("request:", method, path, opts);
|
|
channel = cockpit.channel(opts);
|
|
|
|
var response = { };
|
|
channel.addEventListener("control", function(ev, options) {
|
|
if (options.command == "response") {
|
|
response = options;
|
|
response.statusText = response.reason;
|
|
}
|
|
});
|
|
|
|
channel.addEventListener("message", function(ev, payload) {
|
|
response.data = (response.data || "") + payload;
|
|
});
|
|
|
|
channel.addEventListener("close", function(ev, options) {
|
|
channel = null;
|
|
|
|
response.options = options;
|
|
if (options.problem) {
|
|
response.problem = response.statusText = options.problem;
|
|
response.status = 999;
|
|
}
|
|
|
|
var headers = response.headers || { };
|
|
var content_type = headers[CONTENT_TYPE] || headers[CONTENT_TYPE.toLowerCase()] || "";
|
|
if (content_type.lastIndexOf(JSON_TYPE, 0) === 0) {
|
|
try {
|
|
response.data = JSON.parse(response.data);
|
|
} catch (ex) {
|
|
/* it's not JSON, just leave as text */
|
|
}
|
|
}
|
|
|
|
if (response.status > 299) {
|
|
updateMessage(response);
|
|
defer.reject(response);
|
|
} else {
|
|
defer.resolve(response);
|
|
}
|
|
});
|
|
|
|
if (body)
|
|
channel.send(body);
|
|
channel.control({ command: "done" });
|
|
|
|
/* Failed to connect */
|
|
}, function failed(response) {
|
|
defer.reject(response);
|
|
});
|
|
|
|
/* Helpful function on the promise */
|
|
promise.cancel = function cancel() {
|
|
if (connect.cancel)
|
|
connect.cancel();
|
|
if (channel)
|
|
channel.close("cancelled");
|
|
};
|
|
|
|
return promise;
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("cockpitRunCommand", [
|
|
'$q',
|
|
function($q) {
|
|
return function runCommand(args) {
|
|
var defer = $q.defer();
|
|
var promise = defer.promise;
|
|
var channel = cockpit.channel({
|
|
"payload": "stream",
|
|
"spawn": args,
|
|
"err": "message"
|
|
});
|
|
|
|
var result = "";
|
|
channel.addEventListener("message", function(ev, payload) {
|
|
result += payload;
|
|
});
|
|
channel.addEventListener("close", function(ev, options) {
|
|
channel = null;
|
|
if (options.problem)
|
|
defer.reject(options);
|
|
else
|
|
defer.resolve(result);
|
|
});
|
|
|
|
promise.cancel = function cancel(options) {
|
|
if (channel)
|
|
channel.close(options || "cancelled");
|
|
};
|
|
|
|
promise.send = function send(data) {
|
|
if (channel)
|
|
channel.send(data);
|
|
};
|
|
|
|
return promise;
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("cockpitKubectlConfig", [
|
|
'$q',
|
|
'cockpitRunCommand',
|
|
function($q, runCommand) {
|
|
function generateKubeOptions(cluster, user) {
|
|
var parser, token, provider;
|
|
var options = { port: 8080, headers: { } };
|
|
|
|
if (cluster && cluster.server) {
|
|
parser = document.createElement('a');
|
|
parser.href = cluster.server;
|
|
if (parser.hostname)
|
|
options.address = parser.hostname;
|
|
if (parser.port)
|
|
options.port = parseInt(parser.port, 10);
|
|
if (parser.protocol == 'https:') {
|
|
if (!parser.port || parser.port === "0")
|
|
options.port = parser.href == cluster.server ? 6443 : 443;
|
|
|
|
options.tls = { };
|
|
options.tls.authority = parseCertOption(cluster, "certificate-authority");
|
|
options.tls.validate = !cluster["insecure-skip-tls-verify"];
|
|
}
|
|
}
|
|
|
|
if (user) {
|
|
token = user.token;
|
|
provider = user["auth-provider"] || {};
|
|
if (provider.config) {
|
|
if (provider.config['access-token'])
|
|
token = provider.config['access-token'];
|
|
else if (provider.config['token'])
|
|
token = provider.config['token'];
|
|
}
|
|
|
|
if (token)
|
|
options.headers["Authorization"] = "Bearer " + token;
|
|
if (user.username)
|
|
options.headers["Authorization"] = "Basic " + basicToken(user.username, user.password || "");
|
|
if (options.tls) {
|
|
options.tls.certificate = parseCertOption(user, "client-certificate");
|
|
options.tls.key = parseCertOption(user, "client-key");
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function parseKubeConfig(data, contextName) {
|
|
var config, options;
|
|
|
|
try {
|
|
config = JSON.parse(data);
|
|
} catch (ex) {
|
|
console.warn("received invalid kubectl config", ex);
|
|
return null;
|
|
}
|
|
|
|
if (!contextName)
|
|
contextName = config["current-context"];
|
|
var contexts = config["contexts"] || [];
|
|
|
|
/* Find the cluster info */
|
|
var userName, clusterName;
|
|
contexts.forEach(function(info) {
|
|
if (info.name === contextName) {
|
|
var context = info.context || { };
|
|
userName = context.user;
|
|
clusterName = context.cluster;
|
|
}
|
|
});
|
|
|
|
/* Find the user info */
|
|
var user;
|
|
var users = config["users"] || [];
|
|
users.forEach(function(info) {
|
|
if (info.name === userName)
|
|
user = info.user;
|
|
});
|
|
|
|
/* Find the cluster info */
|
|
var cluster;
|
|
var clusters = config["clusters"] || [];
|
|
clusters.forEach(function(info) {
|
|
if (info.name == clusterName)
|
|
cluster = info.cluster;
|
|
});
|
|
|
|
options = generateKubeOptions(cluster, user);
|
|
debug("parsed kube config", options);
|
|
return options;
|
|
}
|
|
|
|
function read() {
|
|
var cmd = ["kubectl", "config", "view", "--output=json", "--raw"];
|
|
|
|
/* Call kubectl minified config view. That only outputs
|
|
* the objects that would be used by a connection */
|
|
return runCommand(["kubectl", "config", "view", "--minify", "--output=json"])
|
|
.then(function (data) {
|
|
var p;
|
|
var auth_provider;
|
|
var user;
|
|
|
|
/* If the default data has a 'auth-provider'
|
|
* section then call kubectl version to try to
|
|
* get it to fill in any token data.
|
|
*/
|
|
try {
|
|
user = JSON.parse(data)["users"][0];
|
|
if (user && user['user'])
|
|
auth_provider = user['user']['auth-provider'];
|
|
} catch (ex) {
|
|
console.warn("received invalid kubectl config", ex);
|
|
}
|
|
|
|
if (auth_provider) {
|
|
p = runCommand(["kubectl", "version"])
|
|
.then(function () {
|
|
return runCommand(cmd);
|
|
}, function () {
|
|
return runCommand(cmd);
|
|
});
|
|
} else {
|
|
p = runCommand(cmd);
|
|
}
|
|
|
|
return p;
|
|
});
|
|
}
|
|
|
|
return {
|
|
read: read,
|
|
parseKubeConfig: parseKubeConfig,
|
|
generateKubeOptions: generateKubeOptions,
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("cockpitKubeDiscover", [
|
|
"$q",
|
|
"CockpitKubeRequest",
|
|
"cockpitKubectlConfig",
|
|
"cockpitConnectionInfo",
|
|
"cockpitBrowserStorage",
|
|
function($q, CockpitKubeRequest, cockpitKubectlConfig, info, browser) {
|
|
var defer = null;
|
|
|
|
return function cockpitKubeDiscover(force) {
|
|
if (!force && defer)
|
|
return defer.promise;
|
|
|
|
var last, req, kubectl, loginOptions;
|
|
var loginData = browser.localStorage.getItem('login-data', true);
|
|
defer = $q.defer();
|
|
|
|
var schemes = [
|
|
{ port: 8080 },
|
|
{ port: 8443, tls: { } },
|
|
{ port: 6443, tls: { } },
|
|
];
|
|
|
|
function step(options, kubeConfig) {
|
|
if (!options)
|
|
options = schemes.shift();
|
|
|
|
/* No further ports to try? */
|
|
if (!options) {
|
|
last.statusText = cockpit.gettext("Couldn't find running API server");
|
|
last.problem = "not-found";
|
|
defer.reject(last);
|
|
return;
|
|
}
|
|
|
|
/* If options is a function call it, the function is
|
|
* responsible to call step again when ready */
|
|
if (typeof options === "function") {
|
|
options();
|
|
return;
|
|
}
|
|
|
|
options.payload = "http-stream2";
|
|
debug("trying kube at:", options);
|
|
req = new CockpitKubeRequest("GET", "/api", "", options);
|
|
req.then(function(response) {
|
|
req = null;
|
|
var resp = response.data;
|
|
if (resp && resp.versions) {
|
|
debug("discovered kube api", resp);
|
|
if (kubeConfig) {
|
|
info.kubeConfig = kubeConfig;
|
|
if (kubectl)
|
|
info.type = "kubectl";
|
|
else
|
|
info.type = "sessionData";
|
|
} else {
|
|
info.type = "open";
|
|
}
|
|
|
|
defer.resolve(options);
|
|
} else {
|
|
debug("not an api endpoint:", options);
|
|
last = response;
|
|
kubectl = null;
|
|
step();
|
|
}
|
|
})
|
|
.catch(function(response) {
|
|
req = null;
|
|
kubectl = null;
|
|
last = response;
|
|
|
|
if (response.problem === "not-found") {
|
|
debug("api endpoint not found on:", options);
|
|
step();
|
|
} else {
|
|
debug("connecting to kube failed:", response);
|
|
defer.reject(response);
|
|
}
|
|
});
|
|
}
|
|
|
|
function kubectlStep() {
|
|
kubectl = cockpitKubectlConfig.read()
|
|
.then(function(data) {
|
|
var options = cockpitKubectlConfig.parseKubeConfig(data);
|
|
step(options, options ? JSON.parse(data) : null);
|
|
})
|
|
.catch(function(options) {
|
|
console.warn("kubectl failed: " + (options.message || options.problem));
|
|
step();
|
|
});
|
|
}
|
|
|
|
if (force && typeof force === 'object') {
|
|
schemes = [ force ];
|
|
step();
|
|
} else if (force === "kubectl") {
|
|
schemes = [ kubectlStep ];
|
|
step();
|
|
} else {
|
|
schemes.unshift(kubectlStep);
|
|
if (loginData)
|
|
loginOptions = cockpitKubectlConfig.parseKubeConfig(loginData);
|
|
step(loginOptions, loginOptions ? JSON.parse(loginData) : null);
|
|
}
|
|
|
|
defer.promise.cancel = function cancel() {
|
|
if (kubectl && kubectl.cancel)
|
|
kubectl.cancel("cancelled");
|
|
|
|
if (req)
|
|
req.close("cancelled");
|
|
};
|
|
return defer.promise;
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("CockpitEnvironment", [
|
|
"$q",
|
|
function($q) {
|
|
var defer = $q.defer();
|
|
var settings = null;
|
|
return function cockpitKubeSettings() {
|
|
if (settings !== null)
|
|
return defer.promise;
|
|
var channel = cockpit.channel({ payload: "dbus-json3", bus: "internal" });
|
|
channel.addEventListener("close", function(ev, options) {
|
|
if (options.problem) {
|
|
console.warn("couldn't retrieve environment:", options.problem);
|
|
defer.reject(options);
|
|
} else {
|
|
defer.resolve(settings);
|
|
}
|
|
});
|
|
channel.addEventListener("message", function(ev, data) {
|
|
var result = JSON.parse(data);
|
|
if (result.reply) {
|
|
settings = result.reply[0][0].Variables.v;
|
|
channel.close(null);
|
|
} else if (result.error) {
|
|
console.warn("error retrieving environment:", result.error);
|
|
channel.close("internal-error");
|
|
}
|
|
});
|
|
channel.send(JSON.stringify({
|
|
id: "cookie",
|
|
call: [ "/environment", "org.freedesktop.DBus.Properties", "GetAll",
|
|
[ "cockpit.Environment" ] ]
|
|
}));
|
|
return defer.promise;
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("cockpitKubeDiscoverSettings", [
|
|
"$q",
|
|
"CockpitKubeRequest",
|
|
"cockpitKubeDiscover",
|
|
"CockpitEnvironment",
|
|
'kubeLoader',
|
|
'cockpitConnectionInfo',
|
|
function($q, CockpitKubeRequest, cockpitKubeDiscover,
|
|
CockpitEnvironment, loader, info) {
|
|
var promise = null;
|
|
return function kubeDiscoverSettings(force) {
|
|
if (!force && promise)
|
|
return promise;
|
|
|
|
var settings = {
|
|
registry: {
|
|
host: "registry",
|
|
host_explicit: false
|
|
},
|
|
flavor: "kubernetes",
|
|
isAdmin: false,
|
|
currentUser: null,
|
|
canChangeConnection: false
|
|
};
|
|
|
|
var env_p = CockpitEnvironment()
|
|
.then(function(result) {
|
|
var regHost = result["REGISTRY_HOST"];
|
|
if (regHost) {
|
|
settings.registry.host = regHost;
|
|
settings.registry.host_explicit = true;
|
|
}
|
|
var openshifthost = result["OPENSHIFT_OAUTH_PROVIDER_URL"];
|
|
if (openshifthost) {
|
|
settings.registry.openshifthost = openshifthost.replace(/^http(s?):\/\//i, "");
|
|
settings.registry.openshifthost_explicit = true;
|
|
}
|
|
}, function(ex) {});
|
|
|
|
var discover_p = cockpitKubeDiscover(force)
|
|
.then(function(options) {
|
|
var req = new CockpitKubeRequest("GET", "/oapi/v1/users/~", "", options)
|
|
.then(function(response) {
|
|
settings.flavor = "openshift";
|
|
if (response)
|
|
settings.currentUser = response.data;
|
|
}, function () {
|
|
settings.flavor = "kubernetes";
|
|
});
|
|
|
|
var watch = loader.watch("namespaces")
|
|
.then(function () {
|
|
settings.isAdmin = true;
|
|
}, function () {
|
|
settings.isAdmin = false;
|
|
});
|
|
|
|
var authorization;
|
|
/* See if we have a bearer token to use */
|
|
if (options.headers) {
|
|
authorization = (options.headers['Authorization'] || "").trim();
|
|
if (authorization.toLowerCase().indexOf("bearer ") === 0)
|
|
settings.registry.password = authorization.substr(7).trim();
|
|
}
|
|
return $q.all([watch, req]);
|
|
});
|
|
|
|
promise = $q.all([discover_p, env_p])
|
|
.then(function() {
|
|
settings.canChangeConnection = info.type == "kubectl";
|
|
return settings;
|
|
});
|
|
|
|
return promise;
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("cockpitBrowserStorage", [
|
|
"$window",
|
|
function($window) {
|
|
var base = $window;
|
|
if (cockpit && cockpit.sessionStorage && cockpit.localStorage)
|
|
base = cockpit;
|
|
|
|
/* Some browsers fail localStorage access due to corruption, preventing Cockpit login */
|
|
var localStorage;
|
|
try {
|
|
localStorage = base.localStorage;
|
|
} catch (ex) {
|
|
localStorage = base.sessionStorage;
|
|
console.warn(String(ex));
|
|
}
|
|
|
|
return {
|
|
sessionStorage: base.sessionStorage,
|
|
localStorage: localStorage
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory('cockpitConnectionInfo', function () {
|
|
return {
|
|
type: null,
|
|
kubeConfig: null,
|
|
};
|
|
})
|
|
|
|
.factory('cockpitContainerWebSocket', [
|
|
'CockpitKubeSocket',
|
|
'CockpitKubectlWebSocket',
|
|
'cockpitConnectionInfo',
|
|
function (socket, kubectlSocket, info) {
|
|
return function(url, protocols) {
|
|
/* config retrieved from kubectl? */
|
|
if (info.type == "kubectl")
|
|
return kubectlSocket(url, protocols);
|
|
else
|
|
return socket(url, { protocols: protocols });
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory('CockpitFormat', function() {
|
|
return {
|
|
formatBytes: cockpit.format_bytes,
|
|
formatBitsPerSec: cockpit.format_bits_per_sec,
|
|
format: cockpit.format
|
|
};
|
|
})
|
|
|
|
.factory('CockpitMetrics', function() {
|
|
return {
|
|
grid: cockpit.grid,
|
|
series: cockpit.series,
|
|
};
|
|
})
|
|
|
|
.factory('CockpitTranslate', function() {
|
|
return {
|
|
gettext: cockpit.gettext,
|
|
ngettext: cockpit.ngettext,
|
|
};
|
|
});
|
|
}());
|