1582 lines
54 KiB
JavaScript
1582 lines
54 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() {
|
|
"use strict";
|
|
|
|
var angular = require('angular');
|
|
|
|
/*
|
|
* Some notes on the create fields.
|
|
*
|
|
* Namespaces should be created first, as they must exist before objects in
|
|
* them are created.
|
|
*
|
|
* Services should be created before pods (or replication controllers that
|
|
* make pods. This is because of the environment variables that pods get
|
|
* when they want to access a service.
|
|
*
|
|
* Create pods before replication controllers ... corner case, but keeps
|
|
* things sane.
|
|
*/
|
|
|
|
var KUBE = "/api/v1";
|
|
var OPENSHIFT = "/oapi/v1";
|
|
var KUBEVIRT = "/apis/kubevirt.io/v1alpha1";
|
|
var DEFAULT = { api: KUBE, create: 0 };
|
|
var SCHEMA = flatSchema([
|
|
{ kind: "DeploymentConfig", type: "deploymentconfigs", api: OPENSHIFT },
|
|
{ kind: "Endpoints", type: "endpoints", api: KUBE },
|
|
{ kind: "Group", type: "groups", api: OPENSHIFT, global: true },
|
|
{ kind: "Image", type: "images", api: OPENSHIFT, global: true },
|
|
{ kind: "ImageStream", type: "imagestreams", api: OPENSHIFT },
|
|
{ kind: "ImageStreamImage", type: "imagestreamimages", api: OPENSHIFT },
|
|
{ kind: "ImageStreamTag", type: "imagestreamtags", api: OPENSHIFT },
|
|
{ kind: "LocalResourceAccessReview", type: "localresourceaccessreviews", api: OPENSHIFT },
|
|
{ kind: "Namespace", type: "namespaces", api: KUBE, global: true, create: -100 },
|
|
{ kind: "Node", type: "nodes", api: KUBE, global: true },
|
|
{ kind: "Pod", type: "pods", api: KUBE, create: -20 },
|
|
{ kind: "PolicyBinding", type: "policybindings", api: OPENSHIFT },
|
|
{ kind: "RoleBinding", type: "rolebindings", api: OPENSHIFT },
|
|
{ kind: "Route", type: "routes", api: OPENSHIFT },
|
|
{ kind: "PersistentVolume", type: "persistentvolumes", api: KUBE, global: true, create: -100 },
|
|
{ kind: "PersistentVolumeClaim", type: "persistentvolumeclaims", api: KUBE, create: -50 },
|
|
{ kind: "Project", type: "projects", api: OPENSHIFT, global: true, create: -90 },
|
|
{ kind: "ProjectRequest", type: "projectrequests", api: OPENSHIFT, global: true, create: -90 },
|
|
{ kind: "ReplicationController", type: "replicationcontrollers", api: KUBE, create: -60 },
|
|
{ kind: "Service", type: "services", api: KUBE, create: -80 },
|
|
{ kind: "SubjectAccessReview", type: "subjectaccessreviews", api: OPENSHIFT },
|
|
{ kind: "User", type: "users", api: OPENSHIFT, global: true },
|
|
{ kind: "VirtualMachine", type: "virtualmachines", api: KUBEVIRT },
|
|
]);
|
|
|
|
var NAME_RE = /^[a-z0-9]([-a-z0-9_.]*[a-z0-9])?$/;
|
|
var USER_NAME_RE = /^[a-zA-Z0-9_.]([-a-zA-Z0-9 ,=@._]*[a-zA-Z0-9._])?$/;
|
|
|
|
/* Timeout for non-GET requests */
|
|
var REQ_TIMEOUT = "120s";
|
|
|
|
function debug() {
|
|
if (window.debugging == "all" || window.debugging == "kube")
|
|
console.debug.apply(console, arguments);
|
|
}
|
|
|
|
function hash(str) {
|
|
var h, i, chr, len;
|
|
if (str.length === 0)
|
|
return 0;
|
|
for (h = 0, i = 0, len = str.length; i < len; i++) {
|
|
chr = str.charCodeAt(i);
|
|
h = ((h << 5) - h) + chr;
|
|
h |= 0; // Convert to 32bit integer
|
|
}
|
|
return Math.abs(h);
|
|
}
|
|
|
|
function search(arr, val) {
|
|
var low = 0;
|
|
var high = arr.length - 1;
|
|
var mid, v;
|
|
|
|
while (low <= high) {
|
|
mid = (low + high) / 2 | 0;
|
|
v = arr[mid];
|
|
if (v < val)
|
|
low = mid + 1;
|
|
else if (v > val)
|
|
high = mid - 1;
|
|
else
|
|
return mid; /* key found */
|
|
}
|
|
return low;
|
|
}
|
|
|
|
/**
|
|
* HashIndex
|
|
*
|
|
* A probablisting hash index, where items are added with
|
|
* various keys, and probable matches are returned. Similar
|
|
* to bloom filters, false positives are possible, but never
|
|
* false negatives.
|
|
*/
|
|
function HashIndex(size) {
|
|
var self = this;
|
|
var array = [];
|
|
|
|
self.add = function add(keys, value) {
|
|
var i, j, p, x, length = keys.length;
|
|
for (j = 0; j < length; j++) {
|
|
i = hash("" + keys[j]) % size;
|
|
p = array[i];
|
|
if (p === undefined)
|
|
p = array[i] = [];
|
|
x = search(p, value);
|
|
if (p[x] != value)
|
|
p.splice(x, 0, value);
|
|
}
|
|
};
|
|
|
|
self.get = function get(key) {
|
|
var p = array[hash("" + key) % size];
|
|
if (!p)
|
|
return [];
|
|
return p.slice();
|
|
};
|
|
|
|
self.all = function all(keys) {
|
|
var i, j, p, result, n;
|
|
var rl, rv, pv, ri, px;
|
|
|
|
for (j = 0, n = keys.length; j < n; j++) {
|
|
i = hash("" + keys[j]) % size;
|
|
p = array[i];
|
|
|
|
/* No match for this key, short cut out */
|
|
if (!p) {
|
|
result = [];
|
|
break;
|
|
}
|
|
|
|
/* First key */
|
|
if (!result) {
|
|
result = p.slice();
|
|
|
|
/* Calculate intersection */
|
|
} else {
|
|
for (ri = 0, px = 0, rl = result.length; ri < rl; ) {
|
|
rv = result[ri];
|
|
pv = p[ri + px];
|
|
if (pv < rv) {
|
|
px += 1;
|
|
} else if (rv !== pv) {
|
|
result.splice(ri, 1);
|
|
rl -= 1;
|
|
} else {
|
|
ri += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result || [];
|
|
};
|
|
}
|
|
|
|
/*
|
|
* A WeakMap implementation
|
|
*
|
|
* This works on ES5 browsers, with the caveat that the mapped
|
|
* items are discoverable with enough work.
|
|
*
|
|
* To be clear, the principal use of a WeakMap is to associate
|
|
* an value with an object, the object is the key. And then have
|
|
* that value go away when the object does. This is very, very
|
|
* similar to properties.
|
|
*
|
|
* The main difference is that any assigned values are not
|
|
* garbage collected if the *weakmap* itself is collected,
|
|
* and of course one can actually access the non-enumerable
|
|
* property that makes this work.
|
|
*/
|
|
|
|
var weak_property = Math.random().toString(36).slice(2);
|
|
var local_seed = 1;
|
|
|
|
function SimpleWeakMap() {
|
|
var local_property = "weakmap" + local_seed;
|
|
local_seed += 1;
|
|
|
|
var self = this;
|
|
|
|
self.delete = function delete_(obj) {
|
|
var map = obj[weak_property];
|
|
if (map)
|
|
delete map[local_property];
|
|
};
|
|
|
|
self.has = function has(obj) {
|
|
var map = obj[weak_property];
|
|
return (map && local_property in map);
|
|
};
|
|
|
|
self.get = function has(obj) {
|
|
var map = obj[weak_property];
|
|
if (!map)
|
|
return undefined;
|
|
return map[local_property];
|
|
};
|
|
|
|
self.set = function set(obj, value) {
|
|
var map = obj[weak_property];
|
|
if (!map) {
|
|
map = function WeakMapData() { };
|
|
Object.defineProperty(obj, weak_property, {
|
|
enumerable: false, configurable: false,
|
|
writable: false, value: map,
|
|
});
|
|
}
|
|
|
|
map[local_property] = value;
|
|
};
|
|
}
|
|
|
|
function flatSchema(items) {
|
|
var i, len, ret = { "": DEFAULT };
|
|
for (i = 0, len = items.length; i < len; i++) {
|
|
ret[items[i].type] = items[i];
|
|
ret[items[i].kind] = items[i];
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Accepts:
|
|
* 1. an object
|
|
* 2. an involved object
|
|
* 2. a path string
|
|
* 3. type/kind, name, namespace
|
|
*/
|
|
function resourcePath(args) {
|
|
var one = args[0];
|
|
if (one && typeof one === "object") {
|
|
if (one.metadata) {
|
|
/* An object with a link */
|
|
if (one.metadata.selfLink)
|
|
return one.metadata.selfLink;
|
|
|
|
/* Pull out the arguments */
|
|
args = [ one.kind, one.metadata.name, one.metadata.namespace ];
|
|
} else if (one.name && one.kind) {
|
|
/* An involved object */
|
|
args = [ one.kind, one.name, one.namespace ];
|
|
}
|
|
|
|
|
|
/* Already a path */
|
|
} else if (one && one[0] == '/') {
|
|
return one;
|
|
}
|
|
|
|
/*
|
|
* Combine into a path.
|
|
*/
|
|
var schema = SCHEMA[args[0]] || SCHEMA[""];
|
|
var path = schema.api;
|
|
if (!schema.global && args[2])
|
|
path += "/namespaces/" + args[2];
|
|
path += "/" + schema.type;
|
|
if (args[1])
|
|
path += "/" + encodeURIComponent(args[1]);
|
|
|
|
return path;
|
|
}
|
|
|
|
/*
|
|
* Angular definitions start here
|
|
*/
|
|
|
|
angular.module("kubeClient", [])
|
|
|
|
/**
|
|
* KUBE_SCHEMA
|
|
*
|
|
* A dict of schema information. The keys are both object types
|
|
* and resource kinds. The values are objects with the following
|
|
* properties:
|
|
*
|
|
* schema.kind The object kind
|
|
* schema.type The resource type (ie: used in urls)
|
|
* schema.api The api endpoint to use
|
|
* schema.global Set to true if resource is not namespaced.
|
|
*/
|
|
|
|
.value("KUBE_SCHEMA", SCHEMA)
|
|
|
|
.constant("KubevirtPrefix", KUBEVIRT)
|
|
|
|
/**
|
|
* KUBE_NAME_RE
|
|
*
|
|
* Regular Expression that names in kubernetes must match.
|
|
*/
|
|
.value("KUBE_NAME_RE", NAME_RE)
|
|
|
|
/**
|
|
* kubeLoader
|
|
*
|
|
* Loads kubernetes objects either by watching them or loading
|
|
* objects explicitly. The loaded objects are available at
|
|
* the .objects property, although you probably want to
|
|
* use kubeSelect() to interact with these objects.
|
|
*
|
|
* loader.handle(objects, [removed])
|
|
*
|
|
* Tell the loader about a objects that has been loaded
|
|
* or removed elsewhere.
|
|
*
|
|
* loader.listen(callback, until)
|
|
*
|
|
* Register a callback to be invoked some time after new
|
|
* objects have been loaded. Returns an object with a
|
|
* .cancel() method, that can be used to stop listening.
|
|
*
|
|
* promise = loader.load(path)
|
|
* promise = loader.load(involvedObject)
|
|
* promise = loader.load(resource)
|
|
* promise = loader.load(kind, [name], [namespace])
|
|
*
|
|
* Load the resource at the path. Returns a promise that will
|
|
* resolve with the resource or an array of objects at the
|
|
* given path.
|
|
*
|
|
* loader.limits
|
|
*
|
|
* Contains various limits that govern what the loader loads
|
|
* from watches. Of note is loader.limits.namespace which is set
|
|
* to null for the loader to load all objects, or a namespace
|
|
* string or array of namespace strings for the loader to watch
|
|
* objects from specific namespaces.
|
|
*
|
|
* loader.limit(options)
|
|
*
|
|
* Adjust the loader limits that govern what the loader loads
|
|
* from watches. Options can contain a "namespace" field to
|
|
* set the namespace or namespaces to limit watching to.
|
|
*
|
|
* loader.reset()
|
|
*
|
|
* Clear out all loaded objects, and clear all watches. Also
|
|
* clears the limits and other state.
|
|
*
|
|
* loader.objects
|
|
*
|
|
* A dict of all loaded objects.
|
|
*
|
|
* promise = loader.watch(type, until)
|
|
*
|
|
* Start watching the given resource type. The returned promise
|
|
* will be resolved when an initial set of objects have been
|
|
* loaded for the watch, or rejected if the watch has failed.
|
|
*/
|
|
|
|
.factory("kubeLoader", [
|
|
"$q",
|
|
"$timeout",
|
|
"KubeWatch",
|
|
"KubeRequest",
|
|
"KUBE_SCHEMA",
|
|
function($q, $timeout, KubeWatch, KubeRequest, KUBE_SCHEMA) {
|
|
var self;
|
|
|
|
var callbacks = [];
|
|
var limits = { namespace: null };
|
|
|
|
/* All the current watches */
|
|
var watching = { };
|
|
|
|
/* All the loaded objects */
|
|
var objects = { };
|
|
|
|
/* Timeout batching */
|
|
var batch = null;
|
|
var batchTimeout = null;
|
|
|
|
function ensureWatch(what, namespace, increment) {
|
|
var schema = SCHEMA[what] || SCHEMA[""];
|
|
var watch, path = schema.api;
|
|
if (!schema.global && namespace)
|
|
path += "/namespaces/" + namespace;
|
|
path += "/" + schema.type;
|
|
|
|
if (!(path in watching)) {
|
|
watch = new KubeWatch(path, handleFrames);
|
|
watch.what = what;
|
|
watch.global = schema.global;
|
|
watch.namespace = namespace;
|
|
watch.cancelWatch = watch.cancel;
|
|
|
|
/* Replace the cancel function with one that does ref counting */
|
|
watch.cancel = function() {
|
|
var w = watching[path];
|
|
if (w) {
|
|
w.references -= 1;
|
|
if (w.references <= 0) {
|
|
w.cancelWatch();
|
|
delete watching[path];
|
|
}
|
|
}
|
|
};
|
|
watching[path] = watch;
|
|
}
|
|
|
|
/* Increase the references here */
|
|
watching[path].references += increment;
|
|
return watching[path];
|
|
}
|
|
|
|
function ensureWatches(what, increment) {
|
|
var namespace = limits.namespace;
|
|
if (!angular.isArray(namespace))
|
|
return ensureWatch(what, namespace, increment);
|
|
|
|
var parts = [];
|
|
angular.forEach(namespace, function(val) {
|
|
parts.push(ensureWatch(what, val, increment));
|
|
});
|
|
var ret = $q.all(parts);
|
|
ret.cancel = function() {
|
|
angular.forEach(parts, function(val) {
|
|
val.cancel();
|
|
});
|
|
};
|
|
return ret;
|
|
}
|
|
|
|
function handleFrames(frames) {
|
|
if (batch === null)
|
|
batch = frames;
|
|
else
|
|
batch.push.apply(batch, frames);
|
|
|
|
/* When called with empty data, flush, don't wait */
|
|
if (frames.length > 0) {
|
|
if (batchTimeout === null)
|
|
batchTimeout = window.setTimeout(handleTimeout, 150);
|
|
else
|
|
return; /* called again later */
|
|
}
|
|
|
|
handleFlush(invokeCallbacks);
|
|
}
|
|
|
|
function resourceVersion(resource) {
|
|
var version;
|
|
if (resource && resource.metadata)
|
|
version = parseInt(resource.metadata.resourceVersion, 10);
|
|
|
|
if (!isNaN(version))
|
|
return version;
|
|
}
|
|
|
|
function handleFlush(invoke) {
|
|
var drain = batch;
|
|
batch = null;
|
|
|
|
if (!drain)
|
|
return;
|
|
|
|
var present = { };
|
|
var removed = { };
|
|
var i, len, link, resource;
|
|
var cVersion, lVersion;
|
|
for (i = 0, len = drain.length; i < len; i++) {
|
|
resource = drain[i].object;
|
|
if (resource) {
|
|
link = decodeURIComponent(resourcePath([resource]));
|
|
if (drain[i].type == "DELETED") {
|
|
delete objects[link];
|
|
delete present[link];
|
|
removed[link] = resource;
|
|
} else if (drain[i].checkResourceVersion) {
|
|
/* There is a race between items loaded from
|
|
* watchers and items loaded other ways such as
|
|
* from KubeMethods callbacks, where we might
|
|
* end up saving the older item if loader.load is
|
|
* called after the watcher has already loaded fresher
|
|
* data. Look at the resourceVersion and only add
|
|
* if it is the same or newer than what we already have.
|
|
*/
|
|
cVersion = resourceVersion(resource);
|
|
lVersion = resourceVersion(objects[link]);
|
|
if (!cVersion || !lVersion || cVersion >= lVersion) {
|
|
present[link] = resource;
|
|
objects[link] = resource;
|
|
}
|
|
} else {
|
|
present[link] = resource;
|
|
objects[link] = resource;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Run all the listeners and then digest */
|
|
invoke(present, removed);
|
|
}
|
|
|
|
function invokeCallbacks(/* ... */) {
|
|
var i, len, func;
|
|
for (i = 0, len = callbacks.length; i < len; i++) {
|
|
func = callbacks[i];
|
|
if (func)
|
|
func.apply(self, arguments);
|
|
}
|
|
}
|
|
|
|
function handleTimeout() {
|
|
batchTimeout = null;
|
|
handleFlush(invokeCallbacks);
|
|
}
|
|
|
|
function resetLoader() {
|
|
var link;
|
|
|
|
/* We drop any batched objects in flight */
|
|
window.clearTimeout(batchTimeout);
|
|
batchTimeout = null;
|
|
batch = null;
|
|
|
|
/* Cancel all the watches */
|
|
var old = watching;
|
|
watching = { };
|
|
angular.forEach(old, function(w) {
|
|
w.cancelWatch();
|
|
});
|
|
|
|
/* Clear out everything */
|
|
for (link in objects)
|
|
delete objects[link];
|
|
|
|
for (link in limits)
|
|
delete limits[link];
|
|
limits.namespace = null;
|
|
|
|
/* Tell the callbacks we're resetting */
|
|
invokeCallbacks();
|
|
}
|
|
|
|
function handleObjects(objects, removed, kind) {
|
|
handleFrames(objects.map(function(resource) {
|
|
if (kind)
|
|
resource.kind = kind;
|
|
|
|
return {
|
|
type: removed ? "DELETED" : "ADDED",
|
|
object: resource,
|
|
checkResourceVersion: true
|
|
};
|
|
}));
|
|
handleFlush(invokeCallbacks);
|
|
}
|
|
|
|
function loadObjects(/* ... */) {
|
|
var path = resourcePath(arguments);
|
|
var req = new KubeRequest("GET", path);
|
|
var promise = req.then(function(response) {
|
|
req = null;
|
|
var resource = response.data;
|
|
if (!resource || !resource.kind) {
|
|
return null;
|
|
} else if (resource.kind.indexOf("List") === resource.kind.length - 4) {
|
|
handleObjects(resource.items, false, resource.kind.slice(0, -4));
|
|
return resource.items;
|
|
} else {
|
|
handleObjects([resource]);
|
|
return resource;
|
|
}
|
|
}, function(response) {
|
|
req = null;
|
|
var resp = response.data;
|
|
return $q.reject(resp || response);
|
|
});
|
|
promise.cancel = function cancel(ex) {
|
|
req.cancel(ex);
|
|
};
|
|
return promise;
|
|
}
|
|
|
|
function adjustNamespace(value) {
|
|
window.clearTimeout(batchTimeout);
|
|
batchTimeout = null;
|
|
|
|
/* Convert this to our native format */
|
|
var only = { };
|
|
if (value === null) {
|
|
only = null;
|
|
} else if (angular.isArray(value)) {
|
|
angular.forEach(value, function(namespace) {
|
|
only[namespace] = true;
|
|
});
|
|
} else {
|
|
only[value] = true;
|
|
}
|
|
limits.namespace = value;
|
|
|
|
/* Flush everything that's outstanding */
|
|
var present = { }, removed = { };
|
|
handleFlush(function(a, b) {
|
|
present = a;
|
|
removed = b;
|
|
});
|
|
|
|
/* Remove objects that are not in these namespaces */
|
|
var meta, link;
|
|
for (link in objects) {
|
|
meta = objects[link].metadata;
|
|
if (only && meta.namespace && !(meta.namespace in only)) {
|
|
removed[link] = objects[link];
|
|
delete objects[link];
|
|
delete present[link];
|
|
}
|
|
}
|
|
|
|
/* Cancel any watches not applicable to these namespaces */
|
|
var path, w, reconnect = [ ];
|
|
for (path in watching) {
|
|
w = watching[path];
|
|
if ((!only && w.namespace) || (only && !w.global && !(w.namespace in only))) {
|
|
w.cancelWatch();
|
|
delete watching[path];
|
|
reconnect.push(w);
|
|
}
|
|
}
|
|
|
|
/* Tell the world what we did */
|
|
invokeCallbacks(present, removed);
|
|
|
|
/* Reconnect all the watches we cancelled with proper namespace */
|
|
angular.forEach(reconnect, function(w) {
|
|
ensureWatches(w.what, w.references);
|
|
});
|
|
}
|
|
|
|
function connectUntil(ret, until) {
|
|
if (until) {
|
|
if (until.$on) {
|
|
until.$on("$destroy", function() {
|
|
ret.cancel();
|
|
});
|
|
} else {
|
|
console.warn("invalid until passed to watch", until);
|
|
}
|
|
}
|
|
}
|
|
|
|
self = {
|
|
watch: function watch(what, until) {
|
|
var ret = ensureWatches(what, 1);
|
|
connectUntil(ret, until);
|
|
return ret;
|
|
},
|
|
load: function load(/* ... */) {
|
|
return loadObjects.apply(this, arguments);
|
|
},
|
|
limit: function limit(options) {
|
|
if ("namespace" in options)
|
|
adjustNamespace(options.namespace);
|
|
},
|
|
reset: resetLoader,
|
|
listen: function listen(callback, until) {
|
|
if (callback.early)
|
|
callbacks.unshift(callback);
|
|
else
|
|
callbacks.push(callback);
|
|
var timeout = $timeout(function() {
|
|
timeout = null;
|
|
callback.call(self, objects);
|
|
}, 0);
|
|
var ret = {
|
|
cancel: function() {
|
|
var i, len;
|
|
$timeout.cancel(timeout);
|
|
timeout = null;
|
|
for (i = 0, len = callbacks.length; i < len; i++) {
|
|
if (callbacks[i] === callback)
|
|
callbacks[i] = null;
|
|
}
|
|
}
|
|
};
|
|
connectUntil(ret, until);
|
|
return ret;
|
|
},
|
|
handle: function handle(objects, removed, kind) {
|
|
if (!angular.isArray(objects))
|
|
objects = [ objects ];
|
|
handleObjects(objects, removed, kind);
|
|
},
|
|
resolve: function resolve(/* ... */) {
|
|
return resourcePath(arguments);
|
|
},
|
|
objects: objects,
|
|
limits: limits,
|
|
};
|
|
|
|
return self;
|
|
}
|
|
])
|
|
|
|
/**
|
|
* kubeSelect
|
|
*
|
|
* Allows selecting loaded objects based on various criteria. The
|
|
* goal here is to allow selection to be fast enough that it can be
|
|
* done repeatedly and regularly, without keeping caches of objects
|
|
* all over the place.
|
|
*
|
|
* Resources may be filtered in a chain by calling various filter
|
|
* functions. Lets start with an example that finds a pod:
|
|
*
|
|
* pod = kubeSelect()
|
|
* .kind("Pod")
|
|
* .namespace("default")
|
|
* .name("docker-registry")
|
|
* .one();
|
|
*
|
|
* Calling kubeSelect() will return a dict with all loaded objects,
|
|
* containing unique keys, and then various filters can be called to
|
|
* further narrow results.
|
|
*
|
|
* You can also pass a dict of objects into kubeSelect() and then
|
|
* perform actions on it.
|
|
*
|
|
* The following filters are available by default:
|
|
*
|
|
* .kind(kind) Limit to specified kind
|
|
* .namespace(ns) Limit to specified namespace
|
|
* .name(name) Limit to this name
|
|
* .host(name) Limit to this host
|
|
* .label(selector) Limit to objects whose label match selector
|
|
* .one() Choose one of results, or null
|
|
* .extend(obj) Extend obj with the results
|
|
*
|
|
* Additional filters can be registered by calling the function:
|
|
*
|
|
* kubeSelect.register(name, function)
|
|
* kubeSelect.register(filterobj)
|
|
*
|
|
* Ask on FreeNode #cockpit for documentation on filters.
|
|
*/
|
|
|
|
.factory("kubeSelect", [
|
|
"kubeLoader",
|
|
function(loader) {
|
|
/* A list of all registered filters */
|
|
var filters = { };
|
|
|
|
/* A hash index */
|
|
var index = null;
|
|
|
|
/* The filter prototype for functions available on selector */
|
|
var proto = null;
|
|
|
|
/* Cache data */
|
|
var weakmap = new SimpleWeakMap();
|
|
var version = 1;
|
|
|
|
function listener(present, removed) {
|
|
version += 1;
|
|
|
|
/* Get called like this when reset */
|
|
if (!present) {
|
|
index = null;
|
|
|
|
/* Called like this when more objects arrive */
|
|
} else if (index) {
|
|
indexObjects(present);
|
|
}
|
|
}
|
|
|
|
listener.early = true;
|
|
loader.listen(listener);
|
|
|
|
/* Create a new index and populate */
|
|
function indexCreate() {
|
|
/* TODO: Derive this value from cluster size */
|
|
index = new HashIndex(262139);
|
|
|
|
/* And index all the objects */
|
|
indexObjects(loader.objects);
|
|
}
|
|
|
|
/* Populate index for the given objects and current filters */
|
|
function indexObjects(objects) {
|
|
var link, object, name, key, keys, filter;
|
|
for (link in objects) {
|
|
object = objects[link];
|
|
for (name in filters) {
|
|
filter = filters[name];
|
|
if (filter.digest) {
|
|
key = filter.digest.call(null, object);
|
|
if (key)
|
|
index.add([ key ], link);
|
|
} else if (filter.digests) {
|
|
keys = filter.digests.call(null, object);
|
|
if (keys.length)
|
|
index.add(keys, link);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Return a place to cache data related to obj */
|
|
function cached(obj) {
|
|
var data = weakmap.get(obj);
|
|
if (!data || data.version !== version) {
|
|
data = { version: version, length: data ? data.length : undefined };
|
|
weakmap.set(obj, data);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function makePrototypeCall(filter) {
|
|
return function() {
|
|
var cache = cached(this);
|
|
|
|
/*
|
|
* Do this early, since some browsers cannot pass
|
|
* arguments to JSON.stringify()
|
|
*/
|
|
var args = Array.prototype.slice.call(arguments);
|
|
|
|
/* Fast path, already calculated results */
|
|
var desc = filter.name + ": " + JSON.stringify(args);
|
|
if (desc in cache)
|
|
return cache[desc];
|
|
|
|
var results;
|
|
if (filter.filter) {
|
|
results = filter.filter.apply(this, args);
|
|
|
|
} else {
|
|
if (!index)
|
|
indexCreate();
|
|
if (!cache.indexed) {
|
|
indexObjects(this);
|
|
cache.indexed = true;
|
|
}
|
|
if (filter.digests) {
|
|
results = digestsFilter(filter, this, args);
|
|
} else if (filter.digest) {
|
|
results = digestFilter(filter, this, args);
|
|
} else {
|
|
console.warn("invalid filter: " + filter.name);
|
|
results = { };
|
|
}
|
|
}
|
|
|
|
cache[desc] = results;
|
|
return results;
|
|
};
|
|
}
|
|
|
|
function makePrototype() {
|
|
var name, ret = {
|
|
length: {
|
|
enumerable: false,
|
|
configurable: true,
|
|
get: function() { return cached(this).length; }
|
|
}
|
|
};
|
|
for (name in filters) {
|
|
ret[name] = {
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: makePrototypeCall(filters[name])
|
|
};
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
function mixinSelection(results, length, indexed) {
|
|
var link, data;
|
|
if (length === undefined) {
|
|
length = 0;
|
|
for (link in results)
|
|
length += 1;
|
|
}
|
|
proto = proto || makePrototype();
|
|
Object.defineProperties(results, proto);
|
|
data = cached(results);
|
|
data.length = length;
|
|
data.selection = results;
|
|
data.indexed = indexed;
|
|
return results;
|
|
}
|
|
|
|
function digestFilter(filter, what, criteria) {
|
|
var p, pl, key, possible, link, object;
|
|
var results = { }, count = 0;
|
|
|
|
key = filter.digest.apply(null, criteria);
|
|
if (key !== null && key !== undefined) {
|
|
possible = index.get(key);
|
|
} else {
|
|
possible = [];
|
|
}
|
|
|
|
for (p = 0, pl = possible.length; p < pl; p++) {
|
|
link = possible[p];
|
|
object = what[link];
|
|
if (object) {
|
|
if (key === filter.digest.call(null, object)) {
|
|
results[link] = object;
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return mixinSelection(results, count, true);
|
|
}
|
|
|
|
function digestsFilter(filter, what, criteria) {
|
|
var keys, keyn, keyo, k, link, match, object, possible;
|
|
var p, pl, j, jl;
|
|
var results = { }, count = 0;
|
|
|
|
keys = filter.digests.apply(null, criteria);
|
|
keyn = keys.length;
|
|
if (keyn > 0) {
|
|
possible = index.all(keys);
|
|
keys.sort();
|
|
} else {
|
|
possible = [];
|
|
}
|
|
|
|
for (p = 0, pl = possible.length; p < pl; p++) {
|
|
link = possible[p];
|
|
object = what[link];
|
|
if (object) {
|
|
keyo = filter.digests.call(null, object);
|
|
keyo.sort();
|
|
match = false;
|
|
|
|
/* Search for first key */
|
|
for (j = 0, jl = keyo.length; !match && j < jl; j++) {
|
|
if (keys[0] === keyo[j]) {
|
|
match = true;
|
|
for (k = 0; match && k < keyn; k++) {
|
|
if (keys[k] !== keyo[j + k])
|
|
match = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (match) {
|
|
results[link] = object;
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
return mixinSelection(results, count, true);
|
|
}
|
|
|
|
function registerFilter(filter, optional) {
|
|
if (typeof (optional) == "function") {
|
|
filter = {
|
|
name: filter,
|
|
filter: optional,
|
|
};
|
|
}
|
|
|
|
filters[filter.name] = filter;
|
|
index = null;
|
|
proto = null;
|
|
version += 1;
|
|
}
|
|
|
|
/* The one filter */
|
|
registerFilter("one", function() {
|
|
var link;
|
|
for (link in this)
|
|
return this[link];
|
|
return null;
|
|
});
|
|
|
|
/* The extend filter */
|
|
registerFilter("extend", function(target) {
|
|
var link;
|
|
for (link in this)
|
|
target[link] = this[link];
|
|
return target;
|
|
});
|
|
|
|
/* The label filter */
|
|
registerFilter({
|
|
name: "label",
|
|
digests: function(arg) {
|
|
var ret = [];
|
|
if (!arg)
|
|
return ret;
|
|
var i, meta = arg.metadata;
|
|
var labels = meta ? meta.labels : arg;
|
|
for (i in labels || [])
|
|
ret.push(i + "=" + labels[i]);
|
|
return ret;
|
|
}
|
|
});
|
|
|
|
/* The namespace filter */
|
|
registerFilter({
|
|
name: "namespace",
|
|
digest: function(arg) {
|
|
if (!arg)
|
|
return null;
|
|
if (typeof arg === "string")
|
|
return arg;
|
|
var meta = arg.metadata;
|
|
return meta ? meta.namespace : null;
|
|
}
|
|
});
|
|
|
|
/* The name filter */
|
|
registerFilter({
|
|
name: "name",
|
|
digest: function(arg) {
|
|
if (!arg)
|
|
return null;
|
|
if (typeof arg === "string")
|
|
return arg;
|
|
var meta = arg.metadata;
|
|
return meta ? meta.name : null;
|
|
}
|
|
});
|
|
|
|
/* The kind filter */
|
|
registerFilter({
|
|
name: "kind",
|
|
digest: function(arg) {
|
|
if (!arg)
|
|
return null;
|
|
if (typeof arg === "string")
|
|
return arg;
|
|
return arg.kind;
|
|
}
|
|
});
|
|
|
|
/* The host filter */
|
|
registerFilter({
|
|
name: "host",
|
|
digest: function(arg) {
|
|
if (!arg)
|
|
return null;
|
|
if (typeof arg === "string")
|
|
return arg;
|
|
var spec = arg.spec;
|
|
return spec ? spec.nodeName : null;
|
|
}
|
|
});
|
|
|
|
/* The namespace filter */
|
|
registerFilter({
|
|
name: "uid",
|
|
digest: function(arg) {
|
|
if (!arg)
|
|
return null;
|
|
if (typeof arg === "string")
|
|
return arg;
|
|
var meta = arg.metadata;
|
|
return meta ? meta.uid : null;
|
|
}
|
|
});
|
|
|
|
/* The statusPhase filter */
|
|
registerFilter({
|
|
name: "statusPhase",
|
|
digest: function(arg) {
|
|
var status;
|
|
if (typeof arg == "string") {
|
|
return arg;
|
|
} else {
|
|
status = arg.status || { };
|
|
return status.phase ? status.phase : null;
|
|
}
|
|
}
|
|
});
|
|
|
|
var empty = { };
|
|
|
|
function select(arg) {
|
|
var cache, indexed = false;
|
|
if (arg === undefined) {
|
|
arg = loader.objects;
|
|
indexed = true;
|
|
} else if (!arg) {
|
|
arg = empty;
|
|
}
|
|
|
|
/* Next the specific object */
|
|
if (typeof arg !== "object") {
|
|
console.warn("Pass resources or resource dicts or null to kubeSelect()");
|
|
arg = empty;
|
|
}
|
|
|
|
cache = cached(arg);
|
|
if (cache.selection)
|
|
return cache.selection;
|
|
|
|
/* A single resource object */
|
|
var meta, single;
|
|
if (typeof arg.kind === "string") {
|
|
if (!cache.single) {
|
|
meta = arg.meta || { };
|
|
single = { };
|
|
single[meta.selfLink || 1] = arg;
|
|
cache.single = mixinSelection(single, undefined, false);
|
|
}
|
|
return cache.single;
|
|
}
|
|
|
|
return mixinSelection(arg, undefined, indexed);
|
|
}
|
|
|
|
/* A seldom used 'static' method */
|
|
select.register = registerFilter;
|
|
|
|
return select;
|
|
}
|
|
])
|
|
|
|
/**
|
|
* kubeMethods
|
|
*
|
|
* Methods that operate on kubernetes objects.
|
|
*
|
|
* promise = methods.create(objects, namespace)
|
|
*
|
|
* Create the given resource or objects in the specified namespace.
|
|
*
|
|
* promise = methods.remove(resource)
|
|
* promise = methods.remove(path)
|
|
* promise = methods.remove(type, name, namespace)
|
|
*
|
|
* Delete the given resource from kubernetes.
|
|
*/
|
|
.factory("kubeMethods", [
|
|
"$q",
|
|
"KUBE_SCHEMA",
|
|
"KubeRequest",
|
|
"kubeLoader",
|
|
function($q, KUBE_SCHEMA, KubeRequest, loader) {
|
|
function createCompare(a, b) {
|
|
var sa = KUBE_SCHEMA[a.kind].create || 0;
|
|
var sb = KUBE_SCHEMA[b.kind].create || 0;
|
|
return sa - sb;
|
|
}
|
|
|
|
function createObjects(objects, namespace) {
|
|
var defer = $q.defer();
|
|
var promise = defer.promise;
|
|
var request = null;
|
|
|
|
if (!angular.isArray(objects)) {
|
|
if (objects.kind == "List")
|
|
objects = objects.items;
|
|
else
|
|
objects = [ objects ];
|
|
}
|
|
|
|
var haveNs = false;
|
|
var wantNs = false;
|
|
|
|
objects.forEach(function(resource) {
|
|
var meta = resource.metadata || { };
|
|
if ((resource.kind == "Namespace" || resource.kind == "Project") && meta.name === namespace)
|
|
haveNs = true;
|
|
var schema = SCHEMA[resource.kind] || SCHEMA[""];
|
|
if (!schema.global)
|
|
wantNs = true;
|
|
});
|
|
|
|
/* Shallow copy of the array, we modify it below */
|
|
objects = objects.slice();
|
|
|
|
/* Create the namespace */
|
|
if (namespace && wantNs && !haveNs) {
|
|
objects.unshift({
|
|
apiVersion : "v1",
|
|
kind : "Namespace",
|
|
metadata : { name: namespace }
|
|
});
|
|
}
|
|
|
|
/* Now sort the array with create preference */
|
|
objects.sort(createCompare);
|
|
|
|
function step() {
|
|
var resource = objects.shift();
|
|
if (!resource) {
|
|
defer.resolve();
|
|
return;
|
|
}
|
|
|
|
var path = resourcePath([resource.kind, null, namespace || "default"]);
|
|
path += "?timeout=" + REQ_TIMEOUT;
|
|
|
|
request = new KubeRequest("POST", path, JSON.stringify(resource))
|
|
.then(function(response) {
|
|
var meta;
|
|
|
|
debug("created resource:", path, response.data);
|
|
if (response.data.kind) {
|
|
/* HACK: https://github.com/openshift/origin/issues/8167 */
|
|
if (response.data.kind == "Project") {
|
|
meta = response.data.metadata || { };
|
|
delete meta.selfLink;
|
|
}
|
|
loader.handle(response.data);
|
|
}
|
|
step();
|
|
}, function(response) {
|
|
var resp = response.data;
|
|
var code = response.status;
|
|
if (resp && resp.code)
|
|
code = resp.code;
|
|
|
|
/* Ignore failures creating the namespace if it already exists */
|
|
if (resource.kind == "Namespace" && (code === 409 || code === 403)) {
|
|
debug("skipping namespace creation");
|
|
step();
|
|
} else {
|
|
debug("create failed:", path, resp || response);
|
|
defer.reject(resp || response);
|
|
}
|
|
});
|
|
}
|
|
|
|
step();
|
|
|
|
promise.cancel = function cancel() {
|
|
if (request)
|
|
request.cancel();
|
|
};
|
|
return promise;
|
|
}
|
|
|
|
function deleteResource(/* ... */) {
|
|
var path = resourcePath(arguments);
|
|
var resource = loader.objects[path];
|
|
path += "?timeout=" + REQ_TIMEOUT;
|
|
var promise = new KubeRequest("DELETE", path);
|
|
return promise.then(function() {
|
|
debug("deleted resource:", path, resource);
|
|
if (resource)
|
|
loader.handle(resource, true);
|
|
}, function(response) {
|
|
var resp = response.data;
|
|
return $q.reject(resp || response);
|
|
});
|
|
}
|
|
|
|
function patchResource(resource, patch) {
|
|
var path = resourcePath([resource]);
|
|
path += "?timeout=" + REQ_TIMEOUT;
|
|
var body = JSON.stringify(patch);
|
|
var config = { headers: { "Content-Type": "application/strategic-merge-patch+json" } };
|
|
var promise = new KubeRequest("PATCH", path, body, config);
|
|
return promise.then(function(response) {
|
|
debug("patched resource:", path, response.data);
|
|
if (response.data.kind)
|
|
loader.handle(response.data);
|
|
}, function(response) {
|
|
var resp = response.data;
|
|
return $q.reject(resp || response);
|
|
});
|
|
}
|
|
|
|
function generalMethodRequest(method, resource, body, config) {
|
|
var path = resourcePath([resource]);
|
|
if (method != "GET")
|
|
path += "?timeout=" + REQ_TIMEOUT;
|
|
var promise = new KubeRequest(method, path, JSON.stringify(body), config);
|
|
return promise.then(function(response) {
|
|
var resp = response.data;
|
|
return resp || response;
|
|
}, function(response) {
|
|
var resp = response.data;
|
|
return $q.reject(resp || response);
|
|
});
|
|
}
|
|
|
|
function putResource(resource, body, config) {
|
|
return generalMethodRequest("PUT", resource, body, config);
|
|
}
|
|
|
|
function postResource(resource, body, config) {
|
|
return generalMethodRequest("POST", resource, body, config);
|
|
}
|
|
|
|
function checkResource(resource, targets) {
|
|
var defer = $q.defer();
|
|
var ex, exs = [];
|
|
|
|
if (!targets)
|
|
targets = { };
|
|
|
|
/* Some simple metadata checks */
|
|
var meta = resource.metadata;
|
|
if (meta) {
|
|
ex = null;
|
|
if (meta.name !== undefined) {
|
|
var check_re = (resource.kind == "User") ? USER_NAME_RE : NAME_RE;
|
|
if (!meta.name)
|
|
ex = new Error("The name cannot be empty");
|
|
else if (!check_re.test(meta.name))
|
|
if (check_re == NAME_RE) {
|
|
ex = new Error("The name contains invalid characters. Only letters, numbers and dashes are allowed");
|
|
} else {
|
|
ex = new Error("The name contains invalid characters. Only letters, numbers, spaces and the following symbols are allowed: , = @ . _");
|
|
}
|
|
}
|
|
if (ex) {
|
|
ex.target = targets["metadata.name"];
|
|
exs.push(ex);
|
|
}
|
|
|
|
ex = null;
|
|
if (meta.namespace !== undefined) {
|
|
if (!meta.namespace)
|
|
ex = new Error("The namespace cannot be empty");
|
|
else if (!NAME_RE.test(meta.namespace))
|
|
ex = new Error("The name contains invalid characters. Only letters, numbers and dashes are allowed");
|
|
}
|
|
if (ex) {
|
|
ex.target = targets["metadata.namespace"];
|
|
exs.push(ex);
|
|
}
|
|
}
|
|
|
|
if (exs.length)
|
|
defer.reject(exs);
|
|
else
|
|
defer.resolve();
|
|
return defer.promise;
|
|
}
|
|
|
|
return {
|
|
"create": createObjects,
|
|
"delete": deleteResource,
|
|
"check": checkResource,
|
|
"patch": patchResource,
|
|
post: postResource,
|
|
put: putResource,
|
|
};
|
|
}
|
|
])
|
|
|
|
/**
|
|
* KubeRequest
|
|
*
|
|
* Create a new low level kubernetes request. These are instantiated
|
|
* by kubeLoader or kubeMethods, and typically not used directly.
|
|
*
|
|
* An implementation of KubeRequest must be provided. It has the
|
|
* following characteristics.
|
|
*
|
|
* promise = KubeRequest(method, path, [body, [config]])
|
|
*
|
|
* Creates a new request, for the given HTTP method and path. If body
|
|
* is present it will be sent as the request body. If it an object or
|
|
* array it will be encoded as JSON before being sent.
|
|
*
|
|
* If present the config object may include the following properties:
|
|
*
|
|
* headers An dict of headers to include
|
|
*
|
|
* In addition the config object can include implementation specific
|
|
* settings or data.
|
|
*
|
|
* If successful the promise will resolve with a response object that
|
|
* includes the following:
|
|
*
|
|
* status Status code
|
|
* statusText Status reason or message
|
|
* data Response body, JSON decoded if response is json
|
|
* headers Response headers
|
|
*
|
|
* Implementation specific fields may also be present
|
|
*/
|
|
|
|
.provider("KubeRequest", [
|
|
function() {
|
|
var self = this;
|
|
|
|
/* Until we come up with a good default implementation, must be provided */
|
|
self.KubeRequestFactory = "MissingKubeRequest";
|
|
|
|
function load(injector, name) {
|
|
if (angular.isString(name))
|
|
return injector.get(name, "KubeRequest");
|
|
else
|
|
return injector.invoke(name);
|
|
}
|
|
|
|
self.$get = [
|
|
"$injector",
|
|
function($injector) {
|
|
return load($injector, self.KubeRequestFactory);
|
|
}
|
|
];
|
|
}
|
|
])
|
|
|
|
.factory("MissingKubeRequest", [
|
|
function() {
|
|
return function MissingKubeRequest(path, callback) {
|
|
throw new Error("no KubeRequestFactory set");
|
|
};
|
|
}
|
|
])
|
|
|
|
/**
|
|
* KubeSocket
|
|
*
|
|
* Create a new low level kubernetes websocket request
|
|
*
|
|
* An implementation of KubeSocket must be provided. It has the
|
|
* following characteristics.
|
|
*
|
|
* ws = KubeSocket(path, [config])
|
|
*
|
|
* Creates a new websocket request, for the given path.
|
|
*
|
|
* headers An dict of headers to include
|
|
* protocals An list or string of websocket protocols
|
|
*
|
|
* In addition the config object can include implementation specific
|
|
* settings or data.
|
|
*
|
|
* A object is returned that implements the Web API
|
|
* Websocket interface. Specifically it should
|
|
* expose a 'readyState' attribute, provide
|
|
* open and close functions, and emit open, close and
|
|
* message events.
|
|
*
|
|
* Implementation specific fields may also be present
|
|
*/
|
|
|
|
.provider("KubeSocket", [
|
|
function() {
|
|
var self = this;
|
|
|
|
/* Until we come up with a good default implementation, must be provided */
|
|
self.KubeSocketFactory = "MissingKubeSocket";
|
|
|
|
function load(injector, name) {
|
|
if (angular.isString(name))
|
|
return injector.get(name, "KubeSocket");
|
|
else
|
|
return injector.invoke(name);
|
|
}
|
|
|
|
self.$get = [
|
|
"$injector",
|
|
function($injector) {
|
|
return load($injector, self.KubeSocketFactory);
|
|
}
|
|
];
|
|
}
|
|
])
|
|
|
|
.factory("MissingKubeSocket", [
|
|
function() {
|
|
return function MissingKubeSocket(path, callback) {
|
|
throw Error("no KubeSocketFactory set");
|
|
};
|
|
}
|
|
])
|
|
|
|
/**
|
|
* KubeWatch
|
|
*
|
|
* Create a new low level kubernetes watch. These are instantiated
|
|
* by kubeLoader, and typically not used directly.
|
|
*
|
|
* An implementation of the KubeWatch must be provided. It has the
|
|
* following characteristics:
|
|
*
|
|
* promise = KubeWatch(path, callback)
|
|
*
|
|
* The watch is given two arguments. The first is the kube resource
|
|
* url to watch (without query string) a callback to invoke with
|
|
* watch frames.
|
|
*
|
|
* The watch returns a deferred promise which will resolve when the initial
|
|
* set of items has loaded, it will fail if the watch fails. The promise
|
|
* should also have a promise.cancel() method which is invoked when the
|
|
* watch should be stopped.
|
|
*
|
|
* callback(frames)
|
|
*
|
|
* The callback is invoked with an array of kubernetes watch frames that
|
|
* look like: { type: "ADDED", object: { ... } }
|
|
*/
|
|
|
|
.provider("KubeWatch", [
|
|
function() {
|
|
var self = this;
|
|
|
|
/* Until we come up with a good default implementation, must be provided */
|
|
self.KubeWatchFactory = "MissingKubeWatch";
|
|
|
|
function load(injector, name) {
|
|
if (angular.isString(name))
|
|
return injector.get(name, "KubeWatch");
|
|
else
|
|
return injector.invoke(name);
|
|
}
|
|
|
|
self.$get = [
|
|
"$injector",
|
|
function($injector) {
|
|
return load($injector, self.KubeWatchFactory);
|
|
}
|
|
];
|
|
}
|
|
])
|
|
|
|
.factory("MissingKubeWatch", [
|
|
function() {
|
|
return function MissingKubeWatch(path, callback) {
|
|
throw Error("no KubeWatchFactory set");
|
|
};
|
|
}
|
|
])
|
|
|
|
.provider("KubeDiscoverSettings", [
|
|
function() {
|
|
var self = this;
|
|
|
|
/* Until we come up with a good default implementation, must be provided */
|
|
self.KubeDiscoverSettingsFactory = "MissingKubeDiscoverSettings";
|
|
|
|
function load(injector, name) {
|
|
if (angular.isString(name))
|
|
return injector.get(name, "KubeDiscoverSettings");
|
|
else
|
|
return injector.invoke(name);
|
|
}
|
|
|
|
self.$get = [
|
|
"$injector",
|
|
function($injector) {
|
|
return load($injector, self.KubeDiscoverSettingsFactory);
|
|
}
|
|
];
|
|
}
|
|
])
|
|
|
|
.factory("MissingKubeDiscoverSettings", [
|
|
function() {
|
|
return function MissingKubeDiscoverSettings(path, callback) {
|
|
throw Error("no KubeDiscoverSettingsFactory set");
|
|
};
|
|
}
|
|
]);
|
|
}());
|