559 lines
18 KiB
JavaScript
559 lines
18 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/>.
|
|
*/
|
|
|
|
var angular = require("angular");
|
|
|
|
(function() {
|
|
var kubeLast = 100;
|
|
|
|
var kubeData = { };
|
|
var handlers = [ ];
|
|
|
|
function addNotify(handler) {
|
|
handlers.push(handler);
|
|
}
|
|
|
|
function removeNotify(handler) {
|
|
var i;
|
|
var len = handlers.length;
|
|
for (i = 0; i < len; i++) {
|
|
if (handlers[i] === handler)
|
|
handlers[i] = null;
|
|
}
|
|
}
|
|
|
|
function dispatchNotify() {
|
|
var i;
|
|
var len = handlers.length;
|
|
for (i = 0; i < len; i++) {
|
|
if (handlers[i])
|
|
handlers[i].apply(kubeData, arguments);
|
|
}
|
|
}
|
|
|
|
function guid() {
|
|
function s4() {
|
|
return Math.floor((1 + Math.random()) * 0x10000)
|
|
.toString(16)
|
|
.substring(1);
|
|
}
|
|
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
|
s4() + '-' + s4() + s4() + s4();
|
|
}
|
|
|
|
function deparam(query) {
|
|
var parsed = { };
|
|
var vars = query.split("&");
|
|
for (var i = 0; i < vars.length; i++) {
|
|
var pair = vars[i].split("=");
|
|
var k = decodeURIComponent(pair[0]);
|
|
var v = decodeURIComponent(pair[1]);
|
|
if (typeof parsed[k] === "undefined" ||
|
|
typeof parsed[k] === "function") {
|
|
if (k.substr(k.length - 2) != '[]')
|
|
parsed[k] = v;
|
|
else
|
|
parsed[k] = [v];
|
|
} else if (typeof parsed[k] === "string") {
|
|
parsed[k] = v;
|
|
} else {
|
|
parsed[k].push(v);
|
|
}
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function kubeApiserver(req, defer) {
|
|
var path;
|
|
var query;
|
|
|
|
var pos = req.path.indexOf('?');
|
|
if (pos === -1) {
|
|
path = req.path;
|
|
query = { };
|
|
} else {
|
|
path = req.path.substring(0, pos);
|
|
query = deparam(req.path.substring(pos + 1));
|
|
}
|
|
|
|
var parts = path.substring(1).split("/");
|
|
|
|
/* The API check */
|
|
if (parts.length == 1 && (parts[0] == "api" || parts[0] != "oapi")) {
|
|
req.mockRespond(200, "OK", { }, JSON.stringify({
|
|
versions: [ "v1" ]
|
|
}));
|
|
}
|
|
|
|
if (parts[0] != "api" && parts[0] != "oapi" && parts[1] != "v1") {
|
|
req.mockRespond(404, "Not API");
|
|
return;
|
|
}
|
|
|
|
var baseUri = "/" + parts.slice(0, 2).join("/");
|
|
parts = parts.slice(2);
|
|
|
|
var ret = false;
|
|
if (req.method === "POST") {
|
|
ret = kubeApiPost(req, parts, query, baseUri);
|
|
} else if (req.method === "PUT") {
|
|
ret = kubeApiPut(req, parts, query, baseUri);
|
|
} else if (req.method === "GET") {
|
|
ret = kubeApiGet(req, parts, query, baseUri);
|
|
} else if (req.method === "DELETE") {
|
|
ret = kubeApiDelete(req, parts, query, baseUri);
|
|
} else if (req.method === "PATCH") {
|
|
ret = kubeApiPatch(req, parts, query, baseUri);
|
|
} else {
|
|
req.mockRespond(405, "Unsupported method");
|
|
ret = true;
|
|
}
|
|
|
|
if (!ret)
|
|
req.mockRespond(404, "Not found", { "Content-Type": "application/json" }, { code: 404, message: "Not found here" });
|
|
}
|
|
|
|
function kubeUpdate(key, object) {
|
|
var type;
|
|
if (!object) {
|
|
if (kubeData[key]) {
|
|
type = "DELETED";
|
|
object = kubeData[key];
|
|
delete kubeData[key];
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
if (kubeData[key])
|
|
type = "MODIFIED";
|
|
else
|
|
type = "ADDED";
|
|
kubeData[key] = object;
|
|
}
|
|
|
|
if (!object.metadata)
|
|
object.metadata = { };
|
|
if (!object.metadata.uid)
|
|
object.metadata.uid = guid();
|
|
object.metadata.resourceVersion = kubeLast;
|
|
kubeLast += 1;
|
|
|
|
dispatchNotify(type, key, object);
|
|
return object;
|
|
}
|
|
|
|
function kubeApiGet(req, parts, query, baseUri) {
|
|
var resourceVersion = null;
|
|
var namespaceRe = ".+";
|
|
|
|
/* Figure out if this is a watch */
|
|
var watch = false;
|
|
if (query.hasOwnProperty("watch")) {
|
|
watch = true;
|
|
if (query.resourceVersion) {
|
|
resourceVersion = parseInt(query.resourceVersion, 10);
|
|
if (isNaN(resourceVersion))
|
|
throw Error("invalid resourceVersion");
|
|
}
|
|
}
|
|
|
|
/* Figure out if namespace api was used */
|
|
var what = parts.shift();
|
|
if (what == "namespaces" && parts.length > 1) {
|
|
namespaceRe = parts.shift();
|
|
what = parts.shift();
|
|
}
|
|
|
|
var specific = parts.join("/");
|
|
var kind = null;
|
|
var regexp = null;
|
|
|
|
function prepare(key, object) {
|
|
if (resourceVersion) {
|
|
if (!object.metadata || !object.metadata.resourceVersion ||
|
|
object.metadata.resourceVersion < resourceVersion)
|
|
return null;
|
|
}
|
|
if (specific) {
|
|
if (key != specific)
|
|
return null;
|
|
}
|
|
if (regexp) {
|
|
if (!key.match(regexp))
|
|
return null;
|
|
}
|
|
|
|
var copy = JSON.parse(JSON.stringify(object));
|
|
copy.metadata.selfLink = baseUri + "/" + key;
|
|
copy.apiVersion = "v1";
|
|
return copy;
|
|
}
|
|
|
|
/* Various lists */
|
|
if (what == "namespaces") {
|
|
regexp = /namespaces\/[a-z0-9-_]+$/;
|
|
kind = "NamespaceList";
|
|
} else if (what == "nodes") {
|
|
regexp = /nodes\//;
|
|
kind = "NodeList";
|
|
} else if (what == "pods") {
|
|
regexp = RegExp("namespaces/" + namespaceRe + "/pods/");
|
|
kind = "PodList";
|
|
} else if (what == "services") {
|
|
regexp = RegExp("namespaces/" + namespaceRe + "/services/");
|
|
kind = "ServiceList";
|
|
} else if (what == "replicationcontrollers") {
|
|
regexp = RegExp("namespaces/" + namespaceRe + "/replicationcontrollers/");
|
|
kind = "ReplicationControllerList";
|
|
} else if (what == "events") {
|
|
regexp = RegExp("namespaces/" + namespaceRe + "/events/");
|
|
kind = "EventList";
|
|
} else if (what == "images") {
|
|
req.mockRespond(404, "OK", { "Content-Type": "text/plain; charset=utf-8" });
|
|
return;
|
|
} else if (what == "imagestreams") {
|
|
regexp = RegExp("namespaces/" + namespaceRe + "/imagestreams/");
|
|
kind = "ImageStreamList";
|
|
/* Nothing found */
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
function respondGet() {
|
|
var items = [ ];
|
|
var result = {
|
|
kind: kind,
|
|
creationTimestamp: null,
|
|
items: items
|
|
};
|
|
|
|
angular.forEach(kubeData, function(value, key) {
|
|
var object = prepare(key, value);
|
|
if (object)
|
|
items.push(object);
|
|
});
|
|
|
|
req.mockRespond(200, "OK", { "Content-Type": "application/json" }, JSON.stringify(result));
|
|
return true;
|
|
}
|
|
|
|
function respondWatch() {
|
|
req.mockRespond(200, "OK", { "Content-Type": "text/plain; charset=utf-8" }, null);
|
|
|
|
var body = "";
|
|
angular.forEach(kubeData, function(value, key) {
|
|
var object = prepare(key, value);
|
|
if (object)
|
|
body += JSON.stringify({ type: "ADDED", object: object }) + "\n";
|
|
});
|
|
|
|
function streamWatch(type, key, value) {
|
|
var object = prepare(key, value);
|
|
if (object)
|
|
req.mockData(JSON.stringify({ type: type, object: object }) + "\n", true);
|
|
}
|
|
|
|
addNotify(streamWatch);
|
|
req.mockData(body, true);
|
|
|
|
window.setTimeout(function() {
|
|
removeNotify(streamWatch);
|
|
req.mockData("", false);
|
|
}, 5000);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (watch)
|
|
return respondWatch();
|
|
else
|
|
return respondGet();
|
|
}
|
|
|
|
function kubeApiPost(req, parts, query, baseUri) {
|
|
var section;
|
|
|
|
if (parts.length === 3) {
|
|
if (parts[0] != "namespaces")
|
|
return false;
|
|
section = parts[2];
|
|
} else if (parts.length === 1) {
|
|
section = parts[0];
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
var object;
|
|
try {
|
|
object = JSON.parse(req.body);
|
|
} catch (ex) {
|
|
req.mockRespond(400, "Bad JSON");
|
|
return true;
|
|
}
|
|
|
|
var kind = object.kind;
|
|
var meta = object.metadata || { };
|
|
var name = meta.name;
|
|
|
|
if (!kind || !meta || !name) {
|
|
req.mockRespond(400, "Bad fields in JSON");
|
|
return true;
|
|
}
|
|
|
|
if (kind.toLowerCase() + "s" != section) {
|
|
req.mockRespond(400, "Bad section of URI");
|
|
return true;
|
|
}
|
|
|
|
parts.push(name);
|
|
var key = parts.join("/");
|
|
|
|
if (kubeData[key]) {
|
|
req.mockRespond(409, "Already exists", { "Content-Type": "application/json" },
|
|
JSON.stringify({ code: 409, message: "Already exists" }));
|
|
return true;
|
|
}
|
|
|
|
kubeUpdate(key, object);
|
|
req.mockRespond(200, "OK", { "Content-Type": "application/json" }, JSON.stringify(object));
|
|
return true;
|
|
}
|
|
|
|
function kubeApiPut(req, parts, query, baseUri) {
|
|
var object;
|
|
try {
|
|
object = JSON.parse(req.body);
|
|
} catch (ex) {
|
|
req.mockRespond(400, "Bad JSON");
|
|
return true;
|
|
}
|
|
|
|
var key = parts.join("/");
|
|
kubeUpdate(key, object);
|
|
req.mockRespond(200, "OK", { "Content-Type": "application/json" }, JSON.stringify(object));
|
|
return true;
|
|
}
|
|
|
|
function kubeApiDelete(req, parts, query, baseUri) {
|
|
if (parts.length === 4) {
|
|
if (parts[0] != "namespaces")
|
|
return false;
|
|
} else if (parts.length !== 2) {
|
|
return false;
|
|
}
|
|
|
|
var key = parts.join("/");
|
|
|
|
var resp = kubeUpdate(key, null);
|
|
req.mockRespond(200, "OK", { "Content-Type": "application/json" }, JSON.stringify(resp));
|
|
return true;
|
|
}
|
|
|
|
function kubeApiPatch(req, parts, query, baseUri) {
|
|
if (parts.length === 4) {
|
|
if (parts[0] != "namespaces")
|
|
return false;
|
|
} else if (parts.length !== 2) {
|
|
return false;
|
|
}
|
|
|
|
if (req.headers["Content-Type"] != "application/strategic-merge-patch+json") {
|
|
req.mockRespond(400, "Bad Content Type");
|
|
return true;
|
|
}
|
|
|
|
var key = parts.join("/");
|
|
var patch;
|
|
try {
|
|
patch = JSON.parse(req.body);
|
|
} catch (ex) {
|
|
req.mockRespond(400, "Bad JSON");
|
|
return true;
|
|
}
|
|
|
|
var data = angular.extend({ }, kubeData[key] || { }, patch);
|
|
var resp = kubeUpdate(key, data);
|
|
req.mockRespond(200, "OK", { "Content-Type": "application/json" }, JSON.stringify(resp));
|
|
return true;
|
|
}
|
|
|
|
var unique = 0;
|
|
|
|
angular.module("kubeClient.mock", [
|
|
"kubeClient",
|
|
])
|
|
|
|
.value("MockKubeData", {
|
|
load: function load(data) {
|
|
kubeData = JSON.parse(JSON.stringify(data));
|
|
},
|
|
update: kubeUpdate
|
|
})
|
|
|
|
.factory("MockKubeWatch", [
|
|
"$q",
|
|
"KUBE_SCHEMA",
|
|
"MockKubeRequest",
|
|
function($q, KUBE_SCHEMA, MockKubeRequest) {
|
|
return function CockpitKubeWatch(path, callback) {
|
|
var defer = $q.defer();
|
|
var promise = defer.promise;
|
|
|
|
unique += 1;
|
|
|
|
var request = new MockKubeRequest("GET", path + "?watch=true", "", {
|
|
streamer: handleStream,
|
|
unique: unique,
|
|
});
|
|
|
|
var buffer;
|
|
function handleStream(data, response) {
|
|
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];
|
|
|
|
/* Process all the others */
|
|
var frame;
|
|
var frames = [];
|
|
for (i = 0; i < length; i++) {
|
|
frame = JSON.parse(lines[i]);
|
|
if (!frame.object)
|
|
throw Error("invalid watch without object");
|
|
|
|
/* The watch failed, likely due to invalid resourceVersion */
|
|
if (frame.type == "ERROR")
|
|
throw frame;
|
|
|
|
frames.push(frame);
|
|
}
|
|
|
|
callback(frames);
|
|
|
|
var df = defer;
|
|
if (df) {
|
|
callback([]);
|
|
defer = null;
|
|
df.resolve(response);
|
|
}
|
|
}
|
|
|
|
request.then(function(response) {
|
|
var df = defer;
|
|
defer = null;
|
|
if (df)
|
|
df.resolve(response);
|
|
}, function(response) {
|
|
var df = defer;
|
|
defer = null;
|
|
if (df)
|
|
df.reject(response);
|
|
});
|
|
|
|
promise.cancel = function cancel() {
|
|
var df = defer;
|
|
if (request)
|
|
request.cancel();
|
|
if (df) {
|
|
defer = null;
|
|
df.reject({
|
|
status: 999,
|
|
statusText: "Cancelled",
|
|
problem: "cancelled",
|
|
});
|
|
}
|
|
};
|
|
return promise;
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory("MockKubeRequest", [
|
|
"$q",
|
|
function($q) {
|
|
return function MockKubeRequest(method, path, data, config) {
|
|
var req = angular.extend({ }, config, { method: method, path: path, body: data });
|
|
var defer = $q.defer();
|
|
var promise = defer.promise;
|
|
var response;
|
|
function finish() {
|
|
var df = defer;
|
|
defer = null;
|
|
if (response.headers["Content-Type"] == "application/json")
|
|
response.data = JSON.parse(response.data);
|
|
if (response.status < 300)
|
|
df.resolve(response);
|
|
else
|
|
df.reject(response);
|
|
}
|
|
|
|
req.mockRespond = function(status, reason, headers, body) {
|
|
if (!defer)
|
|
return;
|
|
response = {
|
|
status: status,
|
|
statusText: reason,
|
|
headers: headers || { },
|
|
data: "",
|
|
unique: req.unique,
|
|
};
|
|
if (body !== null)
|
|
req.mockData(body || "", false);
|
|
};
|
|
|
|
req.mockData = function(body, stream) {
|
|
if (!defer)
|
|
return;
|
|
if (typeof (body) !== "string")
|
|
body = JSON.stringify(body);
|
|
if (req.streamer)
|
|
req.streamer(body, response);
|
|
else
|
|
response.data += body;
|
|
if (!stream)
|
|
finish();
|
|
};
|
|
|
|
promise.cancel = function cancel() {
|
|
if (!defer)
|
|
return;
|
|
defer.reject({
|
|
status: 999,
|
|
statusText: "Cancelled",
|
|
problem: "cancelled",
|
|
});
|
|
defer = null;
|
|
};
|
|
|
|
window.setTimeout(function() {
|
|
kubeApiserver(req);
|
|
});
|
|
|
|
return promise;
|
|
};
|
|
}
|
|
]);
|
|
}());
|