cockpit/pkg/kubernetes/scripts/kube-client-mock.js

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