Drop cockpit-kubernetes

It's not built any more in any distribution supported by master.

Drop cockpit-stub as well, it was only being used in kubernetes.
This also finally allows us to drop angular.

Closes #12133
This commit is contained in:
Martin Pitt 2019-06-23 21:34:00 +02:00 committed by Martin Pitt
parent 3118819204
commit 819cc0db22
183 changed files with 47 additions and 44253 deletions

5
.gitignore vendored
View File

@ -69,9 +69,6 @@ depcomp
/cockpit-askpass
/cockpit-session
/cockpit-ssh
/cockpit-stub
/cockpit-kube-auth
/cockpit-kube-launch
/test-session
/cockpit-bridge
/remotectl
@ -160,8 +157,6 @@ po*.js.gz
/pkg/*/cockpit-components-*.js
/pkg/*/cockpit-components-*.js
/pkg/kubernetes/kubernetes-templates.js
/pkg/kubernetes/registry-templates.js
/pkg/playground/react-demo-*.js
/pkg/selinux/setroubleshoot-view.js
/pkg/tuned/change-profile.js

View File

@ -154,7 +154,6 @@ WEBPACK_PACKAGES = \
dashboard \
docker \
kdump \
kubernetes \
machines \
networkmanager \
pcp \

1
Vagrantfile vendored
View File

@ -48,7 +48,6 @@ Vagrant.configure(2) do |config|
etcd \
firewalld \
git \
kubernetes \
NetworkManager \
pcp \
qemu \

View File

@ -20,16 +20,12 @@
# To use this example add a line to an issue with the "bot" label
#
# * [ ] npm-update angular
# * [ ] npm-update patternfly
#
# Dependencies that are either fragile (minor updates break)
# or are part of our public Javascript API and need caution
FRAGILE = [
"angular",
"angular-bootstrap-npm",
"angular-gettext",
"angular-route",
"jquery",
"patternfly",
"paternfly-bootstrap-combobox",

View File

@ -26,7 +26,6 @@ GUIDE_INCLUDES = \
doc/guide/feature-docker.xml \
doc/guide/feature-firewall.xml \
doc/guide/feature-journal.xml \
doc/guide/feature-kubernetes.xml \
doc/guide/feature-machines.xml \
doc/guide/feature-networkmanager.xml \
doc/guide/feature-packagekit.xml \

View File

@ -45,7 +45,6 @@
<xi:include href="feature-terminal.xml"/>
<xi:include href="feature-pcp.xml"/>
<xi:include href="feature-subscription.xml"/>
<xi:include href="feature-kubernetes.xml"/>
<xi:include href="feature-machines.xml"/>
<xi:include href="feature-selinux.xml"/>
<xi:include href="feature-tuned.xml"/>

View File

@ -1,34 +0,0 @@
<?xml version="1.0"?>
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
"http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd">
<chapter id="feature-kubernetes">
<title>Kubernetes</title>
<para>Cockpit has a dashboard that interacts with a
<ulink url="https://kubernetes.io/">Kubernetes cluster</ulink> or an
<ulink url="https://enterprise.openshift.com/">Openshift v3 cluster</ulink>. This
functionality is in the Cockpit <emphasis>kubernetes</emphasis> package.</para>
<para>The dashboard is part of a normal authenticated Cockpit session. Cockpit
communicates with Kubernetes via its REST API.</para>
<para>Usually some form of authentication is necessary to access the Kubernetes REST API.
Like the <filename>kubectl</filename> and <filename>oc</filename> commands, Cockpit uses
the authentication and server information in the <code>~/.kube/config</code> file for
the logged in user.</para>
<para>If a user is able to use <code>kubectl</code> successfully when at their shell terminal,
then that same user will able to use Kubernetes dashboard when logged into Cockpit:</para>
<programlisting>
$ <command>kubectl get pods</command>
NAME READY STATUS RESTARTS AGE
docker-registry-1-l4pyh 1/1 Running 10 23d
...
</programlisting>
<para>When running Openshift one can use the <code>oc login</code> command to configure the
<code>~/.kube/config</code> file correctly. This in turn allows Cockpit to use that
login information.</para>
</chapter>

View File

@ -5,10 +5,6 @@
"dependencies": {
"@babel/polyfill": "7.4.4",
"@patternfly/react-console": "1.10.50",
"angular": "1.3.20",
"angular-bootstrap-npm": "0.13.4",
"angular-gettext": "2.3.11",
"angular-route": "1.3.20",
"bootstrap": "3.4.1",
"bootstrap-datepicker": "1.9.0",
"c3": "0.4.23",
@ -17,12 +13,8 @@
"jquery-flot": "0.8.3",
"js-sha1": "0.6.0",
"json-stable-stringify-without-jsonify": "1.0.1",
"kubernetes-container-terminal": "1.0.3",
"kubernetes-object-describer": "1.1.4",
"kubernetes-topology-graph": "0.0.25",
"moment": "2.24.0",
"mustache": "3.0.1",
"object-describer": "git+https://github.com/kubernetes-ui/object-describer.git#v1.0.4",
"patternfly": "3.58.0",
"patternfly-bootstrap-combobox": "1.1.7",
"patternfly-react": "2.34.3",
@ -64,7 +56,6 @@
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "7.13.0",
"eslint-plugin-standard": "^4.0.0",
"exports-loader": "~0.6.3",
"extend": "~3.0.0",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"html-webpack-plugin": "^3.2.0",

View File

@ -34,39 +34,3 @@ EXTRA_DIST += \
$(metainfo_DATA) \
$(pixmaps_DATA) \
$(NULL)
if WITH_GOLANG
AM_V_GO = $(am__v_GO_@AM_V@)
am__v_GO_ = $(am__v_GO_@AM_DEFAULT_V@)
am__v_GO_0 = @echo " GO " $@;
GO_BUILD_RULE = \
GOPATH=$(abs_srcdir)/pkg/kubernetes/standalone $(GOLANG) build \
-ldflags "-B 0x"`head -c20 /dev/urandom|od -An -tx1|tr -d ' \n'` "$@"
COCKPIT_KUBE_HELPERS = \
pkg/kubernetes/standalone/src/cockpit-kube-auth/helpers/creds.go \
pkg/kubernetes/standalone/src/cockpit-kube-auth/helpers/client.go \
$(NULL)
COCKPIT_KUBE_AUTH = $(COCKPIT_KUBE_HELPERS) \
pkg/kubernetes/standalone/src/cockpit-kube-auth/main.go \
$(NULL)
COCKPIT_KUBE_LAUNCH = $(COCKPIT_KUBE_HELPERS) \
pkg/kubernetes/standalone/src/cockpit-kube-launch/main.go \
$(NULL)
cockpit-kube-auth$(EXEEXT): $(COCKPIT_KUBE_AUTH)
$(AM_V_GO) $(GO_BUILD_RULE)
cockpit-kube-launch$(EXEEXT): $(COCKPIT_KUBE_LAUNCH)
$(AM_V_GO) $(GO_BUILD_RULE)
libexec_PROGRAMS += cockpit-kube-auth cockpit-kube-launch
cockpit_kube_auth_SOURCES = $(COCKPIT_KUBE_AUTH)
cockpit_kube_launch_SOURCES = $(COCKPIT_KUBE_LAUNCH)
endif

View File

@ -1,22 +0,0 @@
Developing the kubernetes component
-----------------------------------
This component adds functionality to Cockpit to perform admin tasks on
Kubernetes or Openshift. You'll see a "Cluster" tab listed in Cockpit.
### Running a test instance
There's a test instance of Openshift (and also Kubernetes) that's you
can run using the following command:
$ bots/vm-run openshift
To then access that it run the following in another terminal:
$ sudo yum install kubernetes-client
$ mkdir -p ~/.kube
$ cp bots/images/files/openshift.kubeconfig ~/.kube/config
You should now be able to use the kubectl command to access the cluster:
$ kubectl get pods

View File

@ -1,113 +0,0 @@
<!DOCTYPE html>
<!--
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/>.
-->
<html lang="en" ng-csp class="no-js">
<head>
<meta charset="utf-8">
<title translate>Kubernetes Cluster</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../base1/patternfly.css">
<link rel="stylesheet" href="kubernetes.css">
<script src="../base1/cockpit.js"></script>
<script src="kubernetes.js"></script>
<script src="../*/po.js"></script>
</head>
<body ng-app='kubernetes' ng-controller="MainCtrl" ng-class="{'cards-pf': viewActive(null)}" hidden>
<div ng-cloak ng-if="!curtains">
<div class="nav-pf-vertical nav-sidebar">
<div class="nav-item-pf-header">
<span translate>Cluster</span>
</div>
<ul class="list-group" id="kubernetes-navigation">
<li class="list-group-item" ng-class="{ active: viewActive(null)}">
<a ng-href="#{{ viewUrl(null) }}">
<i class="fa fa-dashboard fa-fw" title="Overview"></i>
<span class="list-group-item-value" translate>Overview</span>
</a>
</li>
<li class="list-group-item" ng-class="{ active: viewActive('nodes')}">
<a ng-href="#/nodes">
<i class="pficon pficon-container-node" title="Nodes"></i>
<span class="list-group-item-value" translate>Nodes</span>
</a>
</li>
<li class="list-group-item" ng-class="{ active: viewActive('pods')}">
<a ng-href="#{{ viewUrl('pods') }}">
<i class="fa fa-cubes fa-fw" title="Containers"></i>
<span class="list-group-item-value" translate>Containers</span>
</a>
</li>
<li class="list-group-item" ng-class="{ active: viewActive('topology')}">
<a ng-href="#{{ viewUrl('topology') }}">
<i class="pficon pficon-topology fa-fw" title="Topology"></i>
<span class="list-group-item-value" translate>Topology</span>
</a>
</li>
<li class="list-group-item" ng-class="{ active: viewActive('list')}">
<a ng-href="#{{ viewUrl('list') }}">
<i class="fa fa-list fa-fw" title="Details"></i>
<span class="list-group-item-value" translate>Details</span>
</a>
</li>
<li class="list-group-item" ng-class="{ active: viewActive('volumes')}">
<!-- Directly to avoid any namespace filtering -->
<a id="kubernetes-volumes" ng-href="#{{ viewUrl('volumes', true) }}">
<i class="fa fa-database fa-fw" title="Volumes"></i>
<span class="list-group-item-value" translate>Volumes</span>
</a>
</li>
<li class="list-group-item" ng-class="{ active: viewActive('images')}"
ng-if="settings.flavor == 'openshift'">
<a ng-href="#{{ viewUrl('images') }}">
<i class="pficon pficon-image fa-fw" title="Images"></i>
<span class="list-group-item-value" translate>Images</span>
</a>
</li>
<li class="list-group-item" ng-class="{ active: viewActive('projects')}"
ng-if="settings.flavor == 'openshift'">
<a ng-href="#{{ viewUrl('projects') }}">
<i class="pficon pficon-project fa-fw" title="Projects"></i>
<span class="list-group-item-value" translate>Projects</span>
</a>
</li>
</ul>
</div>
</div>
<div id="content" role="main" class="container-fluid" ng-view ng-if="!curtains" data-flavor="{{settings.flavor}}">
</div>
<div class="curtains-ct blank-slate-pf" ng-if="curtains">
<div class="blank-slate-pf-icon">
<div class="spinner spinner-lg" ng-show="!curtains.status"></div>
<i class="fa fa-exclamation-circle" ng-if="curtains.status && curtains.status != 403"></i>
<i class="fa fa-lock" ng-if="curtains.status == 403"></i>
</div>
<h1 ng-if="!curtains.status" translate>Connecting...</h1>
<h1 ng-if="curtains.status" role="alert" translate>Couldn't connect to server</h1>
<p ng-if="curtains.message">{{curtains.message}}</p>
<div class="blank-slate-pf-main-action" ng-if="curtains.status">
<button id="kubernetes-reconnect" class="btn btn-primary btn-lg" translate ng-click="reconnect()">Reconnect</button>
<button id="kubernetes-connection-tb" class="btn btn-primary btn-lg" translate ng-click="changeAuth(curtains.resp)">Troubleshoot</button>
</div>
</div>
</body>
</html>

View File

@ -1,14 +0,0 @@
{
"version": "@VERSION@",
"requires": {
"cockpit": "137.x"
},
"dashboard": {
"index": {
"label": "Cluster",
"order": 20,
"icon": "pficon-cluster"
}
}
}

View File

@ -1,9 +0,0 @@
{
"dashboard": {
"registry": {
"label": "Image Registry",
"order": 30,
"icon": "pficon-registry"
}
}
}

View File

@ -1,80 +0,0 @@
<!DOCTYPE html>
<!--
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/>.
-->
<html lang="en" ng-csp class="no-js">
<head>
<meta charset="utf-8">
<title translate>Image Registry</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../base1/patternfly.css">
<link rel="stylesheet" href="registry.css">
<script src="../base1/cockpit.js"></script>
<script src="registry.js"></script>
<script src="../*/po.js"></script>
</head>
<body ng-app='registry' ng-controller="MainCtrl" ng-class="{'cards-pf': viewActive(null)}" hidden>
<div ng-cloak ng-if="!curtains">
<div class="icon-sidebar">
<ul>
<li ng-class="{ active: viewActive(null)}">
<a ng-href="#{{ viewUrl(null) }}">
<i class="fa fa-dashboard fa-fw" title="Overview"></i><br>
<span class="list-group-item-value" translate>Overview</span>
</a>
</li>
<li ng-class="{ active: viewActive('images')}">
<a ng-href="#{{ viewUrl('images') }}">
<i class="pficon pficon-image fa-fw" title="Images"></i><br>
<span class="list-group-item-value" translate>Images</span>
</a>
</li>
<li ng-class="{ active: viewActive('projects')}">
<a ng-href="#{{ viewUrl('projects') }}">
<i class="pficon pficon-project fa-fw" title="Projects"></i><br>
<span class="list-group-item-value" translate>Projects</span>
</a>
</li>
</ul>
</div>
</div>
<div id="content" role="main" ng-view ng-if="!curtains">
</div>
<div class="curtains-ct blank-slate-pf" ng-if="curtains">
<div class="blank-slate-pf-icon">
<div class="spinner spinner-lg" ng-show="!curtains.status"></div>
<i class="fa fa-exclamation-circle" ng-if="curtains.status && curtains.status != 403"></i>
<i class="fa fa-lock" ng-if="curtains.status == 403"></i>
</div>
<h1 ng-if="!curtains.status" translate>Connecting...</h1>
<h1 ng-if="curtains.status" role="alert" translate>Couldn't connect to server</h1>
<p ng-if="curtains.message">{{curtains.message}}</p>
<div class="blank-slate-pf-main-action" ng-if="curtains.status">
<button class="btn btn-primary btn-lg" translate ng-click="reconnect()">Reconnect</button>
</div>
</div>
</body>
</html>

View File

@ -1,355 +0,0 @@
/*
* 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');
require('angular-bootstrap-npm/dist/angular-bootstrap.js');
require('./kube-client');
require('./kube-client-cockpit');
require('./connection');
require('../views/auth-dialog.html');
require('../views/filter-bar.html');
require('../views/filter-project.html');
angular.module('kubernetes.app', [
'ui.bootstrap',
'kubeClient',
'kubeClient.cockpit',
'kubernetes.connection'
])
.controller('MainCtrl', [
'$scope',
'$location',
'$rootScope',
'$timeout',
'$modal',
'kubeLoader',
'kubeSelect',
'KubeDiscoverSettings',
'filterService',
'connectionActions',
function($scope, $location, $rootScope, $timeout, $modal,
loader, select, discoverSettings, filter, connectionActions) {
$scope.settings = { };
/* Used to set detect which route is active */
$scope.viewActive = function(segment) {
var url = $location.url() || "/";
var parts = url.split('?')[0].split("/");
if (!segment && !parts[1])
return true;
if (segment === parts[1])
return true;
return false;
};
/* Used to build simple route URLs */
$scope.viewUrl = function(segment, forceQS) {
var namespace = loader.limits.namespace;
var path;
var parts = [];
if (angular.isArray(namespace))
namespace = null;
if (segment)
parts.push(segment);
else
forceQS = true;
if (!forceQS && namespace)
parts.push(namespace);
path = "/" + parts.map(encodeURIComponent).join("/");
if (namespace && forceQS)
return path + "?namespace=" + encodeURIComponent(namespace);
else
return path;
};
/* Used while debugging */
$scope.console = console;
/* Show the body when ready */
function visible() {
document.getElementsByTagName("body")[0].removeAttribute("hidden");
}
/* Show after some seconds whether ready or not */
$timeout(visible, 3000);
/* Curtains related logic */
function connect(force) {
$scope.curtains = { };
discoverSettings(force).then(function(settings) {
$scope.settings = settings;
$scope.curtains = null;
filter.globals(settings.isAdmin);
filter.load().then(visible);
}, function(resp) {
$scope.curtains = {
status: resp.status,
resp: resp,
message: resp.message || resp.statusText,
};
$scope.settings = null;
visible();
});
}
/* Connect automatically initially */
connect();
/* Used by reconnect buttons */
$scope.reconnect = function(force) {
if (force === undefined)
force = true;
discoverSettings(force);
loader.reset();
connect();
};
/* When the loader changes digest */
loader.listen(function() {
$rootScope.$applyAsync();
}, $rootScope);
$scope.changeAuth = function(ex) {
var promise = $modal.open({
animation: false,
controller: 'ChangeAuthCtrl',
templateUrl: 'views/auth-dialog.html',
resolve: {
dialogData: function() {
return connectionActions.load(ex);
}
},
}).result;
/* If the change is successful, reconnect */
promise.then(function(force) {
$scope.reconnect(force);
});
return promise;
};
}
])
.directive('filterBar', [
'filterService',
function(filter) {
return {
restrict: 'E',
scope: true,
transclude: true,
link: function(scope, element, attrs) {
scope.filter = filter;
},
templateUrl: 'views/filter-bar.html'
};
}
])
.directive('filterProject', [
'filterService',
function(filter) {
return {
restrict: 'E',
scope: true,
link: function(scope, element, attrs) {
scope.filter = filter;
},
templateUrl: 'views/filter-project.html'
};
}
])
.factory('filterService', [
'$q',
'$route',
'$rootScope',
'kubeLoader',
'kubeSelect',
'KubeDiscoverSettings',
function($q, $route, $rootScope, loader, select, discoverSettings) {
/*
* We have the following cases to account for:
*
* Openshift:
* - Have Project objects
* - Project objects are listable by any user, only accessilbe returned
*
* Kubernetes and Openshift
* - Namespace objects are only accessible to all users
*
* The globals variable is set based on this.
*/
var globals = true;
var promise = discoverSettings().then(function(settings) {
var ret = [];
if (settings.flavor === "openshift") {
ret.push(loader.watch("projects", $rootScope));
ret.push(loader.load("projects"));
}
if (settings.isAdmin)
ret.push(loader.watch("namespaces", $rootScope));
return $q.all(ret);
});
/*
* When either a Namespace or Project is loaded we'll want to reinterpret
* how we look at the current namespace. This helps to handle cases where
* the user can't see all projects, and one is loaded.
*/
loader.listen(function(present) {
var link, object;
for (link in present) {
object = present[link];
if (object.kind == "Namespace" || object.kind == "Project") {
loadNamespace($route.current);
return;
}
}
});
function calcAvailable() {
var all;
if (globals)
all = select().kind("Namespace");
if (!all || all.length === 0)
all = select().kind("Project")
.statusPhase("Active");
var link, meta;
var ret = [];
for (link in all) {
meta = all[link].metadata || { };
if (meta.name)
ret.push(meta.name);
}
return ret;
}
function loadNamespace(route) {
var value = null;
if (route)
value = route.params["namespace"] || null;
/*
* When we can't see globals, we tell the loader about
* all namespaces that we can see. It'll open up individual
* watches about those namespaces.
*/
if (value === null && !globals) {
value = calcAvailable();
/* We might not be global, but the projects may not
* be loaded yet */
if (value.length < 1)
value = null;
}
if (!angular.equals(value, loader.limits.namespaces)) {
loader.limit({ namespace: value });
}
}
$rootScope.$on("$routeChangeSuccess", function (event, current, prev) {
loadNamespace(current);
});
$rootScope.$on("$routeUpdate", function (event, current, prev) {
loadNamespace(current);
});
if ($route.current)
loadNamespace($route.current);
return {
load: function() {
return promise;
},
globals: function(value) {
if (arguments.length === 0)
return globals;
value = !!value;
if (globals !== value) {
globals = value;
loadNamespace($route.current);
}
},
namespaces: calcAvailable,
namespace: function(value) {
if (arguments.length === 0)
return $route.current.params["namespace"];
var copy = angular.copy($route.current.params);
copy["namespace"] = value || undefined;
copy["target"] = null;
$route.updateParams(copy);
}
};
}
])
/* The default orderBy filter doesn't work on objects */
.filter('orderObjectBy', function() {
return function(items, field) {
var i;
var sorted = [];
for (i in items)
sorted.push(items[i]);
if (!angular.isArray(field))
field = [ String(field) ];
var criteria = field.map(function(v) {
return v.split('.');
});
function value(obj, x) {
return obj ? obj[x] : undefined;
}
sorted.sort(function(a, b) {
var ra, rb, i;
var len = criteria.length;
for (i = 0; i < len; i++) {
ra = criteria[i].reduce(value, a);
rb = criteria[i].reduce(value, b);
if (ra === rb)
continue;
return (ra > rb ? 1 : -1);
}
return 0;
});
return sorted;
};
})
.filter("formatBytes", [
"KubeFormat",
function(format) {
return function(num) {
if (typeof num == "number")
return format.formatBytes(num);
return num;
};
}]);
}());

View File

@ -1,557 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
(function() {
var angular = require('angular');
require('angular-bootstrap-npm/dist/angular-bootstrap.js');
var d3 = require('d3');
var focusedClasses = {
"chart-focused": true,
"chart-unfocused": false
};
var unfocusedClasses = {
"chart-focused": false,
"chart-unfocused": true
};
var focusResetClasses = {
"chart-focused": false,
"chart-unfocused": false
};
function parsePXVal(val) {
var n;
if (val)
n = parseInt(val.slice(0, val.length - 2), 10);
if (!n || isNaN(n))
n = 0;
return n;
}
function getPadding(el) {
// May return null on FF when iframe is hidden.
var style = window.getComputedStyle(el, null);
if (style) {
return {
left: parsePXVal(style.getPropertyValue('padding-left')),
right: parsePXVal(style.getPropertyValue('padding-right')),
top: parsePXVal(style.getPropertyValue('padding-top')),
bottom: parsePXVal(style.getPropertyValue('padding-bottom'))
};
} else {
return {
left: 0,
right: 0,
top: 0,
bottom: 0
};
}
}
function getSize(el) {
var p = getPadding(el);
var width = el.clientWidth - p.left - p.right;
var height = el.clientHeight - p.top - p.bottom;
if (width < 0)
width = 0;
if (height < 0)
height = 0;
return {
width: width,
height: height
};
}
angular.module('ui.charts', [
'ui.bootstrap',
])
.directive('thresholdHeatMap', [
"$window",
"KubeTranslate",
"$timeout",
function ($window, translate, $timeout) {
const _ = translate.gettext;
return {
restrict: 'A',
scope: {
data: '=?',
legendLabels: '=?',
thresholds: '=?',
colors: '=?',
},
link: function($scope, element, attributes) {
var data;
var padding = 2;
var thresholdDefaults = [0, 0.7, 0.8, 0.9];
var heatmapColorDefaults = ['#bbbbbb', '#d4f0fa', '#F9D67A', '#EC7A08', '#CE0000'];
var legendLabelDefaults = [_("Unavailable"), '< 70%', '70-80%', '80-90%', '> 90%'];
var maxSize = attributes['maxBlockSize'];
if (!maxSize || isNaN(maxSize)) {
maxSize = 64;
} else {
maxSize = parseInt(maxSize, 10);
if (maxSize < 5)
maxSize = 5;
else if (maxSize > 64)
maxSize = 64;
}
if (!$scope.thresholds)
$scope.thresholds = thresholdDefaults;
if (!$scope.colors)
$scope.colors = heatmapColorDefaults;
if (!$scope.legendLabels)
$scope.legendLabels = legendLabelDefaults;
var svg = d3.select(element[0]).append("svg")
.classed("heatmap-pf-svg", true)
.style("width", "100%");
if (attributes['legend'])
buildLegend();
function getBlockSize(n, x, y) {
if (x < 1 || y < 1)
return 0;
if (n === 0)
return maxSize;
var px = Math.ceil(Math.sqrt(n * x / y));
var py = Math.ceil(Math.sqrt(n * y / x));
var sx, sy;
if (Math.floor(px * y / x) * px < n)
sx = y / Math.ceil(px * y / x);
else
sx = x / px;
if (Math.floor(py * x / y) * py < n)
sy = x / Math.ceil(x * py / y);
else
sy = y / py;
return Math.max(sx, sy);
}
function getSizeInfo() {
var length = data ? data.length : 0;
var size = getSize(element[0]);
var h = size.height;
var w = size.width;
var rows;
var blockSize = getBlockSize(length, w, h);
if ((blockSize - padding) > maxSize) {
blockSize = padding + maxSize;
// Attempt to square off the area, check if square fits
rows = Math.ceil(Math.sqrt(length));
if (blockSize * rows > w ||
blockSize * rows > h) {
rows = (blockSize === 0) ? 0 : Math.floor(h / blockSize);
}
} else {
rows = (blockSize === 0) ? 0 : Math.floor(h / blockSize);
}
return {
rows: rows,
block: blockSize
};
}
function buildLegend() {
var colors = $scope.colors.slice(0);
var labels = $scope.legendLabels.slice(0);
labels.reverse();
colors.reverse();
var legend = d3.select("#" + attributes['legend'])
.append('ul')
.classed('chart-legend', true);
var li = legend.selectAll("li").data(labels);
var newLi = li.enter().append("li")
.classed('chart-legend-item', true)
.on("mouseover", function(d, i) {
var color = colors[i];
var rsel = "rect[data-color='" + color + "']";
var lsel = "li[data-color='" + color + "']";
legend.selectAll("li").classed(unfocusedClasses);
legend.select(lsel).classed(focusedClasses);
svg.selectAll("rect").classed(unfocusedClasses);
svg.selectAll(rsel).classed(focusedClasses);
})
.on("mouseout", function (d, i) {
svg.selectAll("rect").classed(focusResetClasses);
legend.selectAll("li").classed(focusResetClasses);
});
newLi.append("span")
.classed('legend-pf-color-box', true);
newLi.append("span")
.classed('legend-pf-text', true);
li.attr("data-color", function (d, i) {
return colors[i];
});
li.select("span.legend-pf-color-box")
.style("background-color", function (d, i) {
return colors[i];
});
li.select("span.legend-pf-text")
.text(function (d) {
return d;
});
}
function getcolor(d, colorFunc) {
var value = d;
if (d && d.value !== undefined)
value = d.value;
if (isNaN(value))
value = -1;
return colorFunc(value);
}
function refresh() {
var colorFunc = d3.scale.threshold()
.domain($scope.thresholds)
.range($scope.colors);
var size = getSizeInfo();
if (!data)
data = [ ];
var fillSize = size.block - padding;
if (fillSize < 1)
return;
svg.attr("height", size.block * size.rows);
var blocks = svg.selectAll('rect').data(data);
blocks.enter().append('rect')
.on("mouseover", function(d, i) {
svg.selectAll('rect').classed(unfocusedClasses);
d3.select(this).classed(focusedClasses);
})
.on("mouseout", function (d, i) {
svg.selectAll('rect').classed(focusResetClasses);
})
.append("title");
blocks
.attr('x', function (d, i) {
return Math.floor(i / size.rows) * size.block;
})
.attr('y', function (d, i) {
return i % size.rows * size.block;
})
.attr('width', fillSize)
.attr('height', fillSize)
.attr('data-color', function (d) {
return getcolor(d, colorFunc);
})
.style('fill', function (d) {
return getcolor(d, colorFunc);
})
.on('click', function (d) {
if (d && d.name)
$scope.$emit("boxClick", d.name);
})
.select("title")
.text(function(d) {
return d.tooltip;
});
blocks.exit().remove();
}
$scope.$watchCollection('data', function(newValue) {
data = newValue;
refresh();
});
angular.element($window).bind('resize', function () {
refresh();
});
$scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
refresh();
}
);
}
};
}
])
.directive('donutPctChart', [
"$window",
function ($window) {
return {
restrict: 'A',
scope: {
data: '=?',
largeTitle: '=?',
smallTitle: '=?',
},
link: function($scope, element, attributes) {
var arc, selectedArc, data;
var colors = d3.scale.category20();
var pie = d3.layout.pie().value(function (d) {
if (typeof d === 'object')
return d.value;
else
return d;
});
var legend;
if (attributes['legend']) {
legend = d3.select("#" + attributes['legend'])
.append('ul')
.classed('chart-legend', true);
}
var svg = d3.select(element[0]).append("svg");
svg.style("width", "100%");
svg.style("height", "100%");
var g = svg.append("g");
g.append('text').attr('class', "chart-title");
function updateSize() {
var size = getSize(element[0]);
var width = size.width;
var height = size.height;
var c;
var radius = Math.min(width, height) / 2;
var barSize = parseInt(attributes['barSize'], 10);
if (isNaN(barSize))
barSize = 20;
if ((barSize * 2) > radius)
barSize = radius;
arc = d3.svg.arc()
.innerRadius(radius - (barSize * 2))
.outerRadius(radius - barSize);
selectedArc = d3.svg.arc()
.innerRadius(radius - (barSize * 2))
.outerRadius(radius - (barSize - 2));
c = (radius - barSize - 2) * 2;
g.attr('data-innersize', Math.sqrt((c * c) / 2));
updateTitle();
g.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
refresh();
}
function calFontSize() {
var width = this.getComputedTextLength();
var inner = parseInt(g.attr("data-innersize"), 10);
var style = window.getComputedStyle(this, null);
var size, ratio;
if (style && width) {
size = style.getPropertyValue('font-size');
ratio = inner / width;
} else {
width = 0;
}
if (!isNaN(inner) && width > inner)
return "calc(" + size + " * " + ratio + ")";
}
function updateTitle() {
var title = g.select('text.chart-title');
var size = parseInt(g.attr("data-innersize"), 10);
if (isNaN(size))
return;
title.selectAll('tspan').remove();
if ($scope.largeTitle) {
title.insert('tspan').text($scope.largeTitle)
.classed('donut-title-big-pf', true)
.attr('dy', 0)
.attr('x', 0)
.style("font-size", calFontSize);
}
if ($scope.smallTitle) {
title.insert('tspan').text($scope.smallTitle)
.classed('donut-title-small-pf', true)
.attr('dy', 20)
.attr('x', 0)
.style("font-size", calFontSize);
}
}
function select(id) {
var sel = "path[data-id='" + id + "']";
var lsel = "li[data-id='" + id + "']";
g.selectAll("path").classed(unfocusedClasses);
g.select(sel).attr("d", function (d, i) {
return selectedArc(d, i);
});
g.select(sel).classed(focusedClasses);
if (legend) {
legend.selectAll("li").classed(unfocusedClasses);
legend.select(lsel).classed(focusedClasses);
}
}
function unselect() {
g.selectAll("path")
.classed(focusResetClasses)
.attr("d", arc);
if (legend) {
legend.selectAll("li")
.classed(focusResetClasses);
}
}
function refreshLegend() {
var li = legend.selectAll("li").data(data);
var newLi = li.enter().append("li")
.classed('chart-legend-item', true)
.on("mouseover", function() {
select(this.getAttribute('data-id'));
})
.on("mouseout", unselect);
newLi.append("span").classed('legend-pf-color-box', true);
newLi.append("span").classed('legend-pf-text', true);
li.attr("data-id", function (d, i) {
return i;
});
li.select("span.legend-pf-color-box")
.style("background-color", function (d, i) {
if (d && d.color)
return d.color;
else
return colors(i);
});
li.select("span.legend-pf-text")
.text(function (d) {
if (d && d.label)
return d.label;
else
return d;
});
li.exit().remove();
}
function refresh() {
if (!data)
data = [];
var path = g.selectAll("path")
.data(pie(data));
path.enter().append("path")
.on("mouseover", function(i) {
select(this.getAttribute('data-id'));
})
.on("mouseout", unselect)
.append("title");
path.attr("fill", function(d, i) {
if (d.data && d.data.color)
return d.data.color;
else
return colors(i);
})
.attr("d", arc)
.attr("data-id", function (d, i) {
return i;
})
.select("title")
.text(function(d) {
if (d.data && d.data.tooltip)
return d.data.tooltip;
});
path.exit().remove();
if (legend)
refreshLegend();
}
$scope.$watchCollection('data', function(newValue) {
data = newValue;
refresh();
});
/* Watch the selection for changes */
$scope.$watchGroup(["largeTitle", "smallTitle"], function() {
updateTitle();
});
angular.element($window).bind('resize', function () {
updateSize();
});
$scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
updateSize();
}
);
}
};
}
]);
}());

View File

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

View File

@ -1,342 +0,0 @@
/*
* 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');
require('angular-route');
require('angular-dialog.js');
require('./kube-client');
require('./listing');
require('kubernetes-container-terminal/dist/container-terminal.js');
angular.module('kubernetes.containers', [
'ngRoute',
'ui.cockpit',
'kubernetesUI',
'kubeClient',
'kubernetes.listing'
])
.config([
'$routeProvider',
function($routeProvider) {
$routeProvider
.when('/pods/:pod_namespace/:pod_name/:container_name', {
templateUrl: 'views/container-page.html',
controller: 'ContainerCtrl'
})
.when('/pods/:pod_namespace/:pod_name', {
redirectTo: '/pods'
})
.when('/pods/:pod_namespace?', {
templateUrl: 'views/containers-page.html',
controller: 'ContainersCtrl'
});
}
])
/*
* The controller for the containers view.
*/
.controller('ContainersCtrl', [
'$scope',
'KubeContainers',
'kubeLoader',
'kubeSelect',
'ListingState',
'$routeParams',
'$location',
function($scope, containers, loader, select, ListingState, $routeParams, $location) {
var selector = {};
var qs = $location.search();
for (var key in qs) {
if (key !== "namespace")
selector[key] = qs[key];
}
loader.listen(function() {
var pods = select().kind("Pod");
if ($routeParams.pod_namespace)
pods.namespace($routeParams.pod_namespace);
if (!angular.equals({}, selector))
pods.label(selector);
$scope.pods = pods;
}, $scope);
loader.watch("Pod", $scope);
$scope.listing = new ListingState($scope);
$scope.containers = containers;
$scope.$on("activate", function(ev, id) {
ev.preventDefault();
$location.path(id);
});
$scope.should_mask = function(name) {
return name.toLowerCase().indexOf("password") !== -1;
};
}
])
/*
* The controller for the containers view.
*/
.controller('ContainerCtrl', [
'$scope',
'KubeContainers',
'kubeLoader',
'kubeSelect',
'$routeParams',
'$route',
function($scope, containers, loader, select, $routeParams, $route) {
var target = $routeParams["container_name"] || "";
$scope.target = target;
loader.listen(function() {
$scope.pod = select().kind("Pod")
.namespace($routeParams.pod_namespace || "")
.name($routeParams.pod_name || "")
.one();
if ($scope.pod) {
angular.forEach(containers($scope.pod) || [], function (con) {
if (con.spec && con.spec.name === target)
$scope.container = con;
});
}
}, $scope);
loader.watch("Pod", $scope);
$scope.back = function() {
$route.updateParams({ "container_name" : undefined });
};
$scope.should_mask = function(name) {
return name.toLowerCase().indexOf("password") !== -1;
};
}
])
/**
* Build an array of container objects where each object contains the data from both
* the spec and status sections of the pod. Looks like this:
* { id: id, spec: pod.spec.containers[n], status: pod.status.containerStatuses[n] }
*
* The returned array will not change once created for a given pod item.
*/
.factory('KubeContainers', [
'KubeMapNamedArray',
function(mapNamedArray) {
return function (item) {
var specs, statuses, pod_id;
if (!item.containers) {
pod_id = "pods/" + item.metadata.namespace + "/" + item.metadata.name;
if (item.spec)
specs = mapNamedArray(item.spec.containers);
else
specs = { };
if (item.status)
statuses = mapNamedArray(item.status.containerStatuses);
else
statuses = { };
item.containers = Object.keys(specs).map(function(name) {
var key = pod_id + "/" + name;
return { spec: specs[name], status: statuses[name], key: key };
});
}
return item.containers;
};
}
])
.directive('containersListing',
function() {
return {
restrict: 'A',
templateUrl: 'views/containers-listing.html'
};
}
)
.directive('containerPageInline',
function() {
return {
restrict: 'A',
templateUrl: 'views/container-page-inline.html'
};
}
)
.directive('kubeContainerBody',
function() {
return {
restrict: 'E',
templateUrl: 'views/container-body.html'
};
}
)
.directive('kubePodBody',
function() {
return {
restrict: 'E',
templateUrl: 'views/pod-body.html'
};
}
)
/*
* Displays a container console.
*
* <kube-console namespace="ns" container="name"></kube-console>
*/
.directive('kubeConsole', [
'kubernetesContainerSocket',
function(socket) {
return {
restrict: 'E',
scope: {
pod: '&',
container: '&',
command: '@',
prevent: '='
},
link: function(scope, element, attrs) {
var limit = 64 * 1024;
var outer = angular.element("<div>");
outer.addClass("console-ct");
element.append(outer);
var pre = angular.element("<pre>");
outer.append(pre);
var wait = null;
var ws = null;
function connect() {
pre.empty();
var url = "";
var pod = scope.pod();
if (pod.metadata)
url += pod.metadata.selfLink;
else
url += pod;
url += "/log";
if (url.indexOf('?') === -1)
url += '?';
url += "follow=1";
var container = scope.container ? scope.container() : null;
if (container)
url += "&container=" + encodeURIComponent(container);
var writing = [];
var count = 0;
function drain() {
wait = null;
var at_bottom = pre[0].scrollHeight - pre[0].scrollTop <= pre[0].offsetHeight;
var text = writing.join("");
/*
* Stay under the limit. I wish we could use some other mechanism
* for limiting the log output, such as:
*
* https://github.com/kubernetes/kubernetes/issues/12447
*/
count += text.length;
var children, first, removed;
while (count > limit) {
children = pre.children();
if (children.length < 1)
break;
first = angular.element(children[0]);
removed = first.text().length;
first.remove();
count -= removed;
}
/* And add our text */
var span = angular.element("<span>").text(text);
writing.length = 0;
pre.append(span);
if (at_bottom)
pre[0].scrollTop = pre[0].scrollHeight;
}
ws = socket(url);
ws.onclose = function(ev) {
writing.push(ev.reason);
drain();
disconnect();
ws = null;
};
ws.onmessage = function(ev) {
writing.push(ev.data);
if (wait === null)
wait = window.setTimeout(drain, 50);
};
}
function disconnect() {
if (ws) {
ws.onopen = ws.onmessage = ws.onerror = ws.onclose = null;
if (ws.readyState < 2) // CLOSING
ws.close();
ws = null;
}
window.clearTimeout(wait);
wait = null;
}
scope.$watch("prevent", function(prevent) {
if (!prevent && !ws)
connect();
});
scope.$on("$destroy", disconnect);
}
};
}
])
/*
* Filter to display short docker ids
*
* {{ myid | kube-identifier }}
*
* Removes docker:// prefix and shortens.
*/
.filter('kubeIdentifier', function() {
var regex = /docker:\/\/([\w]{12})\w+/;
return function(item) {
var match = regex.exec(item);
if (match)
return match[1];
return item;
};
});
}());

View File

@ -1,514 +0,0 @@
/*
* 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');
require('angular-route');
require('./details');
require('./app');
require('./graphs');
require('./nodes');
require('./volumes');
require('../views/dashboard-page.html');
require('../views/deploy.html');
require('../views/file-button.html');
angular.module('kubernetes.dashboard', [
'ngRoute',
'kubernetes.details',
'kubernetes.app',
'kubernetes.graph',
'kubernetes.nodes'
])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
templateUrl: 'views/dashboard-page.html',
controller: 'DashboardCtrl',
reloadOnSearch: false,
});
}])
.controller('DashboardCtrl', [
'$scope',
'kubeLoader',
'kubeSelect',
'dashboardData',
'dashboardActions',
'itemActions',
'nodeActions',
'nodeData',
'$location',
function($scope, loader, select, data, actions, itemActions,
nodeActions, nodeData, $location) {
loader.listen(function() {
$scope.services = select().kind("Service");
$scope.nodes = select().kind("Node");
$scope.pods = select().kind("Pod");
$scope.volumes = select().kind("PersistentVolume");
$scope.pvcs = select().kind("PersistentVolumeClaim");
$scope.status = {
pods: {
Pending: $scope.pods.statusPhase("Pending"),
Failed: $scope.pods.statusPhase("Failed"),
Unknown: $scope.pods.statusPhase("Unknown"),
},
nodes: {
Pending: $scope.nodes.statusPhase("Pending"),
Terminated: $scope.nodes.statusPhase("Terminated"),
NotReady: $scope.nodes.conditionNotTrue("Ready"),
OutOfDisk: $scope.nodes.conditionTrue("OutOfDisk"),
},
volumes: {
Pending: $scope.volumes.statusPhase("Pending"),
PendingClaims: $scope.pvcs.statusPhase("Pending"),
Available: $scope.volumes.statusPhase("Available"),
Released: $scope.volumes.statusPhase("Released"),
Failed: $scope.volumes.statusPhase("Failed"),
},
};
}, $scope);
loader.watch("Node", $scope);
loader.watch("Service", $scope);
loader.watch("ReplicationController", $scope);
loader.watch("Pod", $scope);
loader.watch("PersistentVolume", $scope);
loader.watch("PersistentVolumeClaim", $scope);
$scope.editServices = false;
$scope.toggleServiceChange = function toggleServiceChange() {
$scope.editServices = !$scope.editServices;
};
$scope.jumpService = function jumpService(ev, service) {
if ($scope.editServices)
return;
var meta = service.metadata || {};
var spec = service.spec || {};
if (spec.selector && !angular.equals({}, spec.selector) && meta.namespace)
$location.path("/pods/" + encodeURIComponent(meta.namespace)).search(spec.selector);
};
$scope.navigateNode = function(node) {
var meta = node.metadata || {};
if (meta.name)
$location.path("/nodes/" + encodeURIComponent(meta.name));
};
/* All the actions available on the $scope */
angular.extend($scope, actions);
angular.extend($scope, data);
angular.extend($scope, nodeData);
$scope.modifyService = itemActions.modifyService;
$scope.addNode = nodeActions.addNode;
/* Highlighting */
$scope.highlighted = null;
$scope.$on("highlight", function(ev, uid) {
$scope.highlighted = uid;
});
$scope.highlight = function highlight(uid) {
$scope.$broadcast("highlight", uid);
};
$scope.servicesState = function services_state() {
if ($scope.failure)
return 'failed';
var service;
for (service in $scope.services)
break;
return service ? 'ready' : 'empty';
};
}])
.directive('kubernetesAddress', function() {
return {
restrict: 'E',
link: function($scope, element, attributes) {
$scope.$watchGroup(["item.spec.clusterIP", "item.spec.ports"], function(values) {
var address = values[0];
var ports = values[1];
var href = null;
var text = null;
/* No ports */
if (!ports || !ports.length) {
text = address;
/* One single HTTP or HTTPS port */
} else if (ports.length == 1) {
text = address + ":" + ports[0].port;
if (ports[0].protocol === "TCP") {
if (ports[0].port === 80)
href = "http://" + encodeURIComponent(address);
else if (ports[0].port === 443)
href = "https://" + encodeURIComponent(address);
} else {
text += "/" + ports[0].protocol;
}
} else {
text = " " + address + " " + ports.map(function(p) {
if (p.protocol === "TCP")
return p.port;
else
return p.port + "/" + p.protocol;
}).join(" ");
}
var el;
element.empty();
if (href) {
el = angular.element("<a>")
.attr("href", href)
.attr("target", "_blank")
.on("click", function(ev) { ev.stopPropagation() });
element.append(el);
} else {
el = element;
}
el.text(text);
});
}
};
})
.factory('dashboardActions', [
'$modal',
function($modal) {
function deploy() {
return $modal.open({
animation: false,
controller: 'DeployCtrl',
templateUrl: 'views/deploy.html',
resolve: {},
}).result;
}
return {
deploy: deploy,
};
}
])
.factory('dashboardData', [
'kubeSelect',
function(select) {
function conditionDigest(arg, match) {
if (typeof arg == "string")
return [ arg ];
var conditions = (arg.status || { }).conditions || [ ];
var result = [ ];
conditions.forEach(function(condition) {
if ((match && condition.status == "True") ||
(!match && condition.status != "True")) {
result.push(condition.type);
}
});
return result;
}
select.register({
name: "conditionTrue",
digests: function(arg) {
return conditionDigest(arg, true);
}
});
select.register({
name: "conditionNotTrue",
digests: function(arg) {
return conditionDigest(arg, false);
}
});
return {
nodeContainers: function nodeContainers(node) {
var count = 0;
var meta = node.metadata || { };
angular.forEach(select().kind("Pod")
.host(meta.name), function(pod) {
var spec = pod.spec || { };
var n = 1;
if (spec.containers)
n = spec.containers.length;
count += n;
});
return count;
},
serviceStatus: function serviceStatus(service) {
var spec = service.spec || { };
var meta = service.metadata || { };
var state = "";
var pods = select().kind("Pod")
.namespace(meta.namespace || "")
.label(spec.selector || {});
angular.forEach(pods, function(pod) {
if (!pod.status || !pod.status.phase)
return;
switch (pod.status.phase) {
case "Pending":
if (!state)
state = "wait";
break;
case "Running":
break;
case "Succeeded":
break;
case "Unknown":
break;
case "Failed":
/* falls through */
default: /* assume failed */
state = "fail";
break;
}
});
return state;
},
serviceContainers: function serviceContainers(service) {
var spec = service.spec || { };
var meta = service.metadata || {};
/* Calculate number of containers */
var x = 0;
var y = 0;
/*
* Calculate "x of y" containers, where x is the current
* number and y is the expected number. If x==y then only
* show x. The calculation is based on the statuses of the
* containers within the pod. Pod states: Pending,
* Running, Succeeded, Failed, and Unknown.
*/
var pods = select().kind("Pod")
.namespace(meta.namespace || "")
.label(spec.selector || {});
angular.forEach(pods, function(pod) {
if (!pod.status || !pod.status.phase)
return;
var spec = pod.spec || { };
var n = 1;
if (spec.containers)
n = spec.containers.length;
switch (pod.status.phase) {
case "Pending":
y += n;
break;
case "Running":
x += n;
y += n;
break;
case "Succeeded": // don't increment either counter
break;
case "Unknown":
y += n;
break;
case "Failed":
/* falls through */
default: /* assume failed */
y += n;
break;
}
});
if (x != y)
return x + " of " + y;
else
return "" + x;
}
};
}
])
.controller("DeployCtrl", [
"$q",
"$scope",
"$timeout",
"$modalInstance",
"filterService",
"kubeMethods",
"KubeFormat",
"KubeTranslate",
function($q, $scope, $timeout, $instance, filter, methods, KubeFormat, translate) {
const _ = translate.gettext;
var file;
var fields = {
"filename": "",
"namespace" : filter.namespace(),
};
function validate_manifest() {
var defer = $q.defer();
var ex;
var fails = [];
var ns = fields.namespace;
if (!ns)
ex = new Error(_("Namespace cannot be empty."));
else if (!/^[a-z0-9]+$/i.test(ns))
ex = new Error(_("Please provide a valid namespace."));
if (ex) {
ex.target = "#deploy-app-namespace-group";
fails.push(ex);
ex = null;
}
if (!file)
ex = new Error(_("No metadata file was selected. Please select a Kubernetes metadata file."));
else if (file.type && !file.type.match("json.*"))
ex = new Error(_("The selected file is not a valid Kubernetes application manifest."));
if (ex) {
ex.target = "#deploy-app-manifest-file-button";
fails.push(ex);
ex = null;
}
var reader;
if (fails.length) {
defer.reject(fails);
} else {
reader = new window.FileReader();
reader.onerror = function(event) {
ex = new Error(KubeFormat.format(_("Unable to read the Kubernetes application manifest. Code $0."),
event.target.error.code));
ex.target = "#deploy-app-manifest-file-button";
defer.reject(ex);
};
reader.onload = function() {
try {
defer.resolve({
objects : JSON.parse(reader.result),
namespace : ns
});
} catch (err) {
ex = new Error(KubeFormat.format(_("Unable to decode Kubernetes application manifest.")));
ex.target = "#deploy-app-manifest-file-button";
defer.reject(ex);
}
};
reader.readAsText(file);
}
return defer.promise;
}
function deploy_manifest() {
var defer = $q.defer();
validate_manifest().then(function(data) {
methods.create(data.objects, data.namespace)
.then(function() {
if ($scope.namespace && data.namespace != $scope.namespace)
filter.namespace(data.namespace);
defer.resolve();
})
.catch(function(response) {
var ex;
var resp = response.data;
/* Interpret this code as a conflict, so suggest user creates a new namespace */
if (response && response.code === 409) {
ex = new Error(KubeFormat.format(_("Please create another namespace for $0 \"$1\""),
response.details.kind, response.details.id));
ex.target = "#deploy-app-namespace-field";
} else {
ex = resp || response;
}
defer.reject(ex);
});
}, function(ex) {
defer.reject(ex);
});
return defer.promise;
}
$scope.types = [
{
name: _("Manifest"),
type: "manifest",
}
];
$scope.selected = $scope.types[0];
$scope.fields = fields;
$scope.namespaces = filter.namespaces();
$scope.namespace = filter.namespace();
$scope.$on("file", function(ev, newFile) {
$scope.$applyAsync(function() {
file = newFile;
fields.filename = file ? file.name : "";
});
});
$scope.performDeploy = function performDeploy() {
if ($scope.selected.type == 'manifest') {
return deploy_manifest();
}
};
$scope.select = function(type) {
$scope.selected = type;
};
}
])
.directive('fileButton', function() {
return {
templateUrl: 'views/file-button.html',
restrict: 'A',
link: function($scope, element, attributes) {
var button, file_input;
if (element[0].children.length == 2) {
button = element[0].children[1];
file_input = element[0].children[0];
button.onclick = function () {
file_input.click();
};
file_input.onchange = function () {
var files = file_input.files || [];
$scope.$emit('file', files[0]);
};
}
element.on('$destroy', function() {
if (file_input)
file_input.onchange = null;
if (button)
button.onclick = null;
});
}
};
});
}());

View File

@ -1,73 +0,0 @@
/*
* 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 moment = require('moment');
require('./kube-client');
angular.module('kubernetes.date', [
"kubeClient"
])
.factory('momentLib', [
function() {
return moment;
}
])
.factory('refreshEveryMin', [
"$rootScope",
"$window",
"kubeLoader",
function($rootScope, $window, loader) {
var last = 0;
var interval = 60000;
var tol = 500;
loader.listen(function() {
last = (new Date()).getTime();
});
$window.setInterval(function() {
var now = (new Date()).getTime();
if ((now - last) + tol >= interval)
$rootScope.$applyAsync();
last = now;
}, interval);
return {};
}
])
.filter('dateRelative', [
"refreshEveryMin",
function() {
function dateRelative(timestamp) {
if (!timestamp) {
return timestamp;
}
return moment(timestamp).fromNow();
}
dateRelative.$stateful = true;
return dateRelative;
}
]);
}());

View File

@ -1,736 +0,0 @@
/*
* 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');
require('object-describer/dist/object-describer.js');
require('kubernetes-object-describer/dist/object-describer.js');
require('angular-dialog.js');
require('./containers');
require('./date');
require('./kube-client');
require('./listing');
require('./utils');
require('./volumes');
require('../views/details-page.html');
require('../views/pod-container.html');
require('../views/details-page.html');
require('../views/item-delete.html');
require('../views/route-modify.html');
require('../views/replicationcontroller-modify.html');
require('../views/service-modify.html');
require('../views/deploymentconfig-body.html');
require('../views/replicationcontroller-pods.html');
require('../views/replicationcontroller-body.html');
require('../views/route-body.html');
require('../views/service-body.html');
require('../views/service-endpoint.html');
require('../views/pod-page.html');
require('../views/image-page.html');
require('../views/registry-dashboard-page.html');
require('../views/details-page.html');
require('../views/project-page.html');
require('../views/topology-page.html');
require('../views/node-page.html');
require('../views/dashboard-page.html');
require('../views/nodes-page.html');
require('../views/deploymentconfig-page.html');
require('../views/pv-page.html');
require('../views/container-page.html');
require('../views/service-page.html');
require('../views/group-page.html');
require('../views/containers-page.html');
require('../views/projects-page.html');
require('../views/user-page.html');
require('../views/images-page.html');
require('../views/replicationcontroller-page.html');
require('../views/route-page.html');
require('../views/imagestream-page.html');
require('../views/volumes-page.html');
function validItem(item, type) {
var valid = (item && (!type || item.kind === type) &&
item.spec && item.metadata);
var type_name = type || "Object";
if (!valid)
console.warn("Invalid " + type_name, item);
return valid;
}
function format_addresses_with_ports(addresses, ports) {
var text = addresses.join(", ");
if (ports && ports.length) {
text = text + ":" + ports.map(function(p) {
if (p.protocol === "TCP")
return p.port;
else
return p.port + "/" + p.protocol;
}).join(", ");
}
return text;
}
angular.module('kubernetes.details', [
'ngRoute',
'ui.cockpit',
'kubernetesUI',
'kubeClient',
'kubeUtils',
'kubernetes.listing',
'kubernetes.date',
'kubernetes.volumes',
'kubernetes.containers',
])
.config([
'$routeProvider',
function($routeProvider) {
$routeProvider
.when('/list/:namespace?', {
templateUrl: 'views/details-page.html',
controller: 'DetailsCtrl'
})
.when('/l/pods/:pod_namespace/:pod_name/:container_name', {
templateUrl: 'views/pod-container.html',
controller: 'ContainerCtrl',
})
.when('/l/:target_type/:target_namespace/:target', {
templateUrl: function (params) {
var re = /s$/;
var kind = params.target_type || "";
return 'views/' + kind.replace(re, "") + "-page.html";
},
controller: 'DetailCtrl',
resolve: {
'kindData': [
'kindData',
function (kindData) {
return kindData();
}
]
}
})
.when('/l/:target_type', {
templateUrl: 'views/details-page.html',
controller: 'DetailCtrl',
resolve: {
'kindData': [
'kindData',
function (kindData) {
return kindData();
}
]
}
});
}
])
.factory("kindData", [
"$q",
"$route",
"$location",
function($q, $route, $location) {
var typesToKinds = {
'services': 'Service',
'routes': 'Route',
'deploymentconfigs': 'DeploymentConfig',
'replicationcontrollers': 'ReplicationController',
'pods': 'Pod',
};
return function() {
var current = $route.current.params['target_type'];
var kind;
if (current)
kind = typesToKinds[current];
if (!kind) {
$location.path('/');
return $q.reject();
}
return $q.when({
'kind' : kind,
'type' : current,
});
};
}
])
.factory("detailsWatch", [
"kubeLoader",
"KubeDiscoverSettings",
function (loader, settings) {
return function(until) {
loader.watch("Pod", until);
loader.watch("Service", until);
loader.watch("ReplicationController", until);
loader.watch("Endpoints", until);
loader.watch("PersistentVolumeClaim", until);
settings().then(function(settings) {
if (settings.flavor === "openshift") {
loader.watch("DeploymentConfig", until);
loader.watch("Route", until);
}
});
};
}
])
.factory("detailsData", [
'kubeSelect',
'volumeData',
'KubeContainers',
"KubeTranslate",
function (select, volumeData, containers, translate) {
const _ = translate.gettext;
var names = {
'services': {
'name' : _("Services")
},
'routes': {
'name' : _("Routes"),
'flavor': "openshift"
},
'deploymentconfigs': {
'name': _("Deployment Configs"),
'flavor': "openshift"
},
'replicationcontrollers': {
'name' : _("Replication Controllers")
},
'pods': {
'name' : _("Pods")
}
};
function item_identifier(item) {
var meta = item.metadata || { };
var id = item.kind.toLowerCase() + "s/";
if (meta.namespace)
id = id + meta.namespace + "/";
return id + meta.name;
}
function service_endpoint(service) {
return select().kind("Endpoints")
.namespace(service.metadata.namespace)
.name(service.metadata.name)
.one();
}
function replicationcontroller_pods(item) {
var meta = item.metadata || {};
var spec = item.spec || {};
return select().kind("Pod")
.namespace(meta.namespace || "")
.label(spec.selector || {});
}
function podStatus(item) {
var status = item.status || {};
var meta = item.metadata || {};
if (meta.deletionTimestamp)
return "Terminating";
else
return status.phase;
}
return {
itemIdentifier: item_identifier,
serviceEndpoint: service_endpoint,
replicationcontrollerPods: replicationcontroller_pods,
podStatus: podStatus,
volumesForPod: volumeData.volumesForPod,
claimFromVolumeSource: volumeData.claimFromVolumeSource,
containers: containers,
names: names
};
}
])
/*
* The controller for the details view.
*/
.controller('DetailsCtrl', [
'$scope',
'kubeLoader',
'kubeSelect',
'KubeDiscoverSettings',
'ListingState',
'$location',
'itemActions',
'detailsData',
'detailsWatch',
function($scope, loader, select, discoverSettings, ListingState,
$location, actions, detailsData, detailsWatch) {
loader.listen(function() {
$scope.pods = select().kind("Pod");
$scope.services = select().kind("Service");
$scope.replicationcontrollers = select().kind("ReplicationController");
$scope.deploymentconfigs = select().kind("DeploymentConfig");
$scope.routes = select().kind("Route");
}, $scope);
detailsWatch($scope);
$scope.listing = new ListingState($scope);
$scope.showAll = true;
$scope.$on("activate", function(ev, id) {
ev.preventDefault();
actions.navigate(id);
});
/* All the data and actions available on the $scope */
angular.extend($scope, detailsData);
angular.extend($scope, actions);
}
])
.controller('DetailCtrl', [
'$scope',
'kindData',
'kubeLoader',
'kubeSelect',
'ListingState',
'$routeParams',
'$location',
'itemActions',
'detailsData',
'detailsWatch',
function($scope, kindData, loader, select, ListingState, $routeParams,
$location, actions, detailsData, detailsWatch) {
var target = $routeParams["target"] || "";
$scope.target = target;
$scope.name = detailsData.names[kindData.type].name;
loader.listen(function() {
if (kindData.type)
$scope[kindData.type] = select().kind(kindData.kind);
if (target && $routeParams.target_namespace) {
$scope.item = select().kind(kindData.kind)
.namespace($routeParams.target_namespace)
.name(target)
.one();
}
}, $scope);
detailsWatch($scope);
$scope.listing = new ListingState($scope);
$scope.listing.inline = true;
$scope.$on("activate", function(ev, id) {
ev.preventDefault();
actions.navigate(id);
});
/* All the data and actions available on the $scope */
angular.extend($scope, detailsData);
angular.extend($scope, actions);
angular.extend($scope, kindData);
}
])
.directive('kubernetesServiceCluster', function() {
return {
restrict: 'E',
link: function($scope, element, attributes) {
$scope.$watchGroup(["item.spec.clusterIP",
"item.spec.ports"], function(values) {
var text = format_addresses_with_ports([values[0]],
values[1]);
element.text(text);
});
}
};
})
.factory('itemActions', [
'$modal',
'$location',
function($modal, $location) {
function deleteItem(item) {
return $modal.open({
animation: false,
controller: 'ItemDeleteCtrl',
templateUrl: 'views/item-delete.html',
resolve: {
dialogData: function() {
return { item: item };
}
},
}).result;
}
function modifyRoute(item) {
return $modal.open({
animation: false,
controller: 'RouteModifyCtrl',
templateUrl: 'views/route-modify.html',
resolve: {
dialogData: function() {
return { item: item };
}
},
}).result;
}
function modifyRC(item) {
return $modal.open({
animation: false,
controller: 'RCModifyCtrl',
templateUrl: 'views/replicationcontroller-modify.html',
resolve: {
dialogData: function() {
return { item: item };
}
},
}).result;
}
function modifyService(item) {
return $modal.open({
animation: false,
controller: 'ServiceModifyCtrl',
templateUrl: 'views/service-modify.html',
resolve: {
dialogData: function() {
return { item: item };
}
},
}).result;
}
function navigate(path) {
var prefix = '/l';
path = path || "";
if (!path)
prefix = "/list";
if (path && path.indexOf('/') !== 0)
prefix = prefix + '/';
$location.path(prefix + path);
}
return {
modifyRC: modifyRC,
modifyRoute: modifyRoute,
deleteItem: deleteItem,
modifyService: modifyService,
navigate: navigate,
};
}
])
.controller("ItemDeleteCtrl", [
"$scope",
"$modalInstance",
"dialogData",
"kubeMethods",
function($scope, $instance, dialogData, methods) {
angular.extend($scope, dialogData);
$scope.performDelete = function performDelete() {
return methods.delete($scope.item).catch(function(ex) {
/* HACK: While debugging delete issues */
console.log(JSON.stringify(ex));
});
};
}
])
.controller("RCModifyCtrl", [
"$q",
"$scope",
"$modalInstance",
"dialogData",
"kubeMethods",
"KubeTranslate",
function($q, $scope, $instance, dialogData, methods, translate) {
const _ = translate.gettext;
var item = dialogData.item;
var fields = {};
if (!validItem(item, "ReplicationController")) {
$scope.$applyAsync(function () {
$scope.$dismiss();
});
return;
}
fields.replicas = item.spec ? item.spec.replicas : 1;
function validate() {
var defer = $q.defer();
var replicas = Number(fields.replicas.trim());
var ex;
if (isNaN(replicas) || replicas < 0)
ex = new Error(_("Not a valid number of replicas"));
else if (replicas > 128)
ex = new Error(_("The maximum number of replicas is 128"));
if (ex) {
ex.target = "#replicas";
defer.reject(ex);
} else {
defer.resolve({ spec: { replicas: replicas } });
}
return defer.promise;
}
$scope.fields = fields;
$scope.item = item;
$scope.performModify = function performModify() {
return validate().then(function(data) {
return methods.patch($scope.item, data);
});
};
}
])
.controller("RouteModifyCtrl", [
"$q",
"$scope",
"$modalInstance",
"dialogData",
"kubeMethods",
"KubeTranslate",
function($q, $scope, $instance, dialogData, methods, translate) {
const _ = translate.gettext;
var fields = {};
if (!validItem(dialogData.item, "Route")) {
$scope.$applyAsync(function () {
$scope.$dismiss();
});
return;
}
fields.host = dialogData.item.spec.host;
function validate() {
var defer = $q.defer();
var host = fields.host.trim();
var ex;
if (!host) {
ex = new Error(_("Not a valid value for Host"));
ex.target = "#host-value";
defer.reject(ex);
} else {
defer.resolve({ spec: { host: fields.host.trim() } });
}
return defer.promise;
}
$scope.fields = fields;
angular.extend($scope, dialogData);
$scope.performModify = function performModify() {
return validate().then(function(data) {
return methods.patch($scope.item, data);
});
};
}
])
.controller("ServiceModifyCtrl", [
"$q",
"$scope",
"$modalInstance",
"dialogData",
'kubeLoader',
'kubeSelect',
"KubeRequest",
"KubeTranslate",
"KubeFormat",
function($q, $scope, $instance, dialogData, loader, select, KubeRequest, translate, format) {
const _ = translate.gettext;
var fields = {};
var key;
if (!validItem(dialogData.item, "Service")) {
$scope.$applyAsync(function () {
$scope.$dismiss();
});
return;
}
$scope.rcs = select().kind("ReplicationController")
.namespace(dialogData.item.metadata.namespace || "")
.label(dialogData.item.spec.selector || {});
for (key in $scope.rcs) {
var item = $scope.rcs[key];
fields[key] = {
name: item.metadata.name,
replicas: item.spec.replicas,
};
}
$scope.service = dialogData.item;
$scope.fields = fields;
angular.extend($scope, dialogData);
function validate() {
var defer = $q.defer();
var link;
var objects = [];
var failures = [];
for (link in fields) {
var ex;
var replicas = Number(fields[link].replicas);
var name = fields[link].name;
if (isNaN(replicas) || replicas < 0)
ex = new Error(_("Not a valid number of replicas"));
else if (replicas > 128)
ex = new Error(_("The maximum number of replicas is 128"));
if (ex) {
ex.target = "#" + name;
failures.push(ex);
} else {
objects.push({
link: link,
name: name,
data: { spec: { replicas: replicas } }
});
}
}
if (failures.length > 0) {
defer.reject(failures);
} else
defer.resolve(objects);
return defer.promise;
}
$scope.performModify = function performModify() {
var defer = $q.defer();
var req;
validate().then(function (objects) {
function step() {
var obj = objects.shift();
if (!obj) {
defer.resolve();
return;
}
defer.notify(format.format(_("Updating $0..."), obj.name));
var config = { headers: { "Content-Type": "application/strategic-merge-patch+json" } };
new KubeRequest("PATCH", obj.link, JSON.stringify(obj.data), config)
.then(function(response) {
step();
}, function(response) {
var resp = response.data;
return defer.reject(resp || response);
});
}
step();
})
.catch(function(exs) {
defer.reject(exs);
});
var promise = defer.promise;
promise.cancel = function cancel() {
if (req && req.cancel)
req.cancel();
};
return promise;
};
}
])
.directive('deploymentconfigBody',
function() {
return {
restrict: 'A',
templateUrl: 'views/deploymentconfig-body.html'
};
}
)
.directive('replicationcontrollerPods',
function() {
return {
restrict: 'A',
templateUrl: 'views/replicationcontroller-pods.html'
};
}
)
.directive('replicationcontrollerBody',
function() {
return {
restrict: 'A',
templateUrl: 'views/replicationcontroller-body.html'
};
}
)
.directive('routeBody',
function() {
return {
restrict: 'A',
templateUrl: 'views/route-body.html'
};
}
)
.directive('serviceBody',
function() {
return {
restrict: 'A',
templateUrl: 'views/service-body.html'
};
}
)
.directive('serviceEndpoint',
function() {
return {
restrict: 'A',
templateUrl: 'views/service-endpoint.html'
};
}
);
}());

View File

@ -1,233 +0,0 @@
export const FIXTURE_BASIC = {
"nodes/127.0.0.1": {
"kind": "Node",
"metadata": {
"name": "127.0.0.1",
"uid": "f530580d-a169-11e4-8651-10c37bdb8410",
"creationTimestamp": "2015-01-21T13:35:18+01:00",
"resourceVersion": 1,
},
"spec": {
"capacity": {
"cpu": "1k",
"memory": "3Gi",
}
},
"status": {
"hostIP": "127.0.0.1",
"conditions": [
{
"kind": "Ready",
"status": "Full",
"lastTransitionTime": null
}
]
}
},
"namespaces/default/pods/database-1": {
"kind": "Pod",
"metadata": {
"name": "wordpress",
"resourceVersion": 5,
"uid": "0b547d64-ab8a-11e4-9a7c-080027300d85",
"namespace": "default",
"labels": {
"name": "wordpressreplica"
},
},
"spec": {
"volumes": null,
"containers": [
{
"name": "slave",
"image": "jbfink/wordpress",
"ports": [
{
"hostPort": 81,
"containerPort": 80,
"protocol": "TCP"
}
],
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": {
"always": {}
},
"dnsPolicy": "ClusterFirst",
"nodeName": "127.0.0.1"
},
"status": {
"phase": "Running",
"conditions": [
{
"kind": "Ready",
"status": "Full"
}
],
"hostIP": "127.0.0.1",
"podIP": "172.17.4.173",
"info": {
"POD": {
"state": {
"running": {
"startedAt": "2015-02-13T16:21:35Z"
}
},
"ready": false,
"restartCount": 0,
"containerID": "docker://9031b6aef7829ec029955377bd53642760899d4eed37738830756d0ce092a01d",
"podIP": "172.17.4.173",
"image": "kubernetes/pause:0.8.0",
"imageID": "docker://6c4579af347b649857e915521132f15a06186d73faa62145e3eeeb6be0e97c27"
},
"slave": {
"state": {
"running": {
"startedAt": "2015-02-13T16:27:49Z"
}
},
"ready": true,
"restartCount": 0,
"containerID": "docker://dc70bd24ecc7fd86a385d67bdbc2a60b219cf34fdd215f8f599c95ba93b1a82b",
"image": "jbfink/wordpress",
"imageID": "docker://0beee7f478c860c8444aa6a3966e1cb0cd574a01c874fc5dcc48585bd45dba52"
}
}
}
},
"namespaces/default/pods/apache": {
"kind": "Pod",
"metadata": {
"name": "apache",
"uid": "11768037-ab8a-11e4-9a7c-080027300d85",
"resourceVersion": 5,
"namespace": "default",
"labels": {
"name": "apache"
},
},
"spec": {
"volumes": null,
"containers": [
{
"name": "slave",
"image": "fedora/apache",
"ports": [
{
"hostPort": 8084,
"containerPort": 80,
"protocol": "TCP"
}
],
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": {
"always": {}
},
"dnsPolicy": "ClusterFirst"
},
},
"namespaces/other/pods/apache": {
"kind": "Pod",
"metadata": {
"name": "apache",
"uid": "9f1a316f-4db6-11e5-971a-525400e58104",
"resourceVersion": 5,
"namespace": "other",
"labels": {
"name": "apache"
},
},
"spec": {
"volumes": null,
"containers": [
{
"name": "slave",
"image": "fedora/apache",
"ports": [
{
"hostPort": 8084,
"containerPort": 80,
"protocol": "TCP"
}
],
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": {
"always": {}
},
"dnsPolicy": "ClusterFirst"
},
},
"namespaces/default/services/kubernetes": {
"kind": "Service",
"metadata": {
"name": "kubernetes",
"namespace": "default",
"uid": "9750385b-7fa4-11e4-91e3-10c37bdb8410",
"resourceVersion": "15",
},
"spec": {
"port": 443,
"protocol": "TCP",
"selector": {
"component": "apiserver",
"provider": "kubernetes"
},
"clusterIP": "10.254.224.238",
"containerPort": 0,
"sessionAffinity": "None"
},
"status": {}
},
"namespaces/default/services/kubernetes-ro": {
"kind": "Service",
"apiVersion": "v1",
"metadata": {
"name": "kubernetes-ro",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/services/kubernetes-ro",
"uid": "97504104-7fa4-11e4-91e3-10c37bdb8410",
"resourceVersion": "16",
},
"spec": {
"port": 80,
"protocol": "TCP",
"selector": {
"component": "apiserver",
"provider": "kubernetes"
},
"clusterIP": "10.254.117.100",
"containerPort": 0,
"sessionAffinity": "None"
},
"status": {}
},
"namespaces/default/imagestreams/mock-image-stream": {
"kind": "ImageStream",
"apiVersion": "v1",
"metadata": {
"name": "mock-image-stream",
"namespace":"default",
"uid":"c216455b-4cc5-11e5-8a7f-0e5582eacc27"
},
"spec": {
"dockerImageRepository": "mock/image",
"tags": [
{
"name": "latest",
"annotations": {
"description": "Mock Image",
"iconClass": "icon-mock",
"tags": "builder,mock",
"version": "3.0"
}
}
]
},
"status": {}
},
};

File diff suppressed because it is too large Load Diff

View File

@ -1,76 +0,0 @@
#!/usr/bin/env node
/*
* This is used to generate data sets like the one in mock-large.js
* Use it like so:
*
* $ ./frob-mock.js > ./mock-large.js
*/
var last = 99999999;
function uid() {
last += 1;
return "11768037-ab8a-11e4-9a7c-" + last;
}
var pod_template = {
"kind": "Pod",
"metadata": {
"labels": {
"name": "mock-",
"number": "",
"tag": "silly"
}
}
};
var objects = {};
var pod, i;
for (i = 0; i < 1000; i++) {
pod = JSON.parse(JSON.stringify(pod_template));
pod.metadata.name += i;
pod.metadata.resourceVersion += i;
pod.metadata.labels.name += i;
pod.metadata.labels.number += i;
pod.metadata.labels.type = ["even", "odd"][i % 2];
pod.metadata.labels.factor3 = ["yes", "no", "no"][i % 3];
objects["namespaces/default/pods/mock-" + i] = pod;
}
var repl_template = {
"kind": "ReplicationController",
"metadata": {
"labels": {
"example": "mock"
}
},
"spec": {
"replicas": 1,
"selector": { },
}
};
var repl = JSON.parse(JSON.stringify(repl_template));
repl.metadata.name = "oddcontroller";
repl.spec.selector = { "tag": "silly", "type": "odd" };
objects["namespaces/default/replicationcontrollers/oddcontroller"] = repl;
repl = JSON.parse(JSON.stringify(repl_template));
repl.metadata.name = "3controller";
repl.spec.selector = { "factor3": "yes" };
objects["namespaces/default/replicationcontrollers/3controller"] = repl;
var path, parts;
for (path in objects) {
parts = path.split("/");
objects[path].metadata.resourceVersion = 10000;
objects[path].metadata.uid = uid();
objects[path].metadata.namespace = parts[1];
objects[path].metadata.name = parts.reverse()[0];
objects[path].metadata.labels.name = objects[path].metadata.name;
}
var data = JSON.stringify(objects, null, 4);
process.stdout.write("define(" + data + ");\n");

File diff suppressed because it is too large Load Diff

View File

@ -1,863 +0,0 @@
/*
* 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 d3 = require('d3');
require('./kube-client');
require('./kube-client-cockpit');
require('./utils');
angular.module('kubernetes.graph', [
'kubeClient',
'kubeClient.cockpit',
'kubeUtils',
])
.factory('CAdvisorSeries', [
"KubeRequest",
"CockpitMetrics",
"$exceptionHandler",
"$timeout",
function (KubeRequest, CockpitMetrics, $exceptionHandler, $timeout) {
function CAdvisor(node) {
var self = this;
/* called when containers changed */
var callbacks = [];
/* cAdvisor has 10 second intervals */
var interval = 10000;
var last = { };
var requests = { };
/* Holds the container specs */
self.specs = { };
function feed(containers) {
var x, y, ylen, i, len;
var item, offset, timestamp, container, stat;
/*
* The cAdvisor data doesn't seem to have inherent guarantees of
* continuity or regularity. In theory each stats object can have
* it's own arbitrary timestamp ... although in practice they do
* generally follow the interval to within a few milliseconds.
*
* So we first look for the lowest timestamp, treat that as our
* base index, and then batch the data based on that.
*/
var first = null;
for (x in containers) {
container = containers[x];
if (container.stats) {
len = container.stats.length;
for (i = 0; i < len; i++) {
timestamp = container.stats[i].timestamp;
if (timestamp) {
if (first === null || timestamp < first)
first = timestamp;
}
}
}
}
if (first === null)
return;
var base = Math.floor(new Date(first).getTime() / interval);
var items = [];
var name;
var mapping = { };
var new_ids = [];
var id;
var names = { };
for (x in containers) {
container = containers[x];
/*
* This builds the correct type of object graph for the
* paths seen in grid.add() to operate on
*/
name = container.name;
if (!name)
continue;
names[name] = name;
mapping[name] = { "": name };
id = name;
if (container.aliases) {
ylen = container.aliases.length;
for (y = 0; y < ylen; y++) {
mapping[container.aliases[y]] = { "": name };
/* Try to use the real docker container id as our id */
if (container.aliases[y].length === 64)
id = container.aliases[y];
}
}
if (id && container.spec) {
if (!self.specs[id]) {
self.specs[id] = container.spec;
new_ids.push(id);
}
}
if (container.stats) {
len = container.stats.length;
for (i = 0; i < len; i++) {
stat = container.stats[i];
if (!stat.timestamp)
continue;
/* Convert the timestamp into an index */
offset = Math.floor(new Date(stat.timestamp).getTime() / interval);
item = items[offset - base];
if (!item)
item = items[offset - base] = { };
item[name] = stat;
}
}
}
if (new_ids.length > 0)
invokeCallbacks(new_ids);
/* Make sure each offset has something */
len = items.length;
for (i = 0; i < len; i++) {
if (items[i] === undefined)
items[i] = { };
}
/* Now for each offset, if it's a duplicate, put in a copy */
for (name in names) {
len = items.length;
for (i = 0; i < len; i++) {
if (items[i][name] === undefined)
items[i][name] = last[name];
else
last[name] = items[i][name];
}
}
self.series.input(base, items, mapping);
}
function request(query) {
var body = JSON.stringify(query);
/* Only one request active at a time for any given body */
if (body in requests)
return;
var path = "/api/v1/proxy/nodes/" + encodeURIComponent(node) + ":4194/api/v1.2/docker";
var req = KubeRequest("POST", path, query);
requests[body] = req;
req.then(function(data) {
delete requests[body];
feed(data.data);
})
.catch(function(ex) {
delete requests[body];
if (ex.status != 503)
console.warn(ex);
});
}
function invokeCallbacks(/* ... */) {
var i, len, func;
for (i = 0, len = callbacks.length; i < len; i++) {
func = callbacks[i];
try {
if (func)
func.apply(self, arguments);
} catch (e) {
$exceptionHandler(e);
}
}
}
self.fetch = function fetch(beg, end) {
var query;
if (!beg || !end) {
query = { num_stats: 60 };
} else {
query = {
start: new Date((beg - 1) * interval).toISOString(),
end: new Date(end * interval).toISOString()
};
}
request(query);
};
self.close = function close() {
var req, body;
for (body in requests) {
req = requests[body];
if (req && req.cancel)
req.cancel();
}
};
self.watch = function watch(callback) {
var ids;
var timeout;
callbacks.push(callback);
if (self.specs) {
ids = Object.keys(self.specs);
if (ids.length > 0) {
timeout = $timeout(function() {
timeout = null;
callback.call(self, ids);
}, 0);
}
}
return {
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;
}
}
};
};
var cache = "cadv1-" + (node || null);
self.series = CockpitMetrics.series(interval, cache, self.fetch);
}
return {
new_cadvisor: function(node) {
return new CAdvisor(node);
},
};
}
])
.factory('ServiceGrid', [
"CAdvisorSeries",
"CockpitMetrics",
"kubeSelect",
"kubeLoader",
function ServiceGrid(CAdvisorSeries, CockpitMetrics, select, loader) {
function CockpitServiceGrid(until) {
var self = CockpitMetrics.grid(10000, 0, 0);
/* All the cadvisors that have been opened, one per host */
var cadvisors = { };
/* Service uids */
var services = { };
/* The various rows being shown, override */
self.rows = [ ];
self.events = [ ];
var change_queued = false;
var current_metric = null;
var rows = {
cpu: { },
memory: { },
network: { }
};
var container_cpu = { };
var container_mem = { };
var container_rx = { };
var container_tx = { };
/* Track Pods and Services */
loader.listen(function() {
var changed = false;
var seen_services = {};
var seen_hosts = {};
/* Lookup all the services */
angular.forEach(select().kind("Service"), function(service) {
var spec = service.spec;
var meta = service.metadata;
var name = meta.name;
if (!spec || !spec.selector || name === "kubernetes" || name === "kubernetes-ro")
return;
var uid = meta.uid;
var pods = select().kind("Pod")
.namespace(meta.namespace || "")
.label(spec.selector || {});
seen_services[uid] = true;
if (!services[uid])
add_service(uid);
/* Lookup all the pods for each service */
angular.forEach(pods, function(pod) {
var status = pod.status || { };
var spec = pod.spec || { };
var host = spec.nodeName;
var container_ids = {};
var containers = status.containerStatuses || [];
var i;
var mapped = services[uid];
seen_hosts[host] = true;
if (host && !cadvisors[host]) {
add_cadvisor(host);
}
/* Note all the containers for that pod */
for (i = 0; i < containers.length; i++) {
var container = containers[i];
var id = container.containerID;
if (id && id.indexOf("docker://") === 0) {
container_ids[id] = id;
id = id.substring(9);
container_ids[id] = id;
if (!mapped || !mapped[id])
changed = true;
}
}
services[uid] = container_ids;
});
});
var k;
for (k in services) {
if (!seen_services[k]) {
remove_service(k);
changed = true;
}
}
for (k in cadvisors) {
if (!seen_hosts[k]) {
remove_host(k);
changed = true;
}
}
/* Notify for all rows */
if (changed)
self.sync();
}, until);
loader.watch("Pod", until);
loader.watch("Service", until);
function add_container(cadvisor, id) {
var cpu = self.add(cadvisor, [ id, "cpu", "usage", "total" ]);
container_cpu[id] = self.add(function(row, x, n) {
row_delta(cpu, row, x, n);
}, true);
container_mem[id] = self.add(cadvisor, [ id, "memory", "usage" ]);
var rx = self.add(cadvisor, [ id, "network", "rx_bytes" ]);
container_rx[id] = self.add(function(row, x, n) {
row_delta(rx, row, x, n);
}, true);
var tx = self.add(cadvisor, [ id, "network", "tx_bytes" ]);
container_tx[id] = self.add(function(row, x, n) {
row_delta(tx, row, x, n);
}, true);
self.sync();
}
function add_cadvisor(host) {
var cadvisor = CAdvisorSeries.new_cadvisor(host);
cadvisor.watch(function (ids) {
var i;
for (i = 0; i < ids.length; i++)
add_container(cadvisor, ids[i]);
});
/* A dummy row to force fetching data from the cadvisor */
self.add(cadvisor, [ "unused-dummy" ]);
/* TODO: Handle cadvisor failure somehow */
cadvisors[host] = cadvisor;
}
function remove_host(host) {
var cadvisor = cadvisors[host];
if (cadvisor) {
delete cadvisors[host];
cadvisor.close();
cadvisor = null;
change_queued = true;
}
}
function add_service(uid) {
/* CPU needs summing of containers, and then delta between them */
rows.cpu[uid] = self.add(function(row, x, n) {
containers_sum(uid, container_cpu, row, x, n);
});
/* Memory row is pretty simple, just sum containers */
rows.memory[uid] = self.add(function(row, x, n) {
containers_sum(uid, container_mem, row, x, n);
});
/* Network sums containers, then sum tx and rx, and then delta */
var tx = self.add(function(row, x, n) {
containers_sum(uid, container_tx, row, x, n);
});
var rx = self.add(function(row, x, n) {
containers_sum(uid, container_rx, row, x, n);
});
rows.network[uid] = self.add(function(row, x, n) {
rows_sum([tx, rx], row, x, n);
});
change_queued = true;
}
function remove_service(uid) {
delete services[uid];
delete rows.network[uid];
delete rows.cpu[uid];
delete rows.memory[uid];
change_queued = true;
}
function rows_sum(input, row, x, n) {
var max = row.maximum || 0;
var value, i, v, j;
var len = input.length;
/* Calculate the sum of the rows */
for (i = 0; i < n; i++) {
value = undefined;
for (j = 0; j < len; j++) {
v = input[j][x + i];
if (v !== undefined) {
if (value === undefined)
value = v;
else
value += v;
}
}
if (value !== undefined && value > max) {
row.maximum = max = value;
change_queued = true;
}
row[x + i] = value;
}
}
function row_delta(input, row, x, n) {
var i, last, res, value;
if (x > 0)
last = input[x - 1];
var max = row.maximum || 1;
for (i = 0; i < n; i++) {
value = input[x + i];
if (last === undefined || value === undefined) {
res = undefined;
} else {
res = (value - last);
if (res < 0) {
res = undefined;
} else if (res > max) {
row.maximum = max = res;
change_queued = true;
}
}
row[x + i] = res;
last = value;
}
}
function containers_sum(service, input, row, x, n) {
var id, rowc;
var subset = [];
var mapped = services[service];
if (mapped) {
for (id in mapped) {
rowc = input[id];
if (rowc)
subset.push(rowc);
}
}
rows_sum(subset, row, x, n);
}
self.metric = function metric(type) {
if (type === undefined)
return current_metric;
if (rows[type] === undefined)
throw Error("unsupported metric type");
self.rows = [];
current_metric = type;
var service_uids = Object.keys(services);
var row, i;
var len = service_uids.length;
for (i = 0; i < len; i++) {
row = rows[type][service_uids[i]];
if (row !== undefined) {
self.rows.push(row);
row.uid = service_uids[i];
}
}
var event = new CustomEvent("changed", {
bubbles: false,
cancelable: false,
detail: null
});
self.dispatchEvent(event, null);
};
var base_close = self.close;
self.close = function close() {
var hosts = Object.keys(cadvisors);
var i;
for (i = 0; i < hosts.length; i++) {
var k = hosts[i];
var cadvisor = cadvisors[k];
if (cadvisor) {
delete cadvisors[k];
cadvisor.close();
cadvisor = null;
}
}
base_close.apply(self);
};
self.addEventListener("notify", function () {
if (change_queued) {
change_queued = false;
self.metric(current_metric);
}
});
return self;
}
return {
new_grid: function (until) {
return new CockpitServiceGrid(until);
}
};
}
])
.directive('kubernetesServiceGraph', [
"ServiceGrid",
"KubeTranslate",
"KubeFormat",
function kubernetesServiceGraph(ServiceGrid, KubeTranslate, KubeFormat) {
const _ = KubeTranslate.gettext;
function service_graph($scope, selector, highlighter) {
var grid = ServiceGrid.new_grid($scope);
var outer = d3.select(selector);
var highlighted = null;
/* Various tabs */
var tabs = {
cpu: {
label: _("CPU"),
step: 1000 * 1000 * 1000 * 10,
formatter: function(v) { return (v / (100 * 1000 * 1000)) + "%" }
},
memory: {
label: _("Memory"),
step: 1024 * 1024 * 64,
formatter: function(v) { return KubeFormat.formatBytes(v) }
},
network: {
label: _("Network"),
step: 1000 * 1000 * 10,
formatter: function(v) { return KubeFormat.formatBitsPerSec((v / 10), "Mbps") }
}
};
outer.append("ul")
.attr("class", "nav nav-tabs")
.selectAll("li")
.data(Object.keys(tabs))
.enter()
.append("li")
.attr("data-metric", function(d) { return d })
.append("a")
.text(function(d) { return tabs[d].label });
function metric_tab(tab) {
outer.selectAll("ul li")
.attr("class", function(d) { return tab === d ? "active" : null });
grid.metric(tab);
}
outer.selectAll("ul li")
.on("click", function() {
metric_tab(d3.select(this).attr("data-metric"));
});
metric_tab("cpu");
/* The main svg graph stars here */
var margins = {
top: 12,
right: 15,
bottom: 40,
left: 60
};
var colors = d3.scale.category20();
var element = d3.select(selector).append("svg");
var stage = element.append("g")
.attr("transform", "translate(" + margins.left + "," + margins.top + ")");
var y = d3.scale.linear();
var y_axis = d3.svg.axis()
.scale(y)
.ticks(5)
.orient("left");
var y_group = stage.append("g")
.attr("class", "y axis");
var x = d3.scale.linear();
var x_axis = d3.svg.axis()
.scale(x)
.orient("bottom");
var x_group = stage.append("g")
.attr("class", "x axis");
var offset = 0;
var line = d3.svg.line()
.defined(function(d) { return d !== undefined })
.x(function(d, i) { return x((grid.beg + i) - offset) })
.y(function(d, i) { return y(d) });
/* Initial display: 1024 px, 5 minutes of data */
var factor = 300000 / 1024;
var width = 300;
var height = 300;
var rendered = false;
window.setTimeout(function() {
rendered = true;
adjust();
}, 1);
function ceil(value, step) {
var d = value % step;
if (value === 0 || d !== 0)
value += (step - d);
return value;
}
function jump() {
var interval = grid.interval;
var w = (width - margins.right) - margins.left;
/* This doesn't yet work for an arbitary ponit in time */
var now = new Date().getTime();
var end = Math.floor(now / interval);
var beg = end - Math.floor((factor * w) / interval);
offset = beg;
grid.move(beg, end);
}
function adjust() {
if (!rendered)
return;
element
.attr("width", width)
.attr("height", height);
var w = (width - margins.right) - margins.left;
var h = (height - margins.top) - margins.bottom;
var metric = grid.metric();
var interval = grid.interval;
/* Calculate our maximum value, hopefully rows are tracking this for us */
var rows = grid.rows;
var maximum = 0;
var i, max;
var len = rows.length;
for (i = 0; i < len; i++) {
if (rows[i].maximum !== undefined)
max = rows[i].maximum;
else
max = d3.max(rows[i]);
if (max > maximum)
maximum = Math.ceil(max);
}
/* This doesn't yet work for an arbitary ponit in time */
var end = Math.floor((factor * w) / interval);
x.domain([0, end]).range([0, w]);
y.domain([0, ceil(maximum, tabs[metric].step)]).range([h, 0]);
/* The ticks are inverted backwards */
var tsc = d3.scale.linear().domain([0, end])
.range([end, 0]);
/* Calculate ticks every 60 seconds in past */
var ticks = [];
for (i = 6; i < end; i += 6)
ticks.push(Math.round(tsc(i)));
/* Make x-axis ticks into grid of right width */
x_axis
.tickValues(ticks)
.tickSize(-h, -h)
.tickFormat(function(d) {
d = Math.round(tsc.invert(d));
return (d / 6) + " min";
});
/* Re-render the X axis. Note that we also
* bump down the labels a bit. */
x_group
.attr("transform", "translate(0," + h + ")")
.call(x_axis)
.selectAll("text")
.attr("y", "10px");
/* Turn the Y axis ticks into a grid */
y_axis
.tickSize(-w, -w)
.tickFormat(tabs[metric].formatter);
y_group
.call(y_axis)
.selectAll("text")
.attr("x", "-10px");
jump();
}
function notified() {
var rows = grid.rows;
var series = stage.selectAll("path.line")
.data(rows, function(d, i) { return i });
series
.style("stroke", function(d, i) { return colors(i) })
.attr("d", function(d) { return line(d) })
.classed("highlight", function(d) { return d.uid === highlighted });
series.enter().append("path")
.attr("class", "line")
.on("mouseover", function() {
highlighter(d3.select(this).datum().uid);
})
.on("mouseout", function() {
highlighter(null);
});
series.exit().remove();
}
grid.addEventListener('notify', notified);
function changed() {
adjust();
notified();
}
grid.addEventListener('changed', changed);
function resized() {
width = selector.offsetWidth - 10;
if (width < 0)
width = 0;
adjust();
}
window.addEventListener('resize', resized);
resized();
var timer = window.setInterval(function () {
if (!width)
resized();
else
jump();
}, grid.interval);
return {
highlight: function highlight(uid) {
highlighted = uid;
notified();
},
close: function close() {
if (timer)
window.clearInterval(timer);
timer = null;
window.removeEventListener('resize', resized);
grid.removeEventListener('notify', notified);
grid.removeEventListener('changed', changed);
grid.close();
}
};
}
return {
restrict: 'E',
link: function($scope, element, attributes) {
var graph = service_graph($scope, element[0], function(uid) {
$scope.$broadcast('highlight', uid);
$scope.$digest();
});
$scope.$on("highlight", function(ev, uid) {
graph.highlight(uid);
});
element.on('$destroy', function() {
graph.close();
graph = null;
});
}
};
}
]);
}());

View File

@ -1,723 +0,0 @@
/*
* 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');
require("angular-route");
require('angular-dialog.js');
require('./kube-client');
require('./date');
require('./tags');
require('./policy');
require('registry-image-widgets/dist/image-widgets.js');
require('../views/images-page.html');
require('../views/imagestream-page.html');
require('../views/image-page.html');
require('../views/imagestream-delete.html');
require('../views/imagestream-modify.html');
require('../views/imagestream-modify.html');
require('../views/image-delete.html');
/*
* Executes callback for each stream.status.tag[x].item[y]
* in a stream. Similar behavior to angular.forEach()
*/
function imagestreamEachTagItem(stream, callback, context) {
var i, il, items;
var t, tl;
var tags = (stream.status || {}).tags || [];
for (t = 0, tl = tags.length; t < tl; t++) {
items = (tags[t].items) || [];
for (i = 0, il = items.length; i < il; i++)
callback.call(context || null, tags[t], items[i]);
}
}
function identifier(imagestream, tag) {
var id = imagestream.metadata.namespace + "/" + imagestream.metadata.name;
if (tag)
id += ":" + tag.name;
return id;
}
angular.module('registry.images', [
'ngRoute',
'ui.cockpit',
'kubeClient',
'kubernetes.date',
'registry.tags',
'registryUI.images',
])
.config([
'$routeProvider',
function($routeProvider) {
$routeProvider
.when('/images/:namespace?', {
templateUrl: 'views/images-page.html',
controller: 'ImagesCtrl'
})
.when('/images/:namespace/:target', {
controller: 'ImageCtrl',
templateUrl: function(params) {
var target = params['target'] || '';
if (target.indexOf(':') === -1)
return 'views/imagestream-page.html';
else
return 'views/image-page.html';
}
});
}
])
.factory('registryListingScopeSetup', [
'imageData',
'imageActions',
'projectData',
'kubeSelect',
'$location',
function (data, actions, projectData, select, $location) {
return function($scope, inPage) {
function imageByTag (tag) {
if (tag && tag.items && tag.items.length)
return select().kind("Image")
.name(tag.items[0].image)
.one();
}
function deleteImageStream(stream) {
var promise = actions.deleteImageStream(stream);
/* If the promise is successful, redirect to another page */
promise.then(function() {
$location.path($scope.viewUrl('images'));
});
return promise;
}
function deleteTag(stream, tag) {
var promise = actions.deleteTag(stream, tag);
/* If the promise is successful, redirect to another page */
promise.then(function() {
var parts = [ "images", stream.metadata.namespace, stream.metadata.name ];
$location.path("/" + parts.map(encodeURIComponent).join("/"));
});
return promise;
}
/* All the actions available on the $scope */
angular.extend($scope, actions);
angular.extend($scope, data);
$scope.sharedImages = projectData.sharedImages;
$scope.imageTagNames = data.imageTagNames;
$scope.imageByTag = imageByTag;
if (inPage) {
$scope.deleteTag = deleteTag;
$scope.deleteImageStream = deleteImageStream;
}
$scope.actions = {
modifyImageStream: $scope.modifyImageStream,
deleteImageStream: $scope.deleteImageStream,
deleteTag: $scope.deleteTag,
modifyProject: $scope.modifyProject,
};
};
}
])
.controller('ImagesCtrl', [
'$scope',
'$location',
'imageData',
'imageActions',
'projectData',
'kubeLoader',
'registryListingScopeSetup',
'filterService',
function($scope, $location, data, actions, projectData, loader, registryListingScopeSetup) {
$scope.sharedImages = projectData.sharedImages;
/* Watch all the images in current namespace */
data.watchImages($scope);
$scope.imagestreams = data.allStreams();
loader.listen(function() {
$scope.imagestreams = data.allStreams();
}, $scope);
$scope.$on("activate", function(ev, imagestream, tag) {
ev.preventDefault();
$location.path('/images/' + identifier(imagestream, tag));
});
registryListingScopeSetup($scope, false);
}
])
/*
* Note that we use the same controller for both the ImageStream
* and the Image view. This is because ngRoute can't special case
* routes based on the colon we use to differentiate the two in
* the path.
*
* ie: cockpit/ws vs. cockpit/ws:latest
*
* The |kind| on the scope tells us which is which.
*/
.controller('ImageCtrl', [
'$scope',
'$location',
'$routeParams',
'kubeSelect',
'kubeLoader',
'KubeDiscoverSettings',
'imageData',
'imageActions',
'projectData',
'projectPolicy',
'registryListingScopeSetup',
function($scope, $location, $routeParams, select, loader, discoverSettings, data, actions, projectData, projectPolicy, registryListingScopeSetup) {
var target = $routeParams["target"] || "";
var pos = target.indexOf(":");
/* colon contains a tag name, only set if we're looking at an image */
var namespace = $routeParams["namespace"] || "";
var name, tagname;
if (pos === -1) {
$scope.kind = "ImageStream";
name = target;
tagname = null;
} else {
$scope.kind = "Image";
name = target.substr(0, pos);
tagname = target.substr(pos + 1);
}
registryListingScopeSetup($scope, true);
/* There's no way to watch a single item ... so watch them all :( */
data.watchImages($scope);
loader.listen(function() {
$scope.stream = select().kind("ImageStream")
.namespace(namespace)
.name(name)
.one();
$scope.image = $scope.config = $scope.layers = $scope.labels = $scope.tag = null;
imagestreamEachTagItem($scope.stream || {}, function(tag, item) {
if (tag.tag === tagname)
$scope.tag = tag;
});
if ($scope.tag)
$scope.image = $scope.imageByTag($scope.tag);
if ($scope.image) {
$scope.names = data.imageTagNames($scope.image);
$scope.config = data.imageConfig($scope.image);
$scope.layers = data.imageLayers($scope.image);
$scope.labels = data.imageLabels($scope.image);
}
}, $scope);
$scope.$on("activate", function(ev, imagestream, tag) {
ev.preventDefault();
$location.path('/images/' + identifier(imagestream, tag));
});
function updateShowDockerPushCommands() {
discoverSettings().then(function(settings) {
projectPolicy.subjectAccessReview(namespace, settings.currentUser, 'update', 'imagestreamimages')
.then(function(allowed) {
if (allowed != $scope.showDockerPushCommands) {
$scope.showDockerPushCommands = allowed;
$scope.$applyAsync();
}
});
});
}
// watch for project changes to update showDockerPushCommands, and initialize it
$scope.$on("$routeUpdate", updateShowDockerPushCommands);
updateShowDockerPushCommands();
}
])
.factory("imageData", [
'kubeSelect',
'kubeLoader',
function(select, loader) {
var watching = false;
/* Called when we have to load images via imagestreams */
loader.listen(function(objects) {
for (var link in objects) {
if (objects[link].kind === "ImageStream")
handle_imagestream(objects[link]);
if (objects[link].kind === "Image")
handle_image(objects[link]);
}
});
function handle_imagestream(imagestream) {
var meta = imagestream.metadata || { };
var status = imagestream.status || { };
angular.forEach(status.tags || [ ], function(tag) {
angular.forEach(tag.items || [ ], function(item) {
var link = loader.resolve("Image", item.image);
if (link in loader.objects)
return;
/* An interim object while we're loading */
var interim = { kind: "Image", apiVersion: "v1", metadata: { name: item.image } };
loader.handle(interim);
if (!watching)
return;
var name = meta.name + "@" + item.image;
loader.load("ImageStreamImage", name, meta.namespace).then(function(resource) {
var image = resource.image;
if (image) {
image.kind = "Image";
loader.handle(image);
handle_image(image);
}
}, function(response) {
var message = response.statusText || response.message || String(response);
console.warn("couldn't load image: " + message);
interim.metadata.resourceVersion = "invalid";
});
});
});
}
/*
* Create a pseudo-item with kind DockerImageManifest for
* each image with a dockerImageManifest that we see. Identical
* name to the image itself.
*/
function handle_image(image) {
var item;
var manifest = image.dockerImageManifest;
if (manifest) {
manifest = JSON.parse(manifest);
angular.forEach(manifest.history || [], function(item) {
if (typeof item.v1Compatibility == "string")
item.v1Compatibility = JSON.parse(item.v1Compatibility);
});
item = {
kind: "DockerImageManifest",
metadata: {
name: image.metadata.name,
selfLink: "/internal/manifests/" + image.metadata.name
},
manifest: manifest,
};
loader.handle(item);
}
}
/* Load images, but fallback to loading individually */
function watchImages(until) {
watching = true;
var a = loader.watch("images", until);
var b = loader.watch("imagestreams", until);
return {
cancel: function() {
a.cancel();
b.cancel();
}
};
}
/*
* Filters selection to those with names that is
* in the given TagEvent.
*/
select.register("taggedBy", function(tag) {
var i, len;
var results = { };
// catch condition when tag.items is null due to imagestream import error
if (!tag.items)
return select(null);
for (i = 0, len = tag.items.length; i < len; i++)
this.name(tag.items[i].image).extend(results);
return select(results);
});
/*
* Filters selection to those with names that is in the first
* item in the given TagEvent.
*/
select.register("taggedFirst", function(tag) {
var results = { };
if (!tag.items)
return select(null);
if (tag.items.length)
this.name(tag.items[0].image).extend(results);
return select(results);
});
/*
* Filter that gets image streams for the given tag.
*/
select.register({
name: "containsTagImage",
digests: function(arg) {
var ret = [];
if (typeof arg == "string") {
ret.push(arg);
} else {
imagestreamEachTagItem(arg, function(tag, item) {
ret.push(item.image + "");
});
}
return ret;
}
});
select.register("listTagNames", function(image_name) {
var names = [];
angular.forEach(this.containsTagImage(image_name), function(stream) {
imagestreamEachTagItem(stream, function(tag, item) {
if (!image_name || item.image == image_name)
names.push(stream.metadata.namespace + "/" + stream.metadata.name + ":" + tag.tag);
});
});
return names;
});
/*
* Filter that gets the config object for a docker based
* image.
*/
select.register("dockerImageConfig", function() {
var results = { };
angular.forEach(this, function(image, key) {
var compat;
var layers = imageLayers(image) || { };
if (layers[0]) {
compat = layers[0].v1Compatibility;
if (compat && compat.config) {
results[key] = compat.config;
return;
}
}
var meta = image.dockerImageMetadata || { };
if (meta.Config)
results[key] = meta.Config;
});
return select(results);
});
/*
* Filter that gets a dict of labels for a config
* image.
*/
select.register("dockerConfigLabels", function() {
var results = { };
angular.forEach(this, function(config, key) {
var labels;
if (config)
labels = config.Labels;
if (labels)
results[key] = labels;
});
return select(results);
});
function imageLayers(image) {
if (!image)
return null;
var item = select().kind("DockerImageManifest")
.name(image.metadata.name)
.one();
if (item && item.manifest && item.manifest.schemaVersion === 1)
return item.manifest.history;
if (image.dockerImageLayers)
return image.dockerImageLayers;
return null;
}
/* HACK: We really want a metadata index here */
function configCommand(config) {
var result = [ ];
if (!config)
return "";
if (config.Entrypoint)
result.push.apply(result, config.Entrypoint);
if (config.Cmd)
result.push.apply(result, config.Cmd);
var string = result.join(" ");
if (config.User && config.User.split(":")[0] != "root")
return "$ " + string;
else
return "# " + string;
}
return {
watchImages: watchImages,
allStreams: function allStreams() {
return select().kind("ImageStream");
},
imageLayers: imageLayers,
imageConfig: function imageConfig(image) {
return select(image).dockerImageConfig()
.one() || { };
},
imageTagNames: function imageTagNames(image) {
return select().kind("ImageStream")
.listTagNames(image.metadata.name);
},
imageLabels: function imageLabels(image) {
var labels = select(image).dockerImageConfig()
.dockerConfigLabels()
.one();
if (labels && angular.equals({ }, labels))
labels = null;
return labels;
},
configCommand: configCommand,
};
}
])
.factory('imageActions', [
'$modal',
'$location',
function($modal, $location) {
function deleteImageStream(stream) {
return $modal.open({
animation: false,
controller: 'ImageStreamDeleteCtrl',
templateUrl: 'views/imagestream-delete.html',
resolve: {
dialogData: function() {
return { stream: stream };
}
},
}).result;
}
function createImageStream() {
return $modal.open({
animation: false,
controller: 'ImageStreamModifyCtrl',
templateUrl: 'views/imagestream-modify.html',
resolve: {
dialogData: function() {
return { };
}
},
}).result;
}
function modifyImageStream(stream) {
return $modal.open({
animation: false,
controller: 'ImageStreamModifyCtrl',
templateUrl: 'views/imagestream-modify.html',
resolve: {
dialogData: function() {
return { stream: stream };
}
},
}).result;
}
function deleteTag(stream, tag) {
var modal = $modal.open({
animation: false,
controller: 'ImageDeleteCtrl',
templateUrl: 'views/image-delete.html',
resolve: {
dialogData: function() {
return { stream: stream, tag: tag };
}
},
});
return modal.result;
}
function modifyProject(project) {
$location.path("/projects/" + project);
return false;
}
return {
createImageStream: createImageStream,
modifyImageStream: modifyImageStream,
deleteImageStream: deleteImageStream,
deleteTag: deleteTag,
modifyProject: modifyProject,
};
}
])
.controller("ImageStreamDeleteCtrl", [
"$scope",
"$modalInstance",
"dialogData",
"kubeMethods",
function($scope, $instance, dialogData, methods) {
angular.extend($scope, dialogData);
$scope.performDelete = function performDelete() {
return methods.delete($scope.stream);
};
}
])
.controller("ImageStreamModifyCtrl", [
"$scope",
"$modalInstance",
"dialogData",
"imageTagData",
"kubeMethods",
"filterService",
"gettextCatalog",
function($scope, $instance, dialogData, tagData, methods, filter, gettextCatalog) {
var stream = dialogData.stream || { };
var meta = stream.metadata || { };
var spec = stream.spec || { };
const _ = gettextCatalog.getString.bind(gettextCatalog);
var populate = "none";
if (spec.dockerImageRepository)
populate = "pull";
if (spec.tags)
populate = "tags";
var fields = {
name: meta.name || "",
project: meta.namespace || filter.namespace() || "",
populate: populate,
pull: spec.dockerImageRepository || "",
tags: tagData.parseSpec(spec),
insecure: hasInsecureTag(spec),
};
$scope.fields = fields;
$scope.labels = {
populate: {
none: _("Don't pull images automatically"),
pull: _("Sync all tags from a remote image repository"),
tags: _("Pull specific tags from another image repository"),
}
};
$scope.placeholder = _("eg: my-image-stream");
/* During creation we have a different label */
if (!dialogData.stream)
$scope.labels.populate.none = _("Create empty image stream");
function performModify() {
var data = {
apiVersion: "v1",
kind: "ImageStream",
metadata: { annotations: { "openshift.io/image.dockerRepositoryCheck" : null } }
};
if (fields.populate == "pull")
data.spec = { dockerImageRepository: fields.pull.trim(), };
else if (fields.populate == "tags")
data.spec = tagData.buildSpec(fields.tags, data.spec, fields.insecure, fields.pull.trim());
else
data.spec = { dockerImageRepository: null, tags: null };
return methods.patch(stream, data);
}
function performCreate() {
var data = {
apiVersion: "v1",
kind: "ImageStream",
metadata: {
name: fields.name.trim(),
namespace: fields.project.trim(),
}
};
if (fields.populate == "pull")
data.spec = { dockerImageRepository: fields.pull.trim(), };
else if (fields.populate == "tags")
data.spec = tagData.buildSpec(fields.tags, data.spec, fields.insecure, fields.pull.trim());
return methods.check(data, {
"metadata.name": "#imagestream-modify-name",
"metadata.namespace": "#imagestream-modify-project",
}).then(function() {
return methods.create(data, fields.project);
});
}
function hasInsecureTag(spec) {
// loop through tags, check importPolicy.insecure boolean
// if one tag is insecure the intent is the imagestream is insecure
var insecure;
if (spec) {
for (var tag in spec.tags) {
if (spec.tags[tag].importPolicy.insecure) {
insecure = spec.tags[tag].importPolicy.insecure;
break;
}
}
}
return insecure;
}
$scope.performCreate = performCreate;
$scope.performModify = performModify;
$scope.hasInsecureTag = hasInsecureTag;
$scope.projects = filter.namespaces;
angular.extend($scope, dialogData);
}
])
.controller("ImageDeleteCtrl", [
"$scope",
"$modalInstance",
"dialogData",
"kubeMethods",
function($scope, $instance, dialogData, methods) {
angular.extend($scope, dialogData);
$scope.performDelete = function performDelete() {
var name = $scope.stream.metadata.name + ":" + $scope.tag.tag;
return methods.delete("ImageStreamTag", name, $scope.stream.metadata.namespace);
};
}
]);
}());

File diff suppressed because it is too large Load Diff

View File

@ -1,558 +0,0 @@
/*
* 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;
};
}
]);
}());

File diff suppressed because it is too large Load Diff

View File

@ -1,150 +0,0 @@
/*
* 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');
require('../views/user-panel.html');
require('../views/service-panel.html');
require('../views/pod-panel.html');
require('../views/route-panel.html');
require('../views/pv-panel.html');
require('../views/default-panel.html');
require('../views/node-panel.html');
require('../views/project-panel.html');
require('../views/container-panel.html');
require('../views/deploymentconfig-panel.html');
require('../views/group-panel.html');
require('../views/replicationcontroller-panel.html');
function inClassOrTag(el, cls, tag) {
return (el && el.classList && el.classList.contains(cls)) ||
(el && el.tagName === tag) ||
(el && inClassOrTag(el.parentNode, cls, tag));
}
angular.module('kubernetes.listing', [])
.directive('listingTable', [
function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
}
};
}
])
.factory('ListingState', [
function() {
return function ListingState(scope) {
var self = this;
var data = { };
self.selected = { };
self.enableActions = false;
/* Check that either .btn or li were not clicked */
function checkBrowserEvent(ev) {
return !(ev && inClassOrTag(ev.target, "btn", "li"));
}
self.hasSelected = function hasSelected(id) {
return !angular.equals({}, self.selected);
};
self.expanded = function expanded(id) {
if (angular.isUndefined(id)) {
for (id in data)
return true;
return false;
} else {
return id in data;
}
};
self.toggle = function toggle(id, ev) {
var value;
if (self.enableActions) {
ev.stopPropagation();
return;
}
if (id) {
value = !(id in data);
if (value)
self.expand(id, ev);
else
self.collapse(id, ev);
}
};
self.expand = function expand(id, ev) {
data[id] = true;
if (ev)
ev.stopPropagation();
};
self.activate = function activate(id, ev) {
if (checkBrowserEvent(ev)) {
if (self.expanded(id))
self.collapse(id);
else
scope.$emit("activate", id);
}
};
self.collapse = function collapse(id, ev) {
if (id) {
delete data[id];
} else {
Object.keys(data).forEach(function(old) {
delete data[old];
});
}
if (ev)
ev.stopPropagation();
};
};
}
])
.directive('listingPanel', [
function() {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
var tab = 'main';
scope.tab = function(name, ev) {
if (ev) {
tab = name;
ev.stopPropagation();
}
return tab === name;
};
},
templateUrl: function(element, attrs) {
var kind = attrs.kind;
return "views/" + kind.toLowerCase() + "-panel.html";
}
};
}
]);
}());

View File

@ -1,116 +0,0 @@
/*
* 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() {
/* Tell webpack what to bundle here */
var angular = require('angular');
require('angular-route');
require('angular-gettext/dist/angular-gettext.js');
require('angular-bootstrap-npm/dist/angular-bootstrap.js');
require('kubernetes-container-terminal/dist/container-terminal.js');
require('object-describer/dist/object-describer.js');
require('kubernetes-object-describer/dist/object-describer.js');
/* The kubernetes client */
require('./kube-client');
require('./kube-client-cockpit');
/* The other angular modules */
require('./containers');
require('./dashboard');
require('./details');
require('./graphs');
require('./policy');
require('./projects');
require('./images');
require('./nodes');
require('./topology');
require('./volumes');
/* And the actual application */
require('./app');
angular.module('kubernetes', [
'ngRoute',
'ui.bootstrap',
'gettext',
'kubeClient',
'kubeClient.cockpit',
'kubernetes.app',
'kubernetes.graph',
'kubernetes.dashboard',
'kubernetes.containers',
'kubernetes.details',
'kubernetes.topology',
'kubernetes.volumes',
'kubernetes.nodes',
'kubernetes.date',
'registry.images',
'registry.policy',
'registry.projects',
'registryUI.date',
'kubernetesUI'
])
.config([
'$routeProvider',
'KubeWatchProvider',
'KubeRequestProvider',
'KubeSocketProvider',
'KubeTranslateProvider',
'KubeFormatProvider',
'kubernetesContainerSocketProvider',
'KubeDiscoverSettingsProvider',
'KubeBrowserStorageProvider',
'MomentLibProvider',
'$provide',
function($routeProvider, KubeWatchProvider, KubeRequestProvider,
KubeSocketProvider, KubeTranslateProvider, KubeFormatProvider,
kubernetesContainerSocketProvider, KubeDiscoverSettingsProvider,
KubeBrowserStorageProvider, MomentLibProvider, $provide) {
$routeProvider.otherwise({ redirectTo: '/' });
/* Tell the kube-client code to use cockpit watches and requests */
KubeWatchProvider.KubeWatchFactory = "CockpitKubeWatch";
KubeRequestProvider.KubeRequestFactory = "CockpitKubeRequest";
KubeSocketProvider.KubeSocketFactory = "CockpitKubeSocket";
KubeTranslateProvider.KubeTranslateFactory = "CockpitTranslate";
KubeFormatProvider.KubeFormatFactory = "CockpitFormat";
KubeDiscoverSettingsProvider.KubeDiscoverSettingsFactory = "cockpitKubeDiscoverSettings";
KubeBrowserStorageProvider.KubeBrowserStorageFactory = "cockpitBrowserStorage";
MomentLibProvider.MomentLibFactory = "momentLib";
/* Tell the container-terminal that we want to be involved in WebSocket creation */
kubernetesContainerSocketProvider.WebSocketFactory = 'cockpitContainerWebSocket';
$provide.decorator("$exceptionHandler",
['$delegate',
'$log',
function($delegate, $log) {
return function (exception, cause) {
/* Displays an oops if we're running in cockpit */
if (window.parent !== window && window.name.indexOf("cockpit1:") === 0)
window.parent.postMessage("\n{ \"command\": \"oops\" }", "*");
$delegate(exception, cause);
};
}]);
}
]);
}());

View File

@ -1,898 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
/* This is here to support cockpit.jump
* If this ever needs to be used outsite of cockpit
* then we'll need abstract this away in kube-client-cockpit
*/
/* globals cockpit */
(function() {
var angular = require('angular');
var d3 = require('d3');
require('angular-route');
require('angular-dialog.js');
require('./charts');
require('./date');
require('./kube-client');
require('./listing');
require('./utils');
require('../views/nodes-page.html');
require('../views/node-page.html');
require('../views/node-body.html');
require('../views/node-capacity.html');
require('../views/node-stats.html');
require('../views/node-add.html');
require('../views/node-delete.html');
require('../views/node-alerts.html');
angular.module('kubernetes.nodes', [
'ngRoute',
'kubeClient',
'kubernetes.date',
'kubernetes.listing',
'kubeUtils',
'ui.cockpit',
'ui.charts',
])
.config([
'$routeProvider',
function($routeProvider) {
$routeProvider
.when('/nodes', {
templateUrl: 'views/nodes-page.html',
controller: 'NodeCtrl'
})
.when('/nodes/:target', {
controller: 'NodeCtrl',
templateUrl: 'views/node-page.html'
});
}
])
/*
* The controller for the node view.
*/
.controller('NodeCtrl', [
'$scope',
'kubeLoader',
'kubeSelect',
'ListingState',
'filterService',
'$routeParams',
'$location',
'nodeActions',
'nodeData',
'nodeStatsSummary',
'$timeout',
'KubeBrowserStorage',
function($scope, loader, select, ListingState, filterService,
$routeParams, $location, actions, nodeData, statsSummary,
$timeout, browser) {
var target = $routeParams["target"] || "";
$scope.target = target;
$scope.stats = statsSummary.newNodeStatsSummary();
loader.listen(function() {
var selection;
$scope.nodes = select().kind("Node");
if (target) {
selection = select().kind("Node")
.name(target);
$scope.item = selection.one();
} else {
selection = $scope.nodes;
}
if ($scope.stats)
$scope.stats.trackNodes(selection);
}, $scope);
loader.watch("Node", $scope);
$scope.$on("$destroy", function() {
if ($scope.stats)
$scope.stats.close();
$scope.stats = null;
});
$scope.listing = new ListingState($scope);
/* All the actions available on the $scope */
angular.extend($scope, actions);
angular.extend($scope, nodeData);
$scope.$on("activate", function(ev, id) {
$location.path('/nodes/' + encodeURIComponent(id));
$scope.$applyAsync();
});
$scope.nodePods = function node_pods(item) {
var meta = item.metadata || {};
return select().kind("Pod")
.host(meta.name);
};
$scope.deleteSelectedNodes = function() {
var k;
var selected = [];
for (k in $scope.listing.selected) {
if ($scope.nodes[k] && $scope.listing.selected[k])
selected.push($scope.nodes[k]);
}
if (!selected.length)
return;
return actions.deleteNodes(selected).then(function() {
$scope.listing.selected = {};
});
};
/* Redirect after a delete */
$scope.deleteNode = function(val) {
var promise = actions.deleteNodes(val);
/* If the promise is successful, redirect to another page */
promise.then(function() {
if ($scope.target)
$location.path("/nodes");
});
return promise;
};
$scope.jump = function (node) {
var host, ip;
if (!node || !node.metadata)
return;
host = node.metadata.name;
ip = nodeData.nodeIPAddress(node);
if (ip == "127.0.0.1" || ip == "::1") {
ip = "localhost";
} else {
browser.sessionStorage.setItem(
"v1-session-machine/" + ip,
JSON.stringify({ "address": ip,
"label": host,
visible: true })
);
}
cockpit.jump("/", ip);
};
}
])
.directive('nodeBody',
function() {
return {
restrict: 'A',
templateUrl: 'views/node-body.html',
};
}
)
.directive('nodeCapacity',
function() {
return {
restrict: 'A',
templateUrl: 'views/node-capacity.html',
};
}
)
.directive('nodeStats',
function() {
return {
restrict: 'A',
templateUrl: 'views/node-stats.html',
};
}
)
.factory('nodeActions', [
'$modal',
function($modal) {
function addNode() {
return $modal.open({
animation: false,
controller: 'AddNodeCtrl',
templateUrl: 'views/node-add.html',
resolve: {},
}).result;
}
function deleteNodes(val) {
var nodes;
if (angular.isArray(val))
nodes = val;
else
nodes = [ val ];
return $modal.open({
animation: false,
controller: 'NodeDeleteCtrl',
templateUrl: 'views/node-delete.html',
resolve: {
dialogData: function() {
return { nodes: nodes };
}
},
}).result;
}
return {
addNode: addNode,
deleteNodes: deleteNodes
};
}
])
.controller("NodeDeleteCtrl", [
"$q",
"$scope",
"$modalInstance",
"dialogData",
"kubeMethods",
"kubeSelect",
function($q, $scope, $instance, dialogData, methods, select) {
angular.extend($scope, dialogData);
$scope.performDelete = function performDelete() {
var k;
var errors = [];
var nodes = {};
var promises = [];
function handleError(ex) {
errors.push(ex.message || ex.statusText);
nodes[k] = $scope.nodes[k];
return $q.reject();
}
for (k in $scope.nodes) {
var p = methods.delete($scope.nodes[k])
.catch(handleError);
promises.push(p);
}
return $q.all(promises).catch(function () {
$scope.nodes = select(nodes);
return $q.reject(errors);
});
};
}
])
.factory('nodeData', [
"KubeMapNamedArray",
"KubeTranslate",
function (mapNamedArray, translate) {
const _ = translate.gettext;
function nodeConditions(node) {
var status;
if (!node)
return;
if (!node.conditions) {
status = node.status || { };
node.conditions = mapNamedArray(status.conditions, "type");
}
return node.conditions;
}
function nodeCondition(node, type) {
var conditions = nodeConditions(node) || {};
return conditions[type] || {};
}
function nodeStatus(node) {
var spec = node ? node.spec : {};
if (!nodeCondition(node, "Ready").status)
return _("Unknown");
if (nodeCondition(node, "Ready").status != 'True')
return _("Not Ready");
if (spec && spec.unschedulable)
return _("Scheduling Disabled");
return _("Ready");
}
function nodeStatusIcon(node) {
var state = "";
/* If no status.conditions then it hasn't even started */
if (!nodeCondition(node, "Ready").status) {
state = "wait";
} else {
if (nodeCondition(node, "Ready").status != 'True') {
state = "fail";
} else if (nodeCondition(node, "OutOfDisk").status == "True" ||
nodeCondition(node, "OutOfMemory").status == "True") {
state = "warn";
}
}
return state;
}
function nodeIPAddress(node) {
if (!node || !node.status)
return;
var addresses = node.status.addresses;
var address;
var internal;
/* If no addresses then it hasn't even started */
if (addresses) {
addresses.forEach(function(a) {
if (a.type == "LegacyHostIP" || a.type == "ExternalIP") {
address = a.address;
return false;
} else if (a.type == "InternalIP") {
internal = a.address;
}
});
}
return address || internal;
}
return {
nodeStatusIcon: nodeStatusIcon,
nodeCondition: nodeCondition,
nodeConditions: nodeConditions,
nodeStatus: nodeStatus,
nodeIPAddress: nodeIPAddress
};
}
])
.controller("AddNodeCtrl", [
"$q",
"$scope",
"$modalInstance",
"kubeMethods",
"KubeTranslate",
function($q, $scope, $instance, methods, translate) {
const _ = translate.gettext;
var fields = {
"address" : "",
"name" : "",
};
var dirty = false;
$scope.fields = fields;
function validate() {
var regex = /^[a-z0-9.-]+$/i;
var defer = $q.defer();
var address = fields.address.trim();
var name = fields.name.trim();
var ex;
var failures = [];
var item;
if (!address)
ex = new Error(_("Please type an address"));
else if (!regex.test(address))
ex = new Error(_("The address contains invalid characters"));
if (ex) {
ex.target = "#node-address";
failures.push(ex);
}
if (name && !regex.test(name)) {
ex = new Error(_("The name contains invalid characters"));
ex.target = "#node-name";
failures.push(ex);
}
if (failures.length > 0) {
defer.reject(failures);
} else {
item = {
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": name || address,
}
};
defer.resolve(item);
}
return defer.promise;
}
$scope.nameKeyUp = function nameKeyUp(event) {
dirty = true;
if (event.keyCode == 13)
$scope.performAdd();
};
$scope.addressKeyUp = function addressKeyUp(event) {
if (event.keyCode == 13)
$scope.performAdd();
else if (!dirty)
fields.name = event.target.value;
};
$scope.performAdd = function performAdd() {
return validate().then(function(item) {
return methods.create(item);
});
};
}
])
.directive('kubernetesStatusIcon', function() {
return {
restrict: 'A',
link: function($scope, element, attributes) {
$scope.$watch(attributes["status"], function(status) {
element
.toggleClass("spinner spinner-xs", status == "wait")
.toggleClass("pficon pficon-error-circle-o", status == "fail")
.toggleClass("pficon pficon-warning-triangle-o", status == "warn");
});
}
};
})
.directive('nodeAlerts', function() {
return {
restrict: 'A',
templateUrl: 'views/node-alerts.html'
};
})
.factory('nodeStatsSummary', [
"$q",
"$interval",
"$exceptionHandler",
"KubeRequest",
"nodeData",
"KubeStringToBytes",
"KubeFormat",
function ($q, $interval, $exceptionHandler, KubeRequest, nodeData,
kubeStringToBytes, format) {
function NodeStatsSummary() {
var self = this;
var requests = {};
var statData = {};
var callbacks = [];
var interval;
function request(name) {
if (requests[name])
return $q.when([]);
var path = "/api/v1/nodes/" + encodeURIComponent(name) + "/proxy/stats/summary/";
var req = KubeRequest("GET", path);
requests[name] = req;
return req.then(function(data) {
delete requests[name];
statData[name] = data.data;
})
.catch(function(ex) {
delete requests[name];
delete statData[name];
if (ex.status != 503)
console.warn(ex);
});
}
function invokeCallbacks(/* ... */) {
var i, len, func;
for (i = 0, len = callbacks.length; i < len; i++) {
func = callbacks[i];
try {
if (func)
func.apply(self, arguments);
} catch (e) {
$exceptionHandler(e);
}
}
}
function fetchForNames(names) {
var q = [];
angular.forEach(names, function(name) {
q.push(request(name));
});
$q.all(q).then(invokeCallbacks, invokeCallbacks);
}
interval = $interval(function () {
fetchForNames(Object.keys(statData));
}, 5000);
self.watch = function watch(callback) {
callbacks.push(callback);
return {
cancel: function() {
var i, len;
for (i = 0, len = callbacks.length; i < len; i++) {
if (callbacks[i] === callback)
callbacks[i] = null;
}
}
};
};
self.close = function close() {
var name;
if (interval)
$interval.cancel(interval);
for (name in requests) {
var req = requests[name];
if (req && req.cancel)
req.cancel();
}
};
self.trackNodes = function trackNodes(selection) {
var names = [];
angular.forEach(selection, function(node) {
var ready = nodeData.nodeCondition(node, "Ready");
var meta = node ? node.metadata : {};
var name = meta ? meta.name : "";
if (ready && ready.status === 'True') {
if (!statData[name])
names.push(name);
} else {
// Unfortunally i'm seen some requests
// not error so clean them out.
delete statData[name];
if (request[name]) {
request[name].cancel();
delete request[name];
}
}
});
fetchForNames(names);
};
self.getSimpleUsage = function getSimpleUsage(node, section) {
var meta = node ? node.metadata : {};
var status = node ? node.status : {};
var name = meta ? meta.name : "";
var nodeData = statData[name] ? statData[name].node : {};
var result = nodeData[section];
if (!result)
return;
var allocatable = status ? status.allocatable : {};
if (!allocatable)
return;
switch (section) {
case "cpu":
if (!allocatable.cpu)
return;
return {
used: result.usageNanoCores,
total: allocatable.cpu * 1000000000
};
case "memory":
if (!allocatable.memory)
return;
return {
used: result.usageBytes,
total: kubeStringToBytes(allocatable.memory)
};
case "fs":
return {
used: result.usedBytes,
total: result.capacityBytes
};
default:
}
};
}
return {
newNodeStatsSummary: function(interval) {
return new NodeStatsSummary(interval);
},
};
}
])
.directive('nodeOsGraph', [
"KubeTranslate",
function(translate) {
const _ = translate.gettext;
return {
scope: {
'nodes' : '='
},
template: '<div class="col-xs-12 col-md-6" id="os-counts-graph" donut-pct-chart data="data" bar-size="8" legend="os-counts-legend" large-title="largeTitle"></div><div class="col-xs-12 col-md-6 legend-col"><div id="os-counts-legend"></div></div>',
restrict: 'A',
link: function($scope, element, attributes) {
$scope.data = [];
$scope.largeTitle = 0;
function refresh(items) {
items = items || [];
var data = {};
angular.forEach(items, function(node) {
var os;
var color;
if (node.status && node.status.nodeInfo)
os = node.status.nodeInfo.osImage;
if (!os) {
os = _("Unknown");
color = "#bbbbbb";
}
if (data[os])
data[os].value++;
else
data[os] = { value: 1, label: os, color: color };
});
var arr = Object.keys(data).map(function(k) {
return data[k];
});
arr.sort(function (a, b) {
if (a.label < b.label)
return -1;
if (a.label > b.label)
return 1;
return 0;
});
$scope.data = arr;
$scope.largeTitle = items.length;
}
$scope.$watchCollection('nodes', function(nodes) {
refresh(nodes);
});
}
};
}
])
.directive('nodeHeatMap', [
"KubeTranslate",
"KubeFormat",
function(translate, format) {
const _ = translate.gettext;
return {
restrict: 'A',
scope: {
'nodes' : '=',
'stats' : '='
},
template: '<div class="card-pf-title"><ul class="nav nav-tabs nav-tabs-pf"></ul></div><div threshold-heat-map class="card-pf-body node-heatmap" data="data" clickAction="clickAction()" legend="nodes-heatmap-legend"></div><div id="nodes-heatmap-legend"></div></div>',
link: function($scope, element, attributes) {
var outer = d3.select(element[0]);
var currentTab;
var tabs = {
cpu: {
label: _("CPU"),
tooltip: function(r) { return format.format(_("CPU Utilization: $0%"), Math.round((r.used / r.total) * 100)) }
},
memory: {
label: _("Memory"),
tooltip: function(r) { return format.format(_("Memory Utilization: $0%"), Math.round((r.used / r.total) * 100)) }
},
fs: {
label: _("Disk"),
tooltip: function(r) { return format.format(_("Disk Utilization: $0%"), Math.round((r.used / r.total) * 100)) }
}
};
outer.select("ul.nav-tabs")
.selectAll("li")
.data(Object.keys(tabs))
.enter()
.append("li")
.attr("data-metric", function(d) { return d })
.append("a")
.text(function(d) { return tabs[d].label });
function changeTab(tab) {
currentTab = tab;
outer.selectAll("ul.nav-tabs li")
.attr("class", function(d) { return tab === d ? "active" : null });
refreshData();
}
outer.selectAll("ul.nav-tabs li")
.on("click", function() {
changeTab(d3.select(this).attr("data-metric"));
});
function refreshData() {
var nodes = $scope.nodes;
var data = [];
if (!nodes)
nodes = [];
angular.forEach(nodes, function(node) {
var result, value, name;
var tooltip = _("Unknown");
if (node && node.metadata)
name = node.metadata.name;
if (!name)
return;
result = $scope.stats.getSimpleUsage(node, currentTab);
if (result)
value = result.used / result.total;
if (value === undefined)
value = -1;
else
tooltip = tabs[currentTab].tooltip(result);
data.push({ value: value, name: name,
tooltip: tooltip });
});
data.sort(function (a, b) {
return b.value - a.value;
});
$scope.$applyAsync(function() {
$scope.data = data;
});
return true;
}
$scope.$on("boxClick", function (ev, name) {
$scope.$emit("activate", name);
});
var sw = $scope.stats.watch(refreshData);
$scope.$on("$destroy", function() {
sw.cancel();
});
changeTab("cpu");
}
};
}
])
.directive('nodeUsageDonutChart', [
"KubeTranslate",
"KubeFormat",
function(translate, format) {
const _ = translate.gettext;
return {
restrict: 'A',
scope: {
'node' : '=',
'stats' : '=',
},
template: '<div ng-if="data" donut-pct-chart data="data" bar-size="8" large-title="largeTitle" small-title="smallTitle"></div><div class="text-center" ng-if="data">{{ title }}</div>',
link: function($scope, element, attributes) {
var colorFunc = d3.scale.threshold()
.domain([0.7, 0.8, 0.9])
.range(['#d4f0fa', '#F9D67A', '#EC7A08', '#CE0000']);
var types = {
cpu: {
label: _("CPU"),
smallTitle: function () {},
largeTitle: function (result) {
var r = result.used / result.total;
var p = Math.round(r * 100);
return format.format("$0%", p);
},
},
memory: {
label: _("Memory"),
smallTitle: function (result) {
return _("Used");
},
largeTitle: function (result) {
return format.formatBytes(result.used);
},
},
fs: {
label: _("Disk"),
smallTitle: function (result) {
return _("Used");
},
largeTitle: function (result) {
return format.formatBytes(result.used);
},
}
};
var type = attributes['type'];
$scope.title = types[type].label;
function clear() {
$scope.$applyAsync(function() {
$scope.data = null;
$scope.smallTitle = null;
$scope.largeTitle = null;
});
}
function refreshData() {
var node = $scope.node;
var result;
if (!node)
return clear();
result = $scope.stats.getSimpleUsage(node, type);
if (result) {
$scope.$applyAsync(function() {
$scope.smallTitle = types[type].smallTitle(result);
$scope.largeTitle = types[type].largeTitle(result);
var u = Math.round((result.used / result.total) * 100);
var l = 100 - u;
var freeText = translate.ngettext("$0% Free",
"$0% Free", u);
var usedText = translate.ngettext("$0% Used",
"$0% Used", u);
$scope.data = [
{ value: result.total - result.used,
tooltip : format.format(freeText, l),
color: "#bbbbbb" },
{ value: result.used,
tooltip : format.format(usedText, u),
color: colorFunc(result.used / result.total) }
];
});
} else {
clear();
}
return true;
}
var sw = $scope.stats.watch(refreshData);
$scope.$on("$destroy", function() {
sw.cancel();
});
}
};
}
]);
}());

View File

@ -1,412 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
(function() {
var angular = require('angular');
require('./kube-client');
angular.module('registry.policy', [
'kubeClient',
])
.factory("projectPolicy", [
'$q',
'$rootScope',
'kubeLoader',
'kubeMethods',
'kubeSelect',
'KubeWatch',
'KubeRequest',
'KUBE_SCHEMA',
function($q, $rootScope, loader, methods, select, watch, KubeRequest, KUBE_SCHEMA) {
var apiGroup;
var RBAC_GROUP = "rbac.authorization.k8s.io";
var RBAC_API = "/apis/rbac.authorization.k8s.io/v1beta1";
var POLICY_BINDING_API = KUBE_SCHEMA["RoleBinding"]["api"];
var watchPromise;
function setupRoleBinding(group) {
KUBE_SCHEMA["RoleBinding"]["api"] = group ? RBAC_API : POLICY_BINDING_API;
KUBE_SCHEMA["rolebindings"]["api"] = group ? RBAC_API : POLICY_BINDING_API;
apiGroup = group;
expireSAR(null);
expireWhoCan(null);
return group ? "rolebindings" : "policybindings";
}
function ensureWatchType() {
if (!watchPromise) {
watchPromise = new KubeRequest("GET", "/oapi/v1")
.then(function(response) {
var data = response.data || {};
var i;
var l = data.resources || [];
for (i = 0; i < l.length; i++) {
if (l[i].kind == "PolicyBinding")
return setupRoleBinding();
}
return setupRoleBinding(RBAC_GROUP);
}, function(err) {
console.warn("Error getting API", err);
return setupRoleBinding();
});
}
return watchPromise;
}
/*
* Data loading hacks:
*
* We would like to watch rolebindings, but not all versions support
* that. So we have to watch policybindings and then infer the
* rolebindings from there.
*
* In addition we would like to be able to load User and Group objects,
* even if only for a certain project. However, non-cluster admins
* fail to do this, so we simulate these objects from the role bindings.
*/
loader.listen(function(present, removed) {
var link;
var expire = { };
/* If reseting clear status */
if (!present && !removed) {
expireSAR(null);
expireWhoCan(null);
watchPromise = null;
return;
}
for (link in removed) {
if (removed[link].kind == "PolicyBinding") {
update_rolebindings(removed[link].roleBindings, true);
expire[removed[link].metadata.namespace] = true;
}
}
for (link in present) {
if (present[link].kind == "PolicyBinding") {
update_rolebindings(present[link].roleBindings, false);
expire[present[link].metadata.namespace] = true;
} else if (present[link].kind == "RoleBinding") {
ensure_subjects(present[link].subjects || []);
expire[present[link].metadata.namespace] = true;
}
}
var namespace;
for (namespace in expire) {
expireWhoCan(namespace);
expireSAR(namespace);
}
});
function update_rolebindings(bindings, removed) {
angular.forEach(bindings || [], function(wrapper) {
loader.handle(wrapper.roleBinding, removed, "RoleBinding");
});
}
function ensure_subjects(subjects) {
angular.forEach(subjects, function(subject) {
var link = loader.resolve(subject.kind, subject.name, subject.namespace);
if (link in loader.objects)
return;
/* Don't show system groups */
if (subject.kind == "Group" && subject.name.indexOf("system:") === 0)
return;
/* An interim object, until perhaps the real thing can be loaded */
var interim = { kind: subject.kind, apiVersion: "v1", metadata: { name: subject.name } };
if (subject.namespace)
interim.metadata.namespace = subject.namespace;
loader.handle(interim);
});
}
/*
* Cached localresourceaccessreviews responses, and expired data
* Each one has a project key, containing an object with verb:resource keys.
*/
var cached = { };
var expired = { };
function fillWhoCan(namespace, verb, resource, result) {
var key = verb + ":" + resource;
if (!(namespace in cached))
cached[namespace] = { };
cached[namespace][key] = result;
if (result) {
if (namespace in expired) {
delete expired[namespace][key];
}
}
$rootScope.$applyAsync();
}
function expireWhoCan(namespace) {
if (namespace) {
expired[namespace] = angular.extend({ }, cached[namespace]);
delete cached[namespace];
} else {
expired = cached;
cached = { };
}
$rootScope.$applyAsync();
}
function lookupWhoCan(namespace, verb, resource) {
var key = verb + ":" + resource;
var ask = true;
var result = null;
var data = cached[namespace];
if (data) {
if (key in data) {
result = data[key];
ask = false;
}
}
if (!result) {
data = expired[namespace];
if (data) {
if (key in data)
result = data[key];
}
}
if (!ask)
return result;
/* Perform a request */
var request = {
kind: "LocalResourceAccessReview",
apiVersion: "v1",
namespace: "",
verb: verb,
resource: resource,
resourceName: "",
content: null
};
/* Fill in null info while looking up */
fillWhoCan(namespace, verb, resource, null);
var path = loader.resolve("localresourceaccessreviews", null, namespace);
methods.post(path, request)
.then(function(response) {
fillWhoCan(namespace, verb, resource, response);
}, function(response) {
console.warn("failed to lookup access:", namespace, verb, resource + ":",
response.message || JSON.stringify(response));
});
return result;
}
var sarCache = { };
function subjectAccessReview(namespace, user, verb, resource) {
var key = namespace + ':' + (user ? user.metadata.name : "") + ':' + verb + ':' + resource;
var defer = $q.defer();
if (key in sarCache) {
defer.resolve(sarCache[key]);
} else {
var request = {
kind: "SubjectAccessReview",
apiVersion: "v1",
namespace: namespace,
verb: verb,
resource: resource
};
methods.post(loader.resolve("subjectaccessreviews"), request)
.then(function(response) {
sarCache[key] = response.allowed;
defer.resolve(response.allowed);
}, function(response) {
console.warn("failed to review subject access:", response.message || JSON.stringify(response));
defer.reject(response.message || JSON.stringify(response));
});
}
return defer.promise;
}
function expireSAR(namespace) {
if (namespace) {
for (var key in sarCache) {
if (key.lastIndexOf(namespace + ':', 0) === 0)
delete sarCache[key];
}
} else {
sarCache = { };
}
$rootScope.$applyAsync();
}
/*
* HACK: There's no way to PATCH subjects in or out
* of a role, so we have to use this race prone mechanism.
*/
function modifyRole(namespace, role, callback) {
var path = loader.resolve("RoleBinding", role, namespace);
return loader.load(path)
.then(function(resource) {
callback(resource);
return methods.put(path, resource);
});
}
function createRole(namespace, role, subjects) {
var name = toName(role);
var binding = {
kind: "RoleBinding",
metadata: {
name: name,
namespace: namespace,
creationTimestamp: null,
},
userNames: [],
groupNames: [],
subjects: [],
roleRef: {
name: role,
kind: "ClusterRole",
}
};
addToArray(roleArray(binding, "subjects"), subjects);
addToArray(roleArrayKind(binding, subjects.kind), subjects.name);
return methods.create(binding, namespace);
}
function removeFromRole(project, role, subject) {
subject.apiGroup = apiGroup;
var namespace = toName(project);
return modifyRole(namespace, role, function(data) {
removeFromArray(roleArray(data, "subjects"), subject);
removeFromArray(roleArrayKind(data, subject.kind), subject.name);
}).then(function() {
expireWhoCan(namespace);
}, function(resp) {
/* If the role doesn't exist consider removed to work */
if (resp.code !== 404)
return $q.reject(resp);
});
}
function removeMemberFromProject(project, subjectRoleBindings, subject) {
var registryRoles = ["registry-admin", "registry-editor", "registry-viewer"];
var chain = $q.when();
subject.apiGroup = apiGroup;
angular.forEach(subjectRoleBindings, function(role) {
// Since we only added registry roles
// remove ONLY registry roles
if (indexOf(registryRoles, role.roleRef.name) !== -1) {
chain = chain.then(function() {
return removeFromRole(project, role.roleRef.name, subject);
});
}
});
return chain;
}
function indexOf(array, value) {
var i, len;
for (i = 0, len = array.length; i < len; i++) {
if (angular.equals(array[i], value))
return i;
}
return -1;
}
function addToArray(array, value) {
var index = indexOf(array, value);
if (index < 0)
array.push(value);
}
function removeFromArray(array, value) {
var index = indexOf(array, value);
if (index >= 0)
array.splice(index, 1);
}
function roleArray(data, field) {
var array = data[field] || [];
data[field] = array;
return array;
}
function roleArrayKind(data, kind) {
if (kind == "Group" || kind == "SystemGroup")
return roleArray(data, "groupNames");
else
return roleArray(data, "userNames");
}
function toName(object) {
if (typeof object == "object")
return object.metadata.name;
else
return object;
}
return {
watch: function watch(until) {
ensureWatchType().then(function (what) {
loader.watch(what, until)
.then(function() {
expireWhoCan(null);
});
});
},
whoCan: function whoCan(project, verb, resource) {
return lookupWhoCan(toName(project), verb, resource);
},
addToRole: function addToRole(project, role, subject) {
subject.apiGroup = apiGroup;
var namespace = toName(project);
return modifyRole(namespace, role, function(data) {
addToArray(roleArray(data, "subjects"), subject);
addToArray(roleArrayKind(data, subject.kind), subject.name);
}).then(function() {
expireWhoCan(namespace);
}, function(resp) {
/* If the role doesn't exist create it */
if (resp.code === 404)
return createRole(namespace, role, subject);
return $q.reject(resp);
});
},
removeFromRole: removeFromRole,
removeMemberFromProject: removeMemberFromProject,
subjectAccessReview: subjectAccessReview
};
}
]);
}());

File diff suppressed because it is too large Load Diff

View File

@ -1,198 +0,0 @@
/*
* 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');
require('angular-route');
require('angular-gettext/dist/angular-gettext.js');
require('angular-bootstrap-npm/dist/angular-bootstrap.js');
require('./app');
require('./date');
require('./images');
require('./projects');
require('./policy');
require('./kube-client');
require('./kube-client-cockpit');
require('../views/registry-dashboard-page.html');
var MAX_RECENT_STREAMS = 15;
var MAX_RECENT_TAGS = 8;
angular.module('registry', [
'ngRoute',
'ui.bootstrap',
'ui.bootstrap.popover',
'gettext',
'kubernetes.app',
'kubernetes.date',
'registry.images',
'registry.projects',
'registry.policy',
'registryUI.date',
'kubeClient',
'kubeClient.cockpit'
])
.config([
'$routeProvider',
'KubeWatchProvider',
'KubeRequestProvider',
'KubeDiscoverSettingsProvider',
'MomentLibProvider',
'$provide',
function($routeProvider, KubeWatchProvider, KubeRequestProvider,
KubeDiscoverSettingsProvider, MomentLibProvider, $provide) {
$routeProvider
.when('/', {
templateUrl: 'views/registry-dashboard-page.html',
controller: 'DashboardCtrl',
reloadOnSearch: false,
})
.otherwise({ redirectTo: '/' });
/* Tell the kube-client code to use cockpit watches and requests */
KubeWatchProvider.KubeWatchFactory = "CockpitKubeWatch";
KubeRequestProvider.KubeRequestFactory = "CockpitKubeRequest";
KubeDiscoverSettingsProvider.KubeDiscoverSettingsFactory = "cockpitKubeDiscoverSettings";
MomentLibProvider.MomentLibFactory = "momentLib";
$provide.decorator("$exceptionHandler",
['$delegate',
'$log',
function($delegate, $log) {
return function (exception, cause) {
/* Displays an oops if we're running in cockpit */
if (window.parent !== window && window.name.indexOf("cockpit1:") === 0)
window.parent.postMessage("\n{ \"command\": \"oops\" }", "*");
$delegate(exception, cause);
};
}]);
}
])
.controller('DashboardCtrl', [
'$scope',
'kubeLoader',
'kubeSelect',
'KubeDiscoverSettings',
'imageData',
'imageActions',
'projectActions',
'projectData',
'projectPolicy',
'filterService',
function($scope, loader, select, discoverSettings, imageData, imageActions, projectActions, projectData, projectPolicy, filter) {
loader.load("projects");
/* Watch the for project access changes */
projectPolicy.watch($scope);
/*
* For now the dashboard has to watch all images in
* order to display the 'Images pushed recently' data
*
* In the future we want to have a metadata or filtering
* service that we can query for that data.
*/
imageData.watchImages($scope);
function compareVersion(a, b) {
a = (a.metadata || { }).resourceVersion || 0;
b = (b.metadata || { }).resourceVersion || 0;
return b - a;
}
function compareCreated(a, b) {
a = a.items ? a.items[0] : {};
b = b.items ? b.items[0] : {};
a = a.created || "";
b = b.created || "";
return (b < a ? -1 : (b > a ? 1 : 0));
}
select.register("buildRecentStreams", function() {
var link;
var array = [];
for (link in this)
array.push(this[link]);
array.sort(compareVersion);
array.splice(MAX_RECENT_STREAMS);
var result = [];
var status, tags, stream, i, len, total;
for (i = 0, len = array.length; i < len; i++) {
stream = array[i];
status = stream.status || { };
tags = (status.tags || []).slice();
tags.sort(compareCreated);
total = tags.length;
tags.splice(MAX_RECENT_TAGS);
if (tags.length > 0)
result.push({ stream: stream, tags: tags, truncated: total > tags.length });
}
return result;
});
function setShowDockerPushCommands(visible) {
if (visible != $scope.showDockerPushCommands) {
$scope.showDockerPushCommands = visible;
$scope.$applyAsync();
}
}
function updateShowDockerPushCommands() {
var ns = filter.namespace();
if (ns) {
discoverSettings().then(function(settings) {
projectPolicy.subjectAccessReview(ns, settings.currentUser, 'update', 'imagestreamimages')
.then(setShowDockerPushCommands);
});
} else {
// no current project, always show push commands; too expensive to iterate through all projects
setShowDockerPushCommands(true);
}
}
// watch for project changes to update showDockerPushCommands, and initialize it
$scope.$on("$routeUpdate", updateShowDockerPushCommands);
updateShowDockerPushCommands();
$scope.createProject = projectActions.createProject;
$scope.createImageStream = imageActions.createImageStream;
$scope.sharedImages = projectData.sharedImages;
$scope.recentlyUpdated = function recentlyUpdated() {
return select().kind("ImageStream")
.buildRecentStreams();
};
$scope.projects = function projects() {
return select().kind("Project")
.statusPhase("Active");
};
$scope.filter = filter;
}
]);
}());

View File

@ -1,151 +0,0 @@
/*
* 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 REGTAG = /[\u200B\s,]+/;
function parseNodes(parent) {
var child, text;
var names = [];
function pushName(name) {
if (name)
names.push(name);
}
for (child = parent.firstChild; child; child = child.nextSibling) {
text = "";
if (child.nodeType == 3)
text = child.nodeValue.trim();
else if (child.nodeType == 1 && child.hasAttribute("value"))
text = child.getAttribute("value");
text.split(REGTAG).forEach(pushName);
}
return names;
}
function buildNodes(names) {
var elements = [ document.createTextNode("\u200B") ];
angular.forEach(names, function(name) {
var span = document.createElement("span");
span.setAttribute("contenteditable", "false");
span.setAttribute("class", "image-tag");
span.setAttribute("value", name);
span.appendChild(document.createTextNode(name));
var close = document.createElement("a");
close.setAttribute("class", "pficon pficon-close");
span.appendChild(close);
elements.push(span);
elements.push(document.createTextNode("\u00A0"));
});
return elements;
}
function parseSpec(spec) {
var names = [];
angular.forEach(spec.tags || [ ], function(tag) {
names.push(tag.name);
});
return names;
}
function buildSpec(names, spec, insecure, pull) {
var already = { };
if (!spec)
spec = { };
angular.forEach(spec.tags || [], function(tag) {
already[tag.name] = tag;
});
var tags = [ ];
angular.forEach(names, function(name) {
if (name in already) {
already[name].importPolicy = { "insecure": insecure };
tags.push(already[name]);
} else {
tags.push({
name: name,
"importPolicy": { "insecure": insecure },
from: {
kind: "DockerImage",
name: pull + ":" + name,
},
});
}
});
spec.tags = tags;
return spec;
}
angular.module('registry.tags', [ ])
.directive('imageTagEditor', [
function() {
return {
restrict: 'A',
transclude: true,
scope: {
tags: "=",
},
link: function(scope, element, attrs) {
element.addClass("image-tag-editor");
element.attr("tabindex", "0");
element.attr("contenteditable", "true");
var spans = buildNodes(scope.tags);
element.append(spans);
/* Select the last item when we get focus */
var range = document.createRange();
range.selectNodeContents(spans[spans.length - 1]);
range.collapse(false);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
element.on("click", function(ev) {
var target = ev.target;
var span = target.parentNode;
if (target.nodeName.toLowerCase() == "a" && span.nodeName.toLowerCase() == "span")
span.parentNode.removeChild(span);
});
/* When things change retag */
element.on("blur keyup paste copy cut mouseup", function() {
var tags = parseNodes(element[0]);
while (scope.tags.length > 0)
scope.tags.pop();
tags.forEach(function(tag) {
scope.tags.push(tag);
});
});
}
};
}
])
.factory('imageTagData', [
function() {
return {
parseSpec: parseSpec,
buildSpec: buildSpec,
buildNodes: buildNodes,
parseNodes: parseNodes,
};
}
]);
}());

View File

@ -1,379 +0,0 @@
/*
* 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/>.
*/
import QUnit from "qunit-tests";
var angular = require("angular");
require("./connection");
(function() {
var fixtures = [];
var configJson;
/* Filled in with a function */
var inject;
var module = angular.module("kubernetes.connection.tests", [
"kubeClient",
'kubeClient.cockpit',
"kubernetes.connection",
]);
function injectLoadFixtures(fixtures) {
inject([
"kubeLoader",
function(loader, data) {
loader.reset(true);
if (fixtures)
loader.handle(fixtures);
}
]);
}
QUnit.test("sessionCertificates test", function(assert) {
var done = assert.async();
assert.expect(5);
injectLoadFixtures(fixtures);
inject([
"sessionCertificates",
function(sessionCertificates) {
sessionCertificates.trustCert(null, "data");
assert.equal(sessionCertificates.getCert("localhost"), "data", "data retrive");
assert.equal(sessionCertificates.getCert(null), "data", "null is localhost");
sessionCertificates.trustCert({}, "data1");
assert.equal(sessionCertificates.getCert("localhost"), "data1", "blank server retrive");
assert.equal(sessionCertificates.getCert("address"), undefined, "missing is undefined");
sessionCertificates.trustCert({ server: "address" }, "address-data");
assert.equal(sessionCertificates.getCert("address"), undefined, "address data");
done();
}
]);
});
QUnit.test("cockpitKubectlConfig parseKubeConfig", function (assert) {
var done = assert.async();
assert.expect(5);
injectLoadFixtures(fixtures);
inject(["cockpitKubectlConfig", function (ckg) {
var alpha = {
"address": "alfa.org",
"headers": {
"Authorization": "Bearer provider-token"
},
"port": 443,
"tls": {
"validate": false,
"authority": undefined,
"certificate": undefined,
"key": undefined,
}
};
var bravo = {
"address": "bravo.org",
"headers": {
"Authorization": "Bearer provider-access-token"
},
"port": 8080,
"tls": {
"authority": {
"file": "cert-authority-file"
},
"certificate": undefined,
"key": undefined,
"validate": true
}
};
var charlie = {
"address": "charlie.org",
"headers": {
"Authorization": "Bearer token"
},
"port": 8080
};
var delta1 = {
"address": "delta.org",
"headers": {},
"port": 443,
"tls": {
"authority": undefined,
"certificate": {
"file": "cert-file"
},
"key": {
"file": "key-file"
},
"validate": true
}
};
var delta2 = {
"address": "delta.org",
"headers": {
"Authorization": "Basic dXNlcjpwYXNzd29yZA=="
},
"port": 443,
"tls": {
"validate": true,
"authority": undefined,
"certificate": undefined,
"key": undefined,
}
};
var configData = JSON.stringify(configJson);
assert.deepEqual(ckg.parseKubeConfig(configData), alpha);
assert.deepEqual(ckg.parseKubeConfig(configData, "bravo-with-access-token-auth-provider"), bravo);
assert.deepEqual(ckg.parseKubeConfig(configData, "charlie-with-token"), charlie);
assert.deepEqual(ckg.parseKubeConfig(configData, "delta-with-cert"), delta1);
assert.deepEqual(ckg.parseKubeConfig(configData, "delta-with-basic"), delta2);
done();
}]);
});
QUnit.test("connectionActions prepareData", function (assert) {
var done = assert.async();
assert.expect(6);
injectLoadFixtures(fixtures);
inject(["connectionActions", function(connectionActions) {
var cluster, context, user, data, config;
data = connectionActions.prepareData();
assert.deepEqual(data, {
"cluster": {
"cluster": {
"server": "http://localhost:8080"
},
"name": "localhost:8080"
},
"context": {
"context": {
"cluster": "localhost:8080",
"user": undefined
},
"name": "localhost:8080/noauth"
},
"user": undefined
}, "got right empty values");
cluster = {
"cluster": { "server": "https://127.0.0.1:8000" },
"name": "name"
};
user = {
"user": { "token": "token" },
"name": "user"
};
context = {
"context": {
"user": "user",
"cluster": "name"
},
name : "existing"
};
config = {
users: [user],
clusters: [cluster],
contexts: [ context ]
};
data = connectionActions.prepareData(config, cluster, user);
assert.deepEqual(data, {
"cluster": {
"cluster": { "server": "https://127.0.0.1:8000" },
"name": "name"
},
"context": { "name" : "existing" },
"user": {
"user": { "token": "token" },
"name": "user"
}
}, "matched existing");
// remove names
delete cluster.name;
delete user.name;
data = connectionActions.prepareData({}, cluster, user);
assert.deepEqual(data, {
"cluster": {
"cluster": { "server": "https://127.0.0.1:8000" },
"name": "127-0-0-1:8000"
},
"context": {
"name": "127-0-0-1:8000/user/127-0-0-1:8000",
"context": {
"user": "user/127-0-0-1:8000",
"cluster": "127-0-0-1:8000"
}
},
"user": {
"user": { "token": "token" },
"name": "user/127-0-0-1:8000"
},
}, "generated names");
// No dups
config = {
contexts: [{ "name": "127-0-0-1:8000/user/127-0-0-1:8000" }],
};
data = connectionActions.prepareData(config, cluster, user);
var pos = data.context.name.indexOf("127-0-0-1:8000/user/127-0-0-1:8000");
assert.ok(data.context.name != "127-0-0-1:8000/user/127-0-0-1:8000" && pos === 0, "dedup context name");
config = {
clusters: [{ "name": "127-0-0-1:8000" }],
};
delete cluster.name;
data = connectionActions.prepareData(config, cluster, user);
pos = data.cluster.name.indexOf("127-0-0-1:8000");
assert.ok(data.cluster.name != "127-0-0-1:8000" && pos === 0, "dedup cluster name");
config = {
users: [{ "name": "user/127-0-0-1:8000" }],
};
delete user.name;
data = connectionActions.prepareData(config, cluster, user);
pos = data.user.name.indexOf("user/127-0-0-1:8000");
assert.ok(data.user.name != "user/127-0-0-1:8000" && pos === 0, "dedup user name");
done();
}]);
});
angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
return function(exception, cause) {
exception.message += ' (caused by "' + cause + '")';
throw exception;
};
});
configJson = {
"clusters": [{
"name": "alfa",
"cluster": {
"insecure-skip-tls-verify": true,
"server": "https://alfa.org"
}
}, {
"name": "bravo",
"cluster": {
"server": "https://bravo.org:8080",
"certificate-authority": "cert-authority-file"
}
}, {
"name": "charlie",
"cluster": {
"server": "http://charlie.org"
}
}, {
"name": "delta",
"cluster": {
"server": "https://delta.org:443"
}
}],
"contexts": [{
"name": "alfa-with-token-auth-provider",
"context": {
"cluster": "alfa",
"user": "token-auth-provider"
}
}, {
"name": "bravo-with-access-token-auth-provider",
"context": {
"cluster": "bravo",
"user": "access-token-auth-provider"
}
}, {
"name": "charlie-with-token",
"context": {
"cluster": "charlie",
"user": "token"
}
}, {
"name": "delta-with-cert",
"context": {
"cluster": "delta",
"user": "cert"
}
}, {
"name": "delta-with-basic",
"context": {
"cluster": "delta",
"user": "basic"
}
}],
"current-context": "alfa-with-token-auth-provider",
"users": [{
"name": "token-auth-provider",
"user": {
"auth-provider": {
"config": {
"token": "provider-token"
},
"name": "gcp"
}
}
}, {
"name": "access-token-auth-provider",
"user": {
"auth-provider": {
"config": {
"access-token": "provider-access-token"
},
"name": "gcp"
}
}
}, {
"name": "token",
"user": {
"token": "token"
}
}, {
"name": "cert",
"user": {
"client-certificate": "cert-file",
"client-key": "key-file"
}
}, {
"name": "basic",
"user": {
"username": "user",
"password": "password"
}
}]
};
module.run([
'$injector',
function($injector) {
inject = function inject(func) {
return $injector.invoke(func);
};
QUnit.start();
}
]);
angular.bootstrap(document, ['kubernetes.connection.tests']);
}());

File diff suppressed because one or more lines are too long

View File

@ -1,920 +0,0 @@
/*
* 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/>.
*/
import { FIXTURE_BASIC } from "./fixture-basic.js";
import { FIXTURE_LARGE } from "./fixture-large.js";
import QUnit from "qunit-tests";
var angular = require("angular");
require("./kube-client");
require("./kube-client-cockpit");
require("./kube-client-mock");
(function() {
/* Filled in with a function */
var inject;
var module = angular.module("kubeClient.tests", [
"kubeClient",
"kubeClient.mock"
])
.config([
'KubeWatchProvider',
'KubeRequestProvider',
function(KubeWatchProvider, KubeRequestProvider) {
KubeWatchProvider.KubeWatchFactory = "MockKubeWatch";
KubeRequestProvider.KubeRequestFactory = "MockKubeRequest";
}
]);
function injectLoadFixtures(fixtures) {
inject([
"kubeLoader",
"MockKubeData",
function(loader, data) {
if (fixtures)
data.load(fixtures);
loader.reset(true);
}
]);
}
QUnit.test("loader load", function (assert) {
var done = assert.async();
assert.expect(7);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", function(loader) {
var promise = loader.load("nodes");
assert.ok(!!promise, "promise returned");
assert.equal(typeof promise.then, "function", "promise has then");
assert.equal(typeof promise.catch, "function", "promise has catch");
assert.equal(typeof promise.finally, "function", "promise has finally");
return promise.then(function(items) {
assert.ok(angular.isArray(items), "got items array");
assert.equal(items.length, 1, "one node");
assert.equal(items[0].metadata.name, "127.0.0.1", "localhost node");
done();
});
}]);
});
QUnit.test("loader load encoding", function (assert) {
var done = assert.async();
assert.expect(2);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeSelect", "$q", function(loader, select, $q) {
assert.equal(select().kind("Encoded").length, 0);
var defer = $q.defer();
var x = loader.listen(function() {
assert.equal(select().kind("Image").length, 1);
x.cancel();
defer.resolve();
done();
});
loader.handle([{
"apiVersion": "v1",
"kind": "Image",
"metadata": {
"name": "encoded:one",
"resourceVersion": 10000,
"uid": "11768037-ab8a-11e4-9a7c-100001001",
"namespace": "default",
"selfLink": "/oapi/v1/images/encoded%3Aone",
},
}, {
"apiVersion": "v1",
"kind": "Image",
"metadata": {
"name": "encoded:one",
"resourceVersion": 10000,
"uid": "11768037-ab8a-11e4-9a7c-100001001",
"namespace": "default",
},
}, {
"apiVersion": "v1",
"kind": "Image",
"metadata": {
"name": "encoded:one",
"resourceVersion": 10000,
"uid": "11768037-ab8a-11e4-9a7c-100001001",
"namespace": "default",
"selfLink": "/oapi/v1/images/encoded:one",
},
}]);
return defer.promise;
}]);
});
QUnit.test("loader load fail", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", function(loader) {
var promise = loader.load("nonexistant");
return promise.then(function(data) {
assert.ok(!true, "successfully loaded");
}, function(response) {
assert.equal(response.code, 404, "not found");
assert.equal(response.message, "Not found here", "not found message");
assert.ok(true, "not sucessfully loaded");
done();
});
}]);
});
QUnit.test("loader watch", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", function(loader) {
return loader.watch("nodes").then(function(response) {
assert.ok("/api/v1/nodes/127.0.0.1" in loader.objects, "found node");
var node = loader.objects["/api/v1/nodes/127.0.0.1"];
assert.equal(node.metadata.name, "127.0.0.1", "localhost node");
assert.equal(typeof node.spec.capacity, "object", "node has resources");
done();
});
}]);
});
QUnit.test("list nodes", function (assert) {
var done = assert.async();
assert.expect(6);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeSelect", function(loader, select) {
return loader.watch("nodes").then(function() {
var nodes = select().kind("Node");
assert.ok("/api/v1/nodes/127.0.0.1" in nodes, "found node");
var node = nodes["/api/v1/nodes/127.0.0.1"];
assert.equal(node.metadata.name, "127.0.0.1", "localhost node");
assert.equal(typeof node.spec.capacity, "object", "node has resources");
/* The same thing should be returned */
var nodes1 = select().kind("Node");
assert.strictEqual(nodes, nodes1, "same object returned");
/* Key should not be encoded as JSON */
var parsed = JSON.parse(JSON.stringify(node));
assert.ok(!("key" in parsed), "key should not be serialized");
assert.strictEqual(parsed.key, undefined, "key not be undefined after serialize");
done();
});
}]);
});
QUnit.test("list pods", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeSelect", function(loader, select) {
return loader.watch("pods").then(function() {
var pods = select().kind("Pod");
assert.equal(pods.length, 3, "found pods");
var pod = pods["/api/v1/namespaces/default/pods/apache"];
assert.equal(typeof pod, "object", "found pod");
assert.equal(pod.metadata.labels.name, "apache", "pod has label");
done();
});
}]);
});
QUnit.test("set namespace", function (assert) {
var done = assert.async();
assert.expect(7);
injectLoadFixtures(FIXTURE_BASIC);
inject(["$q", "kubeLoader", "kubeSelect", function($q, loader, select) {
return loader.watch("pods").then(function() {
var pods = select().kind("Pod");
assert.equal(pods.length, 3, "number of pods");
assert.strictEqual(loader.limits.namespace, null, "namespace is null");
loader.limit({ namespace: "other" });
assert.strictEqual(loader.limits.namespace, "other", "namespace is other");
pods = select().kind("Pod");
assert.equal(pods.length, 1, "pods from namespace other");
assert.ok("/api/v1/namespaces/other/pods/apache" in pods, "other pod");
loader.limit({ namespace: null });
assert.strictEqual(loader.limits.namespace, null, "namespace is null again");
var defer = $q.defer();
var listened = false;
var x = loader.listen(function() {
if (listened) {
pods = select().kind("Pod");
assert.equal(pods.length, 3, "all pods back");
x.cancel();
defer.resolve();
done();
}
listened = true;
});
return defer.promise;
});
}]);
});
QUnit.test("add pod", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(FIXTURE_BASIC);
inject(["$q", "kubeLoader", "kubeSelect", "MockKubeData", function($q, loader, select, data) {
return loader.watch("pods").then(function() {
var pods = select().kind("Pod");
assert.equal(pods.length, 3, "number of pods");
assert.equal(pods["/api/v1/namespaces/default/pods/apache"].metadata.labels.name,
"apache", "pod has label");
var defer = $q.defer();
var x = loader.listen(function() {
var pods = select().kind("Pod");
if (pods.length === 4) {
assert.equal(pods["/api/v1/namespaces/default/pods/aardvark"].metadata.labels.name,
"aardvark", "new pod present in items");
x.cancel();
defer.resolve();
done();
}
});
data.update("namespaces/default/pods/aardvark", {
"kind": "Pod",
"metadata": {
"name": "aardvark",
"uid": "22768037-ab8a-11e4-9a7c-080027300d85",
"namespace": "default",
"labels": {
"name": "aardvark"
},
},
"spec": {
"volumes": null,
"containers": [ ],
"imagePullPolicy": "IfNotPresent"
}
});
return defer.promise;
});
}]);
});
QUnit.test("update pod", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(FIXTURE_BASIC);
inject(["$q", "kubeLoader", "kubeSelect", "MockKubeData", function($q, loader, select, data) {
return loader.watch("pods").then(function() {
var pods = select().kind("Pod");
assert.equal(pods.length, 3, "number of pods");
assert.equal(pods["/api/v1/namespaces/default/pods/apache"].metadata.labels.name,
"apache", "pod has label");
var defer = $q.defer();
var listened = false;
var x = loader.listen(function() {
var pods;
if (listened) {
pods = select().kind("Pod");
assert.equal(pods["/api/v1/namespaces/default/pods/apache"].metadata.labels.name,
"apachepooo", "pod has changed");
x.cancel();
defer.resolve();
done();
}
listened = true;
});
data.update("namespaces/default/pods/apache", {
"kind": "Pod",
"metadata": {
"name": "apache",
"uid": "11768037-ab8a-11e4-9a7c-080027300d85",
"namespace": "default",
"labels": {
"name": "apachepooo"
},
}
});
return defer.promise;
});
}]);
});
QUnit.test("remove pod", function (assert) {
var done = assert.async();
assert.expect(5);
injectLoadFixtures(FIXTURE_BASIC);
inject(["$q", "kubeLoader", "kubeSelect", "MockKubeData", function($q, loader, select, data) {
return loader.watch("pods").then(function() {
var pods = select().kind("Pod");
assert.equal(pods.length, 3, "number of pods");
assert.equal(pods["/api/v1/namespaces/default/pods/apache"].metadata.labels.name,
"apache", "pod has label");
var defer = $q.defer();
var listened = false;
var x = loader.listen(function() {
var pods;
if (listened) {
pods = select().kind("Pod");
assert.equal(pods.length, 2, "removed a pod");
assert.strictEqual(pods["/api/v1/namespaces/default/pods/apache"], undefined, "removed pod");
assert.equal(pods["/api/v1/namespaces/default/pods/database-1"].metadata.labels.name,
"wordpressreplica", "other pod");
x.cancel();
defer.resolve();
done();
}
listened = true;
});
data.update("namespaces/default/pods/apache", null);
return defer.promise;
});
}]);
});
QUnit.test("list services", function (assert) {
var done = assert.async();
assert.expect(4);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeSelect", function(loader, select) {
return loader.watch("services").then(function() {
var services = select().kind("Service");
var x;
var svc = null;
for (x in services) {
svc = services[x];
break;
}
assert.ok(!!svc, "got a service");
assert.equal(services.length, 2, "number of services");
assert.equal(svc.metadata.name, "kubernetes", "service id");
assert.equal(svc.spec.selector.component, "apiserver", "service has label");
done();
});
}]);
});
var CREATE_ITEMS = [
{
"kind": "Pod",
"apiVersion": "v1",
"metadata": {
"name": "pod1",
"uid": "d072fb85-f70e-11e4-b829-10c37bdb8410",
"resourceVersion": "634203",
"labels": {
"name": "pod1"
},
},
"spec": {
"volumes": null,
"containers": [{
"name": "database",
"image": "mysql",
"ports": [{ "containerPort": 3306, "protocol": "TCP" }],
}],
"nodeName": "127.0.0.1"
}
}, {
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "node1",
"uid": "6e51438e-d161-11e4-acbc-10c37bdb8410",
"resourceVersion": "634539",
},
"status": {
"addresses": [
{
"address": "172.2.3.1",
"type": "ExternalIP"
}
]
}
}
];
QUnit.test("create", function (assert) {
var done = assert.async();
assert.expect(2);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
loader.watch("pods");
loader.watch("nodes");
loader.watch("namespaces");
return methods.create(CREATE_ITEMS, "namespace1").then(function() {
assert.equal(loader.objects["/api/v1/namespaces/namespace1/pods/pod1"].metadata.name, "pod1", "pod object");
assert.equal(loader.objects["/api/v1/nodes/node1"].metadata.name, "node1", "node object");
done();
});
}]);
});
QUnit.test("create namespace exists", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
loader.watch("pods");
loader.watch("nodes");
loader.watch("namespaces");
var NAMESPACE_ITEM = {
"apiVersion" : "v1",
"kind" : "Namespace",
"metadata" : { "name": "namespace1" }
};
return methods.create(NAMESPACE_ITEM).then(function() {
assert.ok("/api/v1/namespaces/namespace1" in loader.objects, "namespace created");
return methods.create(CREATE_ITEMS, "namespace1").then(function() {
assert.ok("/api/v1/namespaces/namespace1/pods/pod1" in loader.objects, "pod created");
assert.ok("/api/v1/nodes/node1" in loader.objects, "node created");
done();
});
});
}]);
});
QUnit.test("create namespace default", function (assert) {
var done = assert.async();
assert.expect(2);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
loader.watch("pods");
loader.watch("nodes");
loader.watch("namespaces");
return methods.create(CREATE_ITEMS).then(function() {
assert.equal(loader.objects["/api/v1/namespaces/default/pods/pod1"].metadata.name, "pod1", "pod created");
assert.equal(loader.objects["/api/v1/nodes/node1"].metadata.name, "node1", "node created");
done();
});
}]);
});
QUnit.test("create object exists", function (assert) {
var done = assert.async();
assert.expect(1);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
loader.watch("pods");
loader.watch("nodes");
loader.watch("namespaces");
var items = CREATE_ITEMS.slice();
items.push(items[0]);
return methods.create(items).then(function(response) {
assert.equal(response, false, "should have failed");
done();
}, function(response) {
assert.equal(response.code, 409, "http already exists");
done();
});
}]);
});
QUnit.test("delete pod", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
var watch = loader.watch("pods");
return methods.create(CREATE_ITEMS, "namespace2").then(function() {
assert.ok("/api/v1/namespaces/namespace2/pods/pod1" in loader.objects, "pod created");
return methods.delete("/api/v1/namespaces/namespace2/pods/pod1").then(function() {
assert.ok(true, "remove succeeded");
return watch.finally(function() {
assert.ok(!("/api/v1/namespaces/namespace2/pods/pod1" in loader.objects), "pod was removed");
done();
});
});
});
}]);
});
QUnit.test("patch pod", function (assert) {
var done = assert.async();
assert.expect(4);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
var watch = loader.watch("pods");
var path = "/api/v1/namespaces/namespace2/pods/pod1";
return methods.create(CREATE_ITEMS, "namespace2").then(function() {
assert.ok(path in loader.objects, "pod created");
return methods.patch(path, { "extra": "blah" }).then(function() {
assert.ok(true, "patch succeeded");
return methods.patch(loader.objects[path], { "second": "test" }).then(function() {
return watch.finally(function() {
var pod = loader.objects[path];
assert.equal(pod.extra, "blah", "pod has changed");
assert.equal(pod.second, "test", "pod changed by own object");
done();
});
});
});
});
}]);
});
QUnit.test("post", function (assert) {
var done = assert.async();
assert.expect(1);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
return methods.post("/api/v1/namespaces/namespace1/pods", CREATE_ITEMS[0]).then(function(response) {
assert.equal(response.metadata.name, "pod1", "pod object");
done();
});
}]);
});
QUnit.test("post fail", function (assert) {
var done = assert.async();
assert.expect(1);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
return methods.post("/api/v1/nodes", FIXTURE_BASIC["nodes/127.0.0.1"]).then(function() {
assert.ok(false, "shouldn't succeed");
}, function(response) {
assert.deepEqual(response, { "code": 409, "message": "Already exists" }, "got failure code");
done();
});
}]);
});
QUnit.test("put", function (assert) {
var done = assert.async();
assert.expect(1);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeMethods", function(loader, methods) {
var node = { "kind": "Node", "metadata": { "name": "127.0.0.1", labels: { "test": "value" } } };
return methods.put("/api/v1/nodes/127.0.0.1", node).then(function(response) {
assert.deepEqual(response.metadata.labels, { "test": "value" }, "put returned object");
done();
});
}]);
});
QUnit.test("check resource ok", function (assert) {
var done = assert.async();
assert.expect(0);
injectLoadFixtures(null);
inject(["kubeMethods", function(methods) {
var data = { kind: "Blah", metadata: { name: "test" } };
done();
return methods.check(data);
}]);
});
QUnit.test("check resource name empty", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(null);
inject(["kubeMethods", function(methods) {
var data = { kind: "Blah", metadata: { name: "" } };
return methods.check(data).catch(function(ex) {
assert.ok(angular.isArray(ex), "threw array of failures");
assert.equal(ex.length, 1, "number of errors");
assert.ok(ex[0] instanceof Error, "threw an error");
done();
});
}]);
});
QUnit.test("check resource name missing", function (assert) {
var done = assert.async();
assert.expect(1);
injectLoadFixtures(null);
inject(["kubeMethods", function(methods) {
var data = { kind: "Blah", metadata: { } };
return methods.check(data).then(function() {
assert.ok(true, "passed check");
done();
}, null);
}]);
});
QUnit.test("check resource name namespace bad", function (assert) {
var done = assert.async();
assert.expect(6);
injectLoadFixtures(null);
inject(["kubeMethods", function(methods) {
var data = { kind: "Blah", metadata: { name: "a#a", namespace: "" } };
var targets = { "metadata.name": "#name", "metadata.namespace": "#namespace" };
return methods.check(data, targets).catch(function(ex) {
assert.ok(angular.isArray(ex), "threw array of failures");
assert.equal(ex.length, 2, "number of errors");
assert.ok(ex[0] instanceof Error, "threw an error");
assert.equal(ex[0].target, "#name", "correct name target");
assert.ok(ex[1] instanceof Error, "threw an error");
assert.equal(ex[1].target, "#namespace", "correct name target");
done();
});
}]);
});
QUnit.test("check resource namespace bad", function (assert) {
var done = assert.async();
assert.expect(4);
injectLoadFixtures(null);
inject(["kubeMethods", function(methods) {
var data = { kind: "Blah", metadata: { name: "aa", namespace: "" } };
var targets = { "metadata.name": "#name", "metadata.namespace": "#namespace" };
return methods.check(data, targets).catch(function(ex) {
assert.ok(angular.isArray(ex), "threw array of failures");
assert.equal(ex.length, 1, "number of errors");
assert.ok(ex[0] instanceof Error, "threw an error");
assert.equal(ex[0].target, "#namespace", "correct name target");
done();
});
}]);
});
QUnit.test("lookup uid", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeSelect", function(loader, select) {
return loader.watch("pods").then(function() {
/* Get the item */
var item = select().kind("Pod")
.one();
var uid = item.metadata.uid;
assert.ok(uid, "Have uid");
var by_uid_item = select().uid(uid)
.one();
assert.strictEqual(item, by_uid_item, "load uid");
/* Shouldn't match */
item = select().uid("bad")
.one();
assert.strictEqual(item, null, "mismatch uid");
done();
});
}]);
});
QUnit.test("lookup host", function (assert) {
var done = assert.async();
assert.expect(2);
injectLoadFixtures(FIXTURE_BASIC);
inject(["kubeLoader", "kubeSelect", function(loader, select) {
return loader.watch("pods").then(function() {
/* Get the item */
var item = select().host("127.0.0.1")
.one();
assert.deepEqual(item.metadata.selfLink, "/api/v1/namespaces/default/pods/database-1", "correct pod");
/* Shouldn't match */
item = select().host("127.0.0.2")
.one();
assert.strictEqual(item, null, "mismatch host");
done();
});
}]);
});
QUnit.test("lookup", function (assert) {
var done = assert.async();
assert.expect(6);
injectLoadFixtures(FIXTURE_LARGE);
inject(["kubeLoader", "kubeSelect", function(loader, select) {
var expected = {
"apiVersion": "v1",
"kind": "ReplicationController",
"metadata": { "labels": { "example": "mock", "name": "3controller" },
"name": "3controller",
"resourceVersion": 10000,
"uid": "11768037-ab8a-11e4-9a7c-100001001",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/replicationcontrollers/3controller",
},
"spec": { "replicas": 1, "selector": { "factor3": "yes" } }
};
return loader.watch("replicationcontrollers").then(function() {
/* Get the item */
var item = select().kind("ReplicationController")
.name("3controller")
.namespace("default")
.one();
assert.deepEqual(item, expected, "correct item");
/* The same item, without namespace */
item = select().kind("ReplicationController")
.name("3controller")
.one();
assert.deepEqual(item, expected, "selected without namespace");
/* Any replication controller */
item = select().kind("ReplicationController")
.one();
assert.equal(item.kind, "ReplicationController", "any replication controller");
/* Shouldn't match */
item = select().kind("BadKind")
.name("3controller")
.namespace("default")
.one();
assert.strictEqual(item, null, "mismatch kind");
item = select().kind("ReplicationController")
.name("badcontroller")
.namespace("default")
.one();
assert.strictEqual(item, null, "mismatch name");
item = select().kind("ReplicationController")
.name("3controller")
.namespace("baddefault")
.one();
assert.strictEqual(item, null, "mismatch namespace");
done();
});
}]);
});
QUnit.test("select", function (assert) {
var done = assert.async();
assert.expect(12);
injectLoadFixtures(FIXTURE_LARGE);
inject(["kubeLoader", "kubeSelect", function(loader, select) {
return loader.watch("pods").then(function() {
var image = { kind: "Image" };
/* same thing twice */
var first = select(image);
var second = select(image);
assert.strictEqual(first, second, "identical for single object");
/* null thing twice */
first = select(null);
second = select(null);
assert.strictEqual(first, second, "identical for null object");
/* Select everything odd, 500 pods */
var results = select().namespace("default")
.label({ "type": "odd" });
assert.equal(results.length, 500, "correct amount");
/* The same thing should be returned */
var results1 = select().namespace("default")
.label({ "type": "odd" });
assert.strictEqual(results, results1, "same object returned");
/* Select everything odd, but wrong namespace, no pods */
results = select().namespace("other")
.label({ "type": "odd" });
assert.equal(results.length, 0, "other namespace no pods");
/* The same ones selected even when a second (present) label */
results = select().namespace("default")
.label({ "type": "odd", "tag": "silly" });
assert.equal(results.length, 500, "with additional label");
/* Nothing selected when additional invalid field */
results = select().namespace("default")
.label({ "type": "odd", "tag": "billy" });
assert.equal(results.length, 0, "no objects");
/* Limit by kind */
results = select().kind("Pod")
.namespace("default")
.label({ "type": "odd" });
assert.equal(results.length, 500, "by kind");
/* Limit by invalid kind */
results = select().kind("Ood")
.namespace("default")
.label({ "type": "odd" });
assert.equal(results.length, 0, "nothing for invalid kind");
/* Everything selected when no selector */
results = select().namespace("default");
assert.equal(results.length, 1000, "all pods");
/* Nothing selected when bad namespace */
results = select().namespace("bad");
assert.equal(results.length, 0, "bad namespace no objects");
/* Nothing selected when empty selector */
results = select().label({ });
assert.equal(results.length, 0, "nothing selected");
done();
});
}]);
});
angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
return function(exception, cause) {
exception.message += ' (caused by "' + cause + '")';
throw exception;
};
});
module.run([
'$injector',
function($injector) {
inject = function inject(func) {
return $injector.invoke(func);
};
QUnit.start();
}
]);
angular.bootstrap(document, ['kubeClient.tests']);
}());

View File

@ -1,309 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import QUnit from "qunit-tests";
var angular = require("angular");
require("./nodes");
require("./kube-client-cockpit");
function suite(fixtures) {
/* Filled in with a function */
var inject;
var module = angular.module("kubernetes.nodes.tests", [
"kubeClient",
'kubeClient.cockpit',
"kubernetes.nodes",
])
.config([
'KubeTranslateProvider',
'KubeFormatProvider',
function(KubeTranslateProvider, KubeFormatProvider) {
KubeTranslateProvider.KubeTranslateFactory = "CockpitTranslate";
KubeFormatProvider.KubeFormatFactory = "CockpitFormat";
}
]);
function injectLoadFixtures(fixtures) {
inject([
"kubeLoader",
function(loader, data) {
loader.reset(true);
if (fixtures)
loader.handle(fixtures);
}
]);
}
QUnit.test("nodeStatus tests", function (assert) {
var done = assert.async();
assert.expect(10);
injectLoadFixtures(fixtures);
inject(["nodeData", "kubeSelect", function(nodeData, select) {
var nodes = select().kind("Node");
assert.equal(nodeData.nodeStatus(), "Unknown", "undefined node");
assert.equal(nodeData.nodeStatus({}), "Unknown", "empty node");
assert.equal(nodeData.nodeStatus(nodes['/api/v1/nodes/unknown-node']), "Unknown", "no conditions");
assert.equal(nodeData.nodeStatus(nodes['/api/v1/nodes/unknown-node2']), "Unknown", "no ready condition");
assert.equal(nodeData.nodeStatus(nodes['/api/v1/nodes/failed-node']), "Not Ready", "failed");
assert.equal(nodeData.nodeStatus(nodes['/api/v1/nodes/ready-node']), "Ready", "ready");
assert.equal(nodeData.nodeStatus(nodes['/api/v1/nodes/unschedulable-node']), "Scheduling Disabled", "ready but unschedulable");
assert.equal(nodeData.nodeStatus(nodes['/api/v1/nodes/unschedulable-failed-node']), "Not Ready", "not ready and unschedulable");
assert.equal(nodeData.nodeStatus(nodes['/api/v1/nodes/disk-node']), "Ready", "out of disk");
assert.equal(nodeData.nodeStatus(nodes['/api/v1/nodes/mem-node']), "Ready", "out of memory");
done();
}]);
});
QUnit.test("nodeStatusIcon tests", function (assert) {
var done = assert.async();
assert.expect(10);
injectLoadFixtures(fixtures);
inject(["nodeData", "kubeSelect", function(nodeData, select) {
var nodes = select().kind("Node");
assert.equal(nodeData.nodeStatusIcon(), "wait", "undefined node");
assert.equal(nodeData.nodeStatusIcon({}), "wait", "empty node");
assert.equal(nodeData.nodeStatusIcon(nodes['/api/v1/nodes/unknown-node']), "wait", "no conditions");
assert.equal(nodeData.nodeStatusIcon(nodes['/api/v1/nodes/unknown-node2']), "wait", "no ready condition");
assert.equal(nodeData.nodeStatusIcon(nodes['/api/v1/nodes/failed-node']), "fail", "failed");
assert.equal(nodeData.nodeStatusIcon(nodes['/api/v1/nodes/ready-node']), "");
assert.equal(nodeData.nodeStatusIcon(nodes['/api/v1/nodes/unschedulable-node']), "", "ready but unschedulable");
assert.equal(nodeData.nodeStatusIcon(nodes['/api/v1/nodes/unschedulable-failed-node']), "fail", "not ready and unschedulable");
assert.equal(nodeData.nodeStatusIcon(nodes['/api/v1/nodes/disk-node']), "warn", "out of disk");
assert.equal(nodeData.nodeStatusIcon(nodes['/api/v1/nodes/mem-node']), "warn", "out of memory");
done();
}]);
});
QUnit.test("nodeConditions tests", function (assert) {
var done = assert.async();
assert.expect(5);
injectLoadFixtures(fixtures);
inject(["nodeData", "kubeSelect", function(nodeData, select) {
var nodes = select().kind("Node");
assert.equal(nodeData.nodeConditions(), undefined);
assert.equal(nodeData.nodeConditions(null), undefined);
assert.deepEqual(nodeData.nodeConditions({}), {});
var conditions = nodeData.nodeConditions(nodes['/api/v1/nodes/unschedulable-node']);
assert.deepEqual(conditions, {
"OutOfDisk": {
"status": "False",
"type": "OutOfDisk"
},
"Ready": {
"status": "True",
"type": "Ready"
}
}, "Correct object");
assert.strictEqual(conditions, nodeData.nodeConditions(nodes['/api/v1/nodes/unschedulable-node']), "identical when called with the same object");
done();
}]);
});
angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
return function(exception, cause) {
exception.message += ' (caused by "' + cause + '")';
throw exception;
};
});
module.run([
'$injector',
function($injector) {
inject = function inject(func) {
return $injector.invoke(func);
};
QUnit.start();
}
]);
angular.bootstrap(document, ['kubernetes.nodes.tests']);
}
/* Invoke the test suite with this data */
suite([
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "unknown-node",
"selfLink": "/api/v1/nodes/unknown-node",
"uid": "4920ab25-1092-11e6-a03c-5254009e00f1",
"resourceVersion": "17152",
},
"status": {
}
},
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "unknown-node2",
"selfLink": "/api/v1/nodes/unknown-node2",
"uid": "4920ab25-1092-11e6-a03c-5254009e00f2",
"resourceVersion": "17152",
},
"status": {
"conditions": [
{
"type": "Other",
"status": "Other",
},
],
}
},
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "failed-node",
"selfLink": "/api/v1/nodes/failed-node",
"uid": "4920ab25-1092-11e6-a03c-5254009e00f3",
"resourceVersion": "17152",
},
"status": {
"conditions": [
{
"type": "Ready",
"status": "Other",
},
],
}
},
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "ready-node",
"selfLink": "/api/v1/nodes/ready-node",
"uid": "4920ab25-1092-11e6-a03c-5254009e00f4",
"resourceVersion": "17152",
},
"status": {
"conditions": [
{
"type": "Ready",
"status": "True",
},
],
}
},
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "unschedulable-node",
"selfLink": "/api/v1/nodes/unschedulable-node",
"uid": "4920ab25-1092-11e6-a03c-5254009e00f5",
"resourceVersion": "17152",
},
"spec": {
"unschedulable": true
},
"status": {
"conditions": [
{
"type": "Ready",
"status": "True",
},
{
"type": "OutOfDisk",
"status": "False",
}
],
}
},
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "unschedulable-failed-node",
"selfLink": "/api/v1/nodes/unschedulable-failed-node",
"uid": "4920ab25-1092-11e6-a03c-5254009e00f7",
"resourceVersion": "17152",
},
"spec": {
"unschedulable": true
},
"status": {
"conditions": [
{
"type": "Ready",
"status": "False",
},
{
"type": "OutOfDisk",
"status": "False",
}
],
}
},
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "disk-node",
"selfLink": "/api/v1/nodes/disk-node",
"uid": "4920ab25-1092-11e6-a03c-5254009e00f8",
"resourceVersion": "17152",
},
"status": {
"conditions": [
{
"type": "Ready",
"status": "True",
},
{
"type": "OutOfDisk",
"status": "True",
}
],
}
},
{
"kind": "Node",
"apiVersion": "v1",
"metadata": {
"name": "mem-node",
"selfLink": "/api/v1/nodes/mem-node",
"uid": "4920ab25-1092-11e6-a03c-5254009e00f7",
"resourceVersion": "17152",
},
"status": {
"conditions": [
{
"type": "Ready",
"status": "True",
},
{
"type": "OutOfMemory",
"status": "True",
}
],
}
}
]);

View File

@ -1,404 +0,0 @@
/*
* 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/>.
*/
import QUnit from "qunit-tests";
var angular = require("angular");
require("./projects");
function suite(fixtures) {
/* Filled in with a function */
var inject;
var module = angular.module("registry.projects.tests", [
"kubeClient",
"registry.projects",
]);
function injectLoadFixtures(fixtures) {
inject([
"kubeLoader",
function(loader, data) {
loader.reset(true);
if (fixtures)
loader.handle(fixtures);
}
]);
}
QUnit.test("format Users", function (assert) {
var done = assert.async();
assert.expect(4);
injectLoadFixtures(fixtures);
inject(["projectData", 'kubeSelect', function(projectUtil, select) {
var user = select().kind("User")
.name("amanda");
assert.equal(user.length, 1, "number of users");
assert.equal(projectUtil.formatMembers(user.one().groups, 'Groups'),
"finance", "number of groups");
user = select().kind("User")
.name("jay");
assert.equal(projectUtil.formatMembers(user.one().groups, 'Groups'),
"4 Groups", "number of groups");
assert.equal(projectUtil.formatMembers(user.one().groups, 'Users'),
"4 Users", "number of users");
done();
}]);
});
QUnit.test("policy checks", function (assert) {
var done = assert.async();
assert.expect(11);
injectLoadFixtures(fixtures);
inject(["projectData", 'kubeSelect', function(projectUtil, select) {
var user = select().kind("User")
.name("amanda");
var policybinding = select().kind("PolicyBinding")
.namespace("financeprj")
.name(":default");
assert.equal(user.length, 1, "number of users");
assert.equal(policybinding.length, 1, "number of policybinding");
var rolesArray = projectUtil.getAllRoles("", "");
assert.equal(rolesArray.length, 0, "no values passed 1 ");
rolesArray = projectUtil.getAllRoles();
assert.equal(rolesArray.length, 0, "no values passed 2");
rolesArray = projectUtil.getAllRoles(user.one(), "financeprj");
assert.equal(rolesArray.length, 3, "values passed");
var regRolesArray = projectUtil.getRegistryRoles(user.one(), "financeprj");
assert.equal(regRolesArray[0], "Admin", "getRegistryRoles displayRole values");
regRolesArray = projectUtil.getRegistryRoles("", "");
assert.equal(regRolesArray.length, 0, "no values passed 3 ");
regRolesArray = projectUtil.getRegistryRoles();
assert.equal(regRolesArray.length, 0, "no values passed 4");
assert.equal(projectUtil.isRegistryRole(user.one(), "Admin", "financeprj"), true, "check if Admin registry role");
assert.equal(projectUtil.isRegistryRole("system:unauthenticated", "Pull", "financeprj"), true, "check if Pull registry role");
assert.equal(projectUtil.isRoles(user.one(), "financeprj"), true, "check if any role exist");
done();
}]);
});
angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
return function(exception, cause) {
exception.message += ' (caused by "' + cause + '")';
throw exception;
};
});
module.run([
'$injector',
function($injector) {
inject = function inject(func) {
return $injector.invoke(func);
};
QUnit.start();
}
]);
angular.bootstrap(document, ['registry.projects.tests']);
}
/* Invoke the test suite with this data */
suite([
{
"kind": "User",
"apiVersion": "v1",
"metadata": {
"name": "amanda",
"selfLink": "/oapi/v1/users/amanda",
"uid": "8d10b355-b9d4-11e5-b7ad-5254009e00f1",
"resourceVersion": "1114",
"creationTimestamp": "2016-01-13T09:03:45Z"
},
"identities": [
"anypassword:abc123"
],
"groups": [
"finance"
]
},
{
"kind": "User",
"apiVersion": "v1",
"metadata": {
"name": "jay",
"selfLink": "/oapi/v1/users/jay",
"uid": "8d10b355-b9d4-11e5-b7ad-5254009e00f1",
"resourceVersion": "1114",
"creationTimestamp": "2016-01-13T09:03:45Z"
},
"identities": [
"anypassword:abc123"
],
"groups": [
"finance", "admin", "hr", "dev"
]
},
{
"kind": "Group",
"apiVersion": "v1",
"metadata": {
"name": "finance",
"selfLink": "/oapi/v1/groups/finance",
"uid": "bff4578c-b9d4-11e5-b7ad-5254009e00f1",
"resourceVersion": "1124",
"creationTimestamp": "2016-01-13T09:05:10Z"
},
"users": [
"tom",
"jay",
"amanda",
"myadmin"
]
},
{
"kind": "PolicyBinding",
"apiVersion": "v1",
"metadata": {
"name": ":default",
"namespace": "financeprj",
"selfLink": "/oapi/v1/namespaces/financeprj/policybindings/:default",
"uid": "d5a78dfc-e9e4-11e5-a1bd-3c970eb867f7",
"resourceVersion": "53908",
"creationTimestamp": "2016-03-14T13:01:15Z"
},
"lastModified": "2016-03-24T07:19:42Z",
"policyRef": {
"name": "default"
},
"roleBindings": [
{
"name": "admin",
"roleBinding": {
"metadata": {
"name": "admin",
"namespace": "financeprj",
"uid": "f33cbdd7-ea0e-11e5-ba57-3c970eb867f7",
"resourceVersion": "24003",
"creationTimestamp": "2016-03-14T18:02:43Z"
},
"userNames": null,
"groupNames": null,
"subjects": [
],
"roleRef": {
"name": "admin"
}
}
},
{
"name": "edit",
"roleBinding": {
"metadata": {
"name": "edit",
"namespace": "financeprj",
"selfLink": "/oapi/v1/namespaces/financeprj/rolebindings/edit",
"uid": "d5a340e0-e9e4-11e5-a1bd-3c970eb867f7",
"resourceVersion": "24002",
"creationTimestamp": "2016-03-14T13:01:15Z"
},
"userNames": [
"amanda"
],
"groupNames": null,
"subjects": [
{
"kind": "User",
"name": "amanda"
}
],
"roleRef": {
"name": "edit"
}
}
},
{
"name": "registry-admin",
"roleBinding": {
"metadata": {
"name": "registry-admin",
"namespace": "financeprj",
"uid": "c0746786-f0fd-11e5-b5cb-3c970eb867f7",
"resourceVersion": "24005",
"creationTimestamp": "2016-03-23T13:47:15Z"
},
"userNames": [
"amanda"
],
"groupNames": null,
"subjects": [
{
"kind": "User",
"name": "amanda"
}
],
"roleRef": {
"name": "registry-admin"
}
}
},
{
"name": "registry-editor",
"roleBinding": {
"metadata": {
"name": "registry-editor",
"namespace": "financeprj",
"selfLink": "/oapi/v1/namespaces/financeprj/rolebindings/registry-editor",
"uid": "c613716d-f0fd-11e5-b5cb-3c970eb867f7",
"resourceVersion": "24339",
"creationTimestamp": "2016-03-23T13:47:24Z"
},
"userNames": [
"sunny",
"sam",
"janet"
],
"groupNames": null,
"subjects": [
{
"kind": "User",
"name": "sunny"
},
{
"kind": "User",
"name": "sam"
},
{
"kind": "User",
"name": "janet"
}
],
"roleRef": {
"name": "registry-editor"
}
}
},
{
"name": "registry-viewer",
"roleBinding": {
"metadata": {
"name": "registry-viewer",
"namespace": "financeprj",
"uid": "de6e0980-f0fd-11e5-b5cb-3c970eb867f7",
"resourceVersion": "24264",
"creationTimestamp": "2016-03-23T13:48:05Z"
},
"userNames": [
"janet",
"sunny"
],
"groupNames": null,
"subjects": [
{
"kind": "User",
"name": "janet"
},
{
"kind": "User",
"name": "sunny"
},
{
"kind": "SystemGroup",
"name": "system:unauthenticated"
}
],
"roleRef": {
"name": "registry-viewer"
}
}
},
{
"name": "view",
"roleBinding": {
"metadata": {
"name": "view",
"namespace": "financeprj",
"uid": "07e34b4c-ea0f-11e5-ba57-3c970eb867f7",
"resourceVersion": "23860",
"creationTimestamp": "2016-03-14T18:03:18Z"
},
"userNames": null,
"groupNames": null,
"subjects": [
],
"roleRef": {
"name": "view"
}
}
}
]
},
{
"kind": "RoleBinding",
"apiVersion": "v1",
"metadata": {
"name": "registry-admin",
"namespace": "financeprj",
"selfLink": "/oapi/v1/namespaces/financeprj/rolebindings/registry-admin",
"uid": "c0746786-f0fd-11e5-b5cb-3c970eb867f7",
"resourceVersion": "24005",
"creationTimestamp": "2016-03-23T13:47:15Z"
},
"userNames": [
"amanda"
],
"groupNames": null,
"subjects": [
{
"kind": "User",
"name": "amanda"
}
],
"roleRef": {
"name": "registry-admin"
}
},
{
"kind": "RoleBinding",
"apiVersion": "v1",
"metadata": {
"name": "admin",
"namespace": "financeprj",
"selfLink": "/oapi/v1/namespaces/financeprj/rolebindings/admin",
"uid": "c0746786-f0fd-11e5-b5cb-3c970eb867f7",
"resourceVersion": "24005",
"creationTimestamp": "2016-03-23T13:47:15Z"
},
"userNames": [
"amanda"
],
"groupNames": null,
"subjects": [
{
"kind": "User",
"name": "amanda"
}
],
"roleRef": {
"name": "admin"
}
}
]);

View File

@ -1,146 +0,0 @@
/*
* 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/>.
*/
import QUnit from "qunit-tests";
var angular = require("angular");
require("./tags");
var specData;
function suite() {
/* Filled in with a function */
var inject;
var module = angular.module("registry.tags.tests", [
"registry.tags"
]);
QUnit.test("parseSpec", function (assert) {
var done = assert.async();
assert.expect(1);
inject(["imageTagData", function(data) {
var names = data.parseSpec(specData);
assert.deepEqual(names, [ "1", "1.23", "1.24.0", "1.24.2", "latest" ], "parsed names correctly");
done();
}]);
});
QUnit.test("buildSpec with spec", function (assert) {
var done = assert.async();
assert.expect(1);
inject(["imageTagData", function(data) {
var spec = angular.extend({ }, specData);
spec = data.buildSpec(["2.5", "latest", "second"], spec, true, 'docker.io/busybox');
assert.deepEqual(spec, {
"dockerImageRepository": "busybox",
"tags": [
{ "name": "2.5", "importPolicy": { "insecure": true },
"from": { "kind": "DockerImage", "name": "docker.io/busybox:2.5" } },
{ "annotations": null, "from": { "kind": "DockerImage", "name": "docker.io/busybox:latest" },
"generation": 2, "importPolicy": { "insecure": true }, "name": "latest" },
{ "name": "second", "importPolicy": { "insecure": true },
"from": { "kind": "DockerImage", "name": "docker.io/busybox:second" } }
]
}, "build spec correctly");
done();
}]);
});
angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
return function(exception, cause) {
exception.message += ' (caused by "' + cause + '")';
throw exception;
};
});
module.run([
'$injector',
function($injector) {
inject = function inject(func) {
return $injector.invoke(func);
};
QUnit.start();
}
]);
angular.bootstrap(document, ['registry.tags.tests']);
}
specData = {
"dockerImageRepository": "busybox",
"tags": [
{
"name": "1",
"annotations": null,
"from": {
"kind": "DockerImage",
"name": "docker.io/busybox:1"
},
"generation": 2,
"importPolicy": {}
},
{
"name": "1.23",
"annotations": null,
"from": {
"kind": "DockerImage",
"name": "docker.io/busybox:1.23"
},
"generation": 2,
"importPolicy": {}
},
{
"name": "1.24.0",
"annotations": null,
"from": {
"kind": "DockerImage",
"name": "docker.io/busybox:1.24.0"
},
"generation": 2,
"importPolicy": {}
},
{
"name": "1.24.2",
"annotations": null,
"from": {
"kind": "DockerImage",
"name": "docker.io/busybox:1.24.2"
},
"generation": 2,
"importPolicy": {}
},
{
"name": "latest",
"annotations": null,
"from": {
"kind": "DockerImage",
"name": "docker.io/busybox:latest"
},
"generation": 2,
"importPolicy": {
"insecure": false
}
}
]
};
suite();

View File

@ -1,130 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import QUnit from "qunit-tests";
var angular = require("angular");
require("./utils");
function suite() {
/* Filled in with a function */
var inject;
var module = angular.module("kubeUtils.tests", [
"kubeUtils",
]);
QUnit.test("map Named Array", function (assert) {
var done = assert.async();
assert.expect(4);
inject(["KubeMapNamedArray", function lala(mapNamedArray) {
var target = {
"one": {
"name": "one",
"other": "other1",
"value": 1
},
"two": {
"name": "two",
"other": "other2",
"value": 2
}
};
var target2 = {
"other1": {
"name": "one",
"other": "other1",
"value": 1
},
"other2": {
"name": "two",
"other": "other2",
"value": 2
}
};
var source = [{
"name": "one",
"other": "other1",
"value": 1
}, {
"name": "two",
"other": "other2",
"value": 2
}];
assert.deepEqual(mapNamedArray(), {});
assert.deepEqual(mapNamedArray([]), {});
assert.deepEqual(mapNamedArray(source), target);
assert.deepEqual(mapNamedArray(source, "other"), target2);
done();
}]);
});
QUnit.test("Kube string to bytes", function (assert) {
var done = assert.async();
assert.expect(20);
inject(["KubeStringToBytes", function(stringToBytes) {
assert.deepEqual(stringToBytes(), undefined);
assert.deepEqual(stringToBytes("bad"), undefined);
assert.deepEqual(stringToBytes("10"), undefined);
assert.deepEqual(stringToBytes("aGi"), undefined);
assert.deepEqual(stringToBytes("Gi"), undefined);
assert.deepEqual(stringToBytes("Gi10"), undefined);
assert.deepEqual(stringToBytes("Gil"), undefined);
assert.deepEqual(stringToBytes("10E"), 10000000000000000000);
assert.deepEqual(stringToBytes("10P"), 10000000000000000);
assert.deepEqual(stringToBytes("10T"), 10000000000000);
assert.deepEqual(stringToBytes("10G"), 10000000000);
assert.deepEqual(stringToBytes("10M"), 10000000);
assert.deepEqual(stringToBytes("10K"), 10000);
assert.deepEqual(stringToBytes("10m"), 0.01);
assert.deepEqual(stringToBytes("10Ei"), 11529215046068469760);
assert.deepEqual(stringToBytes("10Pi"), 11258999068426240);
assert.deepEqual(stringToBytes("10Ti"), 10995116277760);
assert.deepEqual(stringToBytes("10Gi"), 10737418240);
assert.deepEqual(stringToBytes("10Mi"), 10485760);
assert.deepEqual(stringToBytes("10Ki"), 10240);
done();
}]);
});
angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
return function(exception, cause) {
exception.message += ' (caused by "' + cause + '")';
throw exception;
};
});
module.run([
'$injector',
function($injector) {
inject = function inject(func) {
return $injector.invoke(func);
};
QUnit.start();
}
]);
angular.bootstrap(document, ['kubeUtils.tests']);
}
suite();

View File

@ -1,841 +0,0 @@
/*
* This file is part of Cockpit.
*
* Copyright (C) 2016 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import QUnit from "qunit-tests";
var angular = require("angular");
require("./volumes");
require("./kube-client-cockpit");
function suite(fixtures) {
/* Filled in with a function */
var inject;
var module = angular.module("kubernetes.volumes.tests", [
"kubeClient",
'kubeClient.cockpit',
"kubernetes.volumes",
"kubeUtils",
])
.config([
'KubeTranslateProvider',
'KubeFormatProvider',
function(KubeTranslateProvider, KubeFormatProvider) {
KubeTranslateProvider.KubeTranslateFactory = "CockpitTranslate";
KubeFormatProvider.KubeFormatFactory = "CockpitFormat";
}
]);
function injectLoadFixtures(fixtures) {
inject([
"kubeLoader",
function(loader, data) {
loader.reset(true);
if (fixtures)
loader.handle(fixtures);
}
]);
}
QUnit.test("pods for Claim", function (assert) {
var done = assert.async();
assert.expect(4);
injectLoadFixtures(fixtures);
inject(["volumeData", 'kubeSelect', function(volumeData, select) {
var claim = select().kind("PersistentVolumeClaim")
.name("bound-claim")
.one();
var pods = volumeData.podsForClaim(claim);
assert.equal(pods.length, 1, "number of pods");
assert.equal(pods.one().metadata.name, "mock-pod", "correct pod");
pods = volumeData.podsForClaim();
assert.deepEqual(pods.length, 0, "null claim pods");
pods = volumeData.podsForClaim({});
assert.deepEqual(pods.length, 0, "empty claim pods");
done();
}]);
});
QUnit.test("volumes for Pod", function (assert) {
var done = assert.async();
assert.expect(4);
injectLoadFixtures(fixtures);
inject(["volumeData", 'kubeSelect', function(volumeData, select) {
var pod = select().kind("Pod")
.one();
var volumes = volumeData.volumesForPod(pod);
assert.deepEqual(volumes, {
"default-token-luvqo": {
"name": "default-token-luvqo",
"secret": {
"secretName": "default-token-luvqo"
},
"mounts": {
"mock-volume-container": {
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-luvqo",
"readOnly": true
},
"mock-volume-container1": {
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
"name": "default-token-luvqo",
"readOnly": true
}
}
},
"host-tmp": {
"mounts": {
"mock-volume-container": {
"mountPath": "/other",
"name": "host-tmp"
},
"mock-volume-container1": {
"mountPath": "/tmp",
"name": "host-tmp"
}
},
"name": "host-tmp",
"persistentVolumeClaim": {
"claimName": "bound-claim"
}
},
"missing-claim": {
"mounts": {
"mock-volume-container": {
"mountPath": "/tmp",
"name": "missing-claim"
}
},
"name": "missing-claim",
"persistentVolumeClaim": {
"claimName": "missing-claim"
}
},
"missing-claim2": {
"name": "missing-claim2",
"persistentVolumeClaim": {
"claimName": "missing-claim2"
}
}
});
assert.equal(volumes, volumeData.volumesForPod(pod), "same object");
assert.deepEqual(volumeData.volumesForPod(), {}, "No null volumes");
assert.deepEqual(volumeData.volumesForPod({}), {}, "No empty volumes");
done();
}]);
});
QUnit.test("claim From Volume Source", function (assert) {
var done = assert.async();
assert.expect(4);
injectLoadFixtures(fixtures);
inject(["volumeData", 'kubeSelect', function(volumeData, select) {
var pod = select().kind("Pod")
.one();
var volumes = volumeData.volumesForPod(pod);
var source = volumes["host-tmp"]["persistentVolumeClaim"];
var claim = volumeData.claimFromVolumeSource(source, "default");
assert.equal(claim.metadata.name, "bound-claim", "correct claim");
assert.equal(claim.kind, "PersistentVolumeClaim", "correct type");
assert.deepEqual(volumeData.claimFromVolumeSource(), null, "No null volumes");
assert.deepEqual(volumeData.claimFromVolumeSource({}), null, "No empty volumes");
done();
}]);
});
QUnit.test("claim For Volume", function (assert) {
var done = assert.async();
assert.expect(5);
injectLoadFixtures(fixtures);
inject(["volumeData", 'kubeSelect', function(volumeData, select) {
var bound = select().kind("PersistentVolume")
.name("bound")
.one();
var claim = volumeData.claimForVolume(bound);
assert.equal(claim.metadata.name, "bound-claim", "correct claim");
assert.equal(claim.kind, "PersistentVolumeClaim", "correct claim");
var unbound = select().kind("PersistentVolume")
.name("unbound")
.one();
assert.equal(volumeData.claimForVolume(unbound), null, "no claim");
assert.equal(volumeData.claimForVolume(), null, "null volume");
assert.equal(volumeData.claimForVolume(), null, "empty volume");
done();
}]);
});
QUnit.test("claim phases", function (assert) {
var done = assert.async();
assert.expect(2);
injectLoadFixtures(fixtures);
inject(["volumeData", 'kubeSelect', function(volumeData, select) {
var claim = select().kind("PersistentVolumeClaim")
.statusPhase("Bound")
.one();
assert.equal(claim.metadata.name, "bound-claim", "select bound claims");
var pending = select().kind("PersistentVolumeClaim")
.statusPhase("Pending")
.one();
assert.equal(pending.metadata.name, "unbound-claim", "select unbound claims");
done();
}]);
});
QUnit.test("volume Types", function (assert) {
var done = assert.async();
assert.expect(4);
injectLoadFixtures(fixtures);
inject(["volumeData", 'kubeSelect', function(volumeData, select) {
var pv = select().kind("PersistentVolume")
.name("bound")
.one();
var volumes = volumeData.volumesForPod(select().kind("Pod")
.one());
assert.equal(volumeData.getVolumeType(pv.spec), "nfs", "correct type");
assert.equal(volumeData.getVolumeType(volumes["default-token-luvqo"]), "secret", "secret volume");
assert.equal(volumeData.getVolumeType(), undefined, "null volume");
assert.equal(volumeData.getVolumeType({}), undefined, "empty volume");
done();
}]);
});
QUnit.test("volume Labels", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(fixtures);
inject(["volumeData", 'kubeSelect', function(volumeData, select) {
var pv = select().kind("PersistentVolume")
.name("bound")
.one();
assert.equal(volumeData.getVolumeLabel(), "Unknown", "null volume");
assert.equal(volumeData.getVolumeLabel({}), "Unknown", "empty volume");
assert.equal(volumeData.getVolumeLabel(pv.spec), "NFS Mount", "volume label");
done();
}]);
});
QUnit.test("default volume build", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(fixtures);
inject(["defaultVolumeFields", "kubeSelect", function(volumeFields, select) {
var blank_fields = {
"accessModes": {
"ReadOnlyMany": "Read only from multiple nodes",
"ReadWriteMany": "Read and write from multiple nodes",
"ReadWriteOnce": "Read and write from a single node"
},
"capacity": "",
"policy": "Retain",
"reclaimPolicies": {
"Delete": "Delete",
"Recycle": "Recycle",
"Retain": "Retain"
}
};
assert.deepEqual(blank_fields, volumeFields.build(), "default fields");
assert.deepEqual(blank_fields, volumeFields.build({}), "empty fields");
assert.deepEqual({
"accessModes": {
"ReadOnlyMany": "Read only from multiple nodes",
"ReadWriteMany": "Read and write from multiple nodes",
"ReadWriteOnce": "Read and write from a single node"
},
"capacity": "2Gi",
"policy": "Retain",
"ReadWriteMany": true,
"reclaimPolicies": {
"Delete": "Delete",
"Recycle": "Recycle",
"Retain": "Retain"
}
}, volumeFields.build(select().name("available")
.one()), "default fields");
done();
}]);
});
QUnit.test("default volume validate", function (assert) {
var done = assert.async();
assert.expect(15);
injectLoadFixtures(fixtures);
inject(["defaultVolumeFields", "kubeSelect", function(volumeFields, select) {
var result = volumeFields.validate(null, {});
assert.equal(result.data, null, "blank fields blank data");
assert.equal(result.errors.length, 4);
assert.equal(result.errors[0].target, "#last-access", "blank fields access error");
assert.equal(result.errors[1].target, "#modify-name", "blank fields name error");
assert.equal(result.errors[2].target, "#modify-capacity", "blank fields capacity error");
assert.equal(result.errors[3].target, "#last-policy", "blank fields policy error");
var invalid = {
"reclaimPolicies": { "policy1": "policy2" },
"accessModes": { "mode1": "mode1" },
"mode2": true,
"policy" : "policy2",
"name": "a name",
"capacity": "invalid"
};
result = volumeFields.validate(null, invalid);
assert.equal(result.data, null, "invalid fields invalid data");
assert.equal(result.errors.length, 4);
assert.equal(result.errors[0].target, "#last-access", "invalid fields access error");
assert.equal(result.errors[1].target, "#modify-name", "invalid fields name error");
assert.equal(result.errors[2].target, "#modify-capacity", "invalid fields capacity error");
assert.equal(result.errors[3].target, "#last-policy", "invalid fields policy error");
var valid = {
"reclaimPolicies": { "policy1": "policy2" },
"accessModes": { "mode1": "mode1" },
"mode1": true,
"policy" : "policy1",
"name": " name ",
"capacity": " 2Gi "
};
var spec = {
"accessModes": [
"mode1"
],
"capacity": {
"storage": "2Gi"
},
"persistentVolumeReclaimPolicy": "policy1"
};
result = volumeFields.validate(null, valid);
assert.deepEqual(result.data, {
"kind": "PersistentVolume",
"metadata": {
"name": "name"
},
"spec": spec
}, "no item full object");
assert.equal(result.errors.length, 0);
result = volumeFields.validate({}, valid);
assert.deepEqual(result.data, { "spec": spec }, "with item only spec");
done();
}]);
});
QUnit.test("gluster volume build", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(fixtures);
inject(["glusterfsVolumeFields", "kubeSelect", function(gfs, select) {
var endpoints = select().kind("Endpoints");
var blank_fields = {
"glusterfsPath": undefined,
"endpoint": undefined,
"endpointOptions": endpoints,
"readOnly": undefined,
"reclaimPolicies": {
"Retain": "Retain"
}
};
assert.deepEqual(blank_fields, gfs.build(), "default gluster fields");
assert.deepEqual(blank_fields, gfs.build({}), "empty gluster fields");
assert.deepEqual({
"glusterfsPath": "kube_vo",
"endpointOptions": endpoints,
"readOnly": undefined,
"reclaimPolicies": {
"Retain": "Retain"
},
"endpoint": "my-gluster-endpoint",
}, gfs.build(select().name("gfs-volume")
.one()), "gluster fields");
done();
}]);
});
QUnit.test("gfs volume validate", function (assert) {
var done = assert.async();
assert.expect(9);
injectLoadFixtures(fixtures);
inject(["glusterfsVolumeFields", function(nfsVolumeFields) {
var result = nfsVolumeFields.validate(null, {});
assert.equal(result.data, null, "blank fields blank data");
assert.equal(result.errors.length, 2);
assert.equal(result.errors[0].target, "#modify-endpoint", "blank fields endpoint error");
assert.equal(result.errors[1].target, "#modify-glusterfs-path", "blank fields path error");
var invalid = {
"endpoint": "bad",
"glusterfsPath": "name"
};
result = nfsVolumeFields.validate(null, invalid);
assert.equal(result.data, null, "invalid fields invalid data");
assert.equal(result.errors.length, 1);
assert.equal(result.errors[0].target, "#modify-endpoint", "invalid endpoint error");
var valid = {
"endpoint": "my-gluster-endpoint",
"glusterfsPath": "name",
};
var source = {
"endpoints": "my-gluster-endpoint",
"path": "name",
"readOnly": false
};
result = nfsVolumeFields.validate(null, valid);
assert.deepEqual(result.data, source, "valid source result");
assert.equal(result.errors.length, 0, "no errors when valid");
done();
}]);
});
QUnit.test("nfs volume build", function (assert) {
var done = assert.async();
assert.expect(3);
injectLoadFixtures(fixtures);
inject(["nfsVolumeFields", "kubeSelect", function(nfsVolumeFields, select) {
var blank_fields = {
"path": undefined,
"readOnly": undefined,
"reclaimPolicies": {
"Recycle": "Recycle",
"Retain": "Retain"
},
"server": undefined
};
assert.deepEqual(blank_fields, nfsVolumeFields.build(), "default nfs fields");
assert.deepEqual(blank_fields, nfsVolumeFields.build({}), "empty nfs fields");
assert.deepEqual({
"path": "/tmp",
"readOnly": true,
"reclaimPolicies": {
"Recycle": "Recycle",
"Retain": "Retain"
},
"server": "host-or.ip:port",
}, nfsVolumeFields.build(select().name("bound")
.one()), "nfs fields");
done();
}]);
});
QUnit.test("nfs volume validate", function (assert) {
var done = assert.async();
assert.expect(10);
injectLoadFixtures(fixtures);
inject(["nfsVolumeFields", function(nfsVolumeFields) {
var result = nfsVolumeFields.validate(null, {});
assert.equal(result.data, null, "blank fields blank data");
assert.equal(result.errors.length, 2);
assert.equal(result.errors[0].target, "#nfs-modify-server", "blank fields server error");
assert.equal(result.errors[1].target, "#modify-path", "blank fields path error");
var invalid = {
"server": "server/bad",
"path": "bad",
};
result = nfsVolumeFields.validate(null, invalid);
assert.equal(result.data, null, "invalid fields invalid data");
assert.equal(result.errors.length, 2);
assert.equal(result.errors[0].target, "#nfs-modify-server", "invalid fields server error");
assert.equal(result.errors[1].target, "#modify-path", "invalid fields path error");
var valid = {
"server": "host.or-ip:port",
"path": "/tmp",
};
var source = {
"server": "host.or-ip:port",
"path": "/tmp",
"readOnly": false
};
result = nfsVolumeFields.validate(null, valid);
assert.deepEqual(result.data, source, "valid source result");
assert.equal(result.errors.length, 0, "no errors when valid");
done();
}]);
});
angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
return function(exception, cause) {
exception.message += ' (caused by "' + cause + '")';
throw exception;
};
});
module.run([
'$injector',
function($injector) {
inject = function inject(func) {
return $injector.invoke(func);
};
QUnit.start();
}
]);
angular.bootstrap(document, ['kubernetes.volumes.tests']);
}
/* Invoke the test suite with this data */
suite([
{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": {
"name": "iscsi-vol",
"selfLink": "/api/v1/persistentvolumes/iscsi-vol",
"uid": "3b2e0dc2-f6a4-11e5-9e36-5254009e00f2",
"resourceVersion": "1325",
"creationTimestamp": "2016-03-30T18:21:33Z"
},
"spec": {
"capacity": {
"storage": "2Gi"
},
"iscsi":{
"targetPortal": "host-or.ip:port",
"iqn": "iqn.1994-05.t.com.redhat:83ba4072bb9c",
"lun": 10,
"iscsiInterface": "custom-iface",
"fsType":"ext3",
"readOnly": true,
},
"accessModes":["ReadWriteOnce"],
"persistentVolumeReclaimPolicy": "Retain"
},
"status": {
"phase": "Available"
}
},
{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": {
"name": "available",
"selfLink": "/api/v1/persistentvolumes/available",
"uid": "3b2e0dc2-f6a4-11e5-9e36-5254009e00f1",
"resourceVersion": "1325",
"creationTimestamp": "2016-03-30T18:21:33Z"
},
"spec": {
"capacity": {
"storage": "2Gi"
},
"hostPath": {
"path": "/tmp"
},
"accessModes": [
"ReadWriteMany"
],
"persistentVolumeReclaimPolicy": "Retain"
},
"status": {
"phase": "Available"
}
},
{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": {
"name": "bound",
"selfLink": "/api/v1/persistentvolumes/bound",
"uid": "ae3133fc-f6a4-11e5-9e36-5254009e00f1",
"resourceVersion": "1388",
"creationTimestamp": "2016-03-30T18:24:46Z"
},
"spec": {
"capacity": {
"storage": "5Gi"
},
"nfs": {
"path": "/tmp",
"server": "host-or.ip:port",
"readOnly": true
},
"accessModes": [
"ReadWriteMany"
],
"claimRef": {
"kind": "PersistentVolumeClaim",
"namespace": "default",
"name": "bound-claim",
"uid": "43dfbea5-f6a4-11e5-9e36-5254009e00f1",
"apiVersion": "v1",
"resourceVersion": "1331"
},
"persistentVolumeReclaimPolicy": "Retain"
},
"status": {
"phase": "Bound"
}
},
{
"kind": "PersistentVolume",
"apiVersion": "v1",
"metadata": {
"name": "gfs-volume",
"selfLink": "/api/v1/persistentvolumes/gfs-volume",
"uid": "ae3133fc-f6a4-11e5-9e36-5254009e00cc",
"resourceVersion": "1388",
"creationTimestamp": "2016-03-30T18:24:46Z"
},
"spec": {
"capacity": {
"storage": "5Gi"
},
"glusterfs": {
"path": "kube_vo",
"endpoints": "my-gluster-endpoint"
},
"accessModes": [
"ReadWriteMany"
],
"persistentVolumeReclaimPolicy": "Retain"
},
"status": {
"phase": "Pending"
}
},
{
"kind": "PersistentVolumeClaim",
"apiVersion": "v1",
"metadata": {
"name": "unbound-claim",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/persistentvolumeclaims/unbound-claim",
"uid": "3d474220-f6b3-11e5-ab0c-3b97187a9955",
"resourceVersion": "1331",
"creationTimestamp": "2016-03-30T18:21:47Z"
},
"spec": {
"accessModes": [
"ReadWriteMany"
],
"resources": {
"requests": {
"storage": "5Gi"
}
}
},
"status": {
"phase": "Pending"
}
},
{
"kind": "PersistentVolumeClaim",
"apiVersion": "v1",
"metadata": {
"name": "bound-claim",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/persistentvolumeclaims/bound-claim",
"uid": "43dfbea5-f6a4-11e5-9e36-5254009e00f1",
"resourceVersion": "1387",
"creationTimestamp": "2016-03-30T18:21:47Z"
},
"spec": {
"accessModes": [
"ReadWriteMany"
],
"resources": {
"requests": {
"storage": "5Gi"
}
},
"volumeName": "available"
},
"status": {
"phase": "Bound",
"accessModes": [
"ReadWriteMany"
],
"capacity": {
"storage": "5Gi"
}
}
},
{
"kind": "Pod",
"apiVersion": "v1",
"metadata": {
"name": "mock-pod",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/pods/mock-pod",
"uid": "43d38e8e-f6a4-11e5-9e36-5254009e00f1",
"resourceVersion": "1328",
"creationTimestamp": "2016-03-30T18:21:47Z"
},
"spec": {
"volumes": [
{
"name": "missing-claim",
"persistentVolumeClaim": {
"claimName": "missing-claim"
}
},
{
"name": "host-tmp",
"persistentVolumeClaim": {
"claimName": "bound-claim"
}
},
{
"name": "missing-claim2",
"persistentVolumeClaim": {
"claimName": "missing-claim2"
}
},
{
"name": "default-token-luvqo",
"secret": {
"secretName": "default-token-luvqo"
}
}
],
"containers": [
{
"name": "mock-volume-container1",
"image": "busybox:buildroot-2014.02",
"command": [
"/bin/sh",
"-c",
"for x in $(seq 1 1000); do echo 'HelloMessage.' \u003e\u00262; sleep 1; done"
],
"ports": [
{
"containerPort": 9949,
"protocol": "TCP"
}
],
"resources": {},
"volumeMounts": [
{
"name": "host-tmp",
"mountPath": "/tmp"
},
{
"name": "default-token-luvqo",
"readOnly": true,
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
}
]
},
{
"name": "mock-volume-container",
"image": "busybox:buildroot-2014.02",
"command": [
"/bin/sh",
"-c",
"for x in $(seq 1 1000); do echo 'HelloMessage.' \u003e\u00262; sleep 1; done"
],
"ports": [
{
"containerPort": 9949,
"protocol": "TCP"
}
],
"resources": {},
"volumeMounts": [
{
"name": "host-tmp",
"mountPath": "/other"
},
{
"name": "missing-claim",
"mountPath": "/tmp"
},
{
"name": "default-token-luvqo",
"readOnly": true,
"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
}
]
}
]
},
"status": {
"phase": "Pending"
}
},
{
"kind": "Endpoints",
"apiVersion": "v1",
"metadata": {
"name": "my-gluster-endpoint",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/endpoints/my-gluster-endpoint",
"uid": "498cac38-ffc0-11e5-8098-5254009e00dd",
"resourceVersion": "1078",
"creationTimestamp": "2016-04-11T08:35:03Z"
},
"subsets": [
{
"addresses": [
{
"ip": "172.17.0.2"
}
],
"ports": [
{
"port": 1,
"protocol": "TCP"
}
]
}
]
}
]);

View File

@ -1,189 +0,0 @@
/*
* 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');
require('angular-route');
require('kubernetes-topology-graph/dist/topology-graph.js');
require('./kube-client');
require('./details');
require('../views/topology-page.html');
var icons = {
Pod: '#vertex-Pod',
ReplicationController: '#vertex-ReplicationController',
Node: '#vertex-Node',
Service: '#vertex-Service',
DeploymentConfig: "#vertex-DeploymentConfig",
Route: "#vertex-Route"
};
angular.module('kubernetes.topology', [
'ngRoute',
'kubernetesUI',
'kubeClient',
'kubernetes.details'
])
.config(['$routeProvider',
function($routeProvider) {
$routeProvider.when('/topology/:namespace?', {
templateUrl: 'views/topology-page.html',
controller: 'TopologyCtrl'
});
}
])
.controller('TopologyCtrl', [
'$scope',
'$window',
'kubeLoader',
'kubeSelect',
'KubeDiscoverSettings',
'itemActions',
function($scope, $window, loader, select, discoverSettings, actions) {
$scope.items = { };
$scope.relations = [ ];
$scope.selected = null;
var ready = false;
function link_for_item(kind, namespace, name) {
var rel = select().kind(kind)
.name(name)
.namespace(namespace)
.one();
if (rel)
return rel.metadata.selfLink;
}
function rels_for_item(item) {
var rels = { };
var endpoints, subsets;
var link;
/* Lookup which node this pod is scheduled on */
if (item.kind === "Node") {
rels = select().kind("Pod")
.host(item.metadata.name);
/* Kubernetes tells us about endpoints, which are service to pod mappings */
} else if (item.kind === "Service") {
endpoints = select().kind("Endpoints")
.namespace(item.metadata.namespace)
.name(item.metadata.name)
.one() || { };
subsets = endpoints.subsets || [ ];
subsets.forEach(function(subset) {
var addresses = subset.addresses || [ ];
addresses.forEach(function(address) {
if (address.targetRef && address.targetRef.kind == "Pod")
link = link_for_item("Pod", address.targetRef.namespace,
address.targetRef.name);
if (link)
rels[link] = {};
});
});
/* For ReplicationControllers we just do the selection ourselves */
} else if (item.kind === "ReplicationController") {
rels = select().kind("Pod")
.namespace(item.metadata.namespace);
if (item.spec.selector)
rels = rels.label(item.spec.selector);
} else if (item.kind === "DeploymentConfig") {
rels = select().kind("ReplicationController")
.namespace(item.metadata.namespace)
.label({ "openshift.io/deployment-config.name" : item.metadata.name });
/* For Routes just build it out */
} else if (item.kind === "Route" && item.spec.to) {
link = link_for_item(item.spec.to.kind, item.metadata.namespace,
item.spec.to.name);
if (link)
rels[link] = {};
}
return rels;
}
loader.watch("Node", $scope);
loader.watch("Pod", $scope);
loader.watch("ReplicationController", $scope);
loader.watch("Service", $scope);
loader.watch("Endpoints", $scope);
discoverSettings().then(function(settings) {
if (settings.flavor === "openshift") {
loader.watch("DeploymentConfig", $scope);
loader.watch("Route", $scope);
}
});
loader.listen(function(changed, removed) {
var selected_meta;
var relations = [];
var item;
var key;
$scope.items = select();
if ($scope.selected) {
selected_meta = $scope.selected.metadata || {};
item = select().kind($scope.selected.kind)
.name(selected_meta.name);
if (selected_meta.namespace)
item = item.namespace(selected_meta.namespace);
$scope.selected = item.one();
}
for (key in $scope.items) {
var pkey;
var rels = rels_for_item($scope.items[key]);
for (pkey in rels)
relations.push({ source: key, target: pkey });
}
$scope.relations = relations;
}, $scope);
$scope.$on("select", function(ev, item) {
$scope.$applyAsync(function () {
$scope.selected = item;
});
});
/* Make a copy since we modify */
$scope.kinds = angular.copy(icons);
/* All the actions available on the $scope */
angular.extend($scope, actions);
function resized() {
$scope.height = { height: (window.innerHeight - 60) + "px" };
if (ready)
$scope.$digest();
}
angular.element($window).bind('resize', resized);
resized();
ready = true;
}
]);
}());

View File

@ -1,204 +0,0 @@
/*
* 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');
angular.module("kubeUtils", [])
.factory("KubeMapNamedArray", [
function () {
return function mapNamedArray(array, attr) {
if (!attr)
attr = "name";
var result = { };
var i, len;
if (array) {
for (i = 0, len = array.length; i < len; i++)
result[array[i][attr]] = array[i];
}
return result;
};
}
])
.factory("KubeStringToBytes", [
function() {
return function (byte_string) {
var valid_suffixes = {
"E": 1000000000000000000,
"P": 1000000000000000,
"T": 1000000000000,
"G": 1000000000,
"M": 1000000,
"K": 1000,
"m": 0.001,
"Ei": 1152921504606846976,
"Pi": 1125899906842624,
"Ti": 1099511627776,
"Gi": 1073741824,
"Mi": 1048576,
"Ki": 1024,
};
if (!byte_string)
return;
byte_string = byte_string.trim();
for (var key in valid_suffixes) {
if (byte_string.length > key.length &&
byte_string.slice(-key.length) === key) {
var number = Number(byte_string.slice(0, -key.length));
if (!isNaN(number))
return number * valid_suffixes[key];
}
}
};
}
])
.provider('KubeFormat', [
function() {
var self = this;
/* Until we come up with a good default implementation, must be provided */
self.KubeFormatFactory = "MissingFormat";
function load(injector, name) {
if (angular.isString(name))
return injector.get(name, "KubeFormat");
else
return injector.invoke(name);
}
self.$get = [
"$injector",
function($injector) {
return load($injector, self.KubeFormatFactory);
}
];
}
])
.factory("MissingFormat", [
function() {
return function MissingFormatCapacity(value) {
throw Error("no KubeFormatFactory set");
};
}
])
.provider('KubeTranslate', [
function() {
var self = this;
/* Until we come up with a good default implementation, must be provided */
self.KubeTranslateFactory = "KubeTranslate";
function load(injector, name) {
if (angular.isString(name))
return injector.get(name, "MissingKubeTranslate");
else
return injector.invoke(name);
}
self.$get = [
"$injector",
function($injector) {
return load($injector, self.KubeTranslateFactory);
}
];
}
])
.factory("MissingKubeTranslate", [
function() {
function error_func() {
throw Error("no KubeTranslateFactory set");
}
return {
gettext: error_func,
ngettext: error_func
};
}
])
.provider('KubeBrowserStorage', [
function() {
var self = this;
/* Until we come up with a good default implementation, must be provided */
self.KubeBrowserStorageFactory = "DefaultKubeBrowserStorage";
function load(injector, name) {
if (angular.isString(name))
return injector.get(name, "DefaultKubeBrowserStorage");
else
return injector.invoke(name);
}
self.$get = [
"$injector",
function($injector) {
return load($injector, self.KubeBrowserStorageFactory);
}
];
}
])
.factory("DefaultKubeBrowserStorage", [
"$window",
function($window) {
return {
localStorage: $window.localStorage,
sessionStorage: $window.sessionStorage,
};
}
])
.filter('formatCapacityName', function() {
return function(key) {
var data;
if (key == "cpu") {
data = "CPUs";
} else {
key = key.replace(/-/g, " ");
data = key.charAt(0).toUpperCase() + key.substr(1);
}
return data;
};
})
.filter('formatCapacityValue', [
"KubeFormat",
"KubeStringToBytes",
function (format, stringToBytes) {
return function(value, key) {
if (key == "memory") {
var raw = stringToBytes(value);
if (raw)
value = format.formatBytes(raw);
}
return value;
};
}
]);
}());

File diff suppressed because it is too large Load Diff

View File

@ -1,433 +0,0 @@
/*
* 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/>.
*/
package helpers
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"strings"
)
const CA_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
type AuthError struct {
msg string
}
func (v *AuthError) Error() string {
return v.msg
}
func newAuthError(msg string) error {
return &AuthError{msg}
}
// helpers
func namedArrayObject(key string, name string, m map[string]interface{}) []map[string]interface{} {
nm := make(map[string]interface{})
nm[key] = m
nm["name"] = name
s := make([]map[string]interface{}, 1)
s[0] = nm
return s
}
// Struct for decoding json
type userInfo struct {
FullName string `json:"fullName"`
MetaData struct {
Name string `json:"name"`
} `json:"metadata"`
}
type apiVersions struct {
Versions []string `json:"versions"`
}
// Simple client
type Client struct {
host string
version string
caData string
insecure bool
requireOpenshift bool
isOpenshift bool
userAPI string
client *http.Client
creds *Credentials
}
func doRequest(client *http.Client, method string, path string, auth string, body []byte) (*http.Response, error) {
var req *http.Request
var err error
if body != nil {
req, err = http.NewRequest(method, path, bytes.NewReader(body))
} else {
req, err = http.NewRequest(method, path, nil)
}
if err != nil {
return nil, err
}
if auth != "" {
req.Header.Add("Authorization", auth)
}
return client.Do(req)
}
func (self *Client) fetchVersion(authHeader string) error {
path := fmt.Sprintf("%s/api", self.host)
resp, err := doRequest(self.client, "GET", path, authHeader, nil)
// Treat connection errors as internal errors and invalid
// responses as auth errors
if err != nil {
return errors.New(fmt.Sprintf("Couldn't connect to the api: %s", err))
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return newAuthError(fmt.Sprintf("Couldn't get api version: %s", resp.Status))
}
data := apiVersions{}
deErr := json.NewDecoder(resp.Body).Decode(&data)
if deErr != nil {
return newAuthError(fmt.Sprintf("Couldn't get api version: %s", deErr))
}
if len(data.Versions) < 1 {
return newAuthError(fmt.Sprintf("Couldn't get api version: invalid data"))
}
self.version = data.Versions[0]
return nil
}
func (self *Client) apiStatus(resource string, auth string) (int, error) {
path := fmt.Sprintf("%s/api/%s/%s", self.host, self.version, resource)
resp, rErr := doRequest(self.client, "GET", path, auth, nil)
if rErr != nil {
return 0, errors.New(fmt.Sprintf("Couldn't connect to api: %s", rErr))
}
defer resp.Body.Close()
return resp.StatusCode, nil
}
func (self *Client) confirmBearerAuth(creds *Credentials) error {
// There are no bugs we have to work around here
// so just make sure we get a 200 or 403 to
// a namespace call
status, e := self.apiStatus("namespaces", creds.GetHeader())
if e == nil && status != 403 && status != 200 {
newAuthError(fmt.Sprintf("Couldn't verify bearer token with api: %s", status))
}
return e
}
func (self *Client) confirmBasicAuth(creds *Credentials) error {
// Issue a request to the the /api API endpoint without any
// auth data.
// If we get 401 in response then we know our creds were good and we can log the user in.
// If we get a 200 or a 403 then we don't know if are creds were correct and we need to
// make more calls to figure it out.
// Any other code is treated as an error.
var e error
var status int
status, e = self.apiStatus("", "")
success := status == 401
if status == 200 || status == 403 {
// Either /api is open or the current user, possibly (system:anonymous)
// doesn't have permissions on it
// Send a request to the /api/$version/namespaces endpoint with a
// Authorization header that is guarenteed to be invalid.
// This should return a 200 if the whole api is open or a 401 if the
// api is protected.
status, e = self.apiStatus("namespaces", "Basic Og==")
if e != nil {
return e
}
// Some versions of kubernetes return 403 instead of 401
// when presented with bad basic auth data. In those cases
// we need to refuse authentication, as we have no way
// know if the credentials we have are in fact valid.
// https://github.com/kubernetes/kubernetes/pull/41775
if status == 403 {
e = errors.New("This version of kubernetes is not supported. Turn off anonymous auth or upgrade.")
} else if status == 200 {
success = true
} else if status == 401 {
if creds.GetHeader() != "" {
success = true
} else {
status = 403
}
}
}
if !success && e == nil {
e = newAuthError(fmt.Sprintf("Couldn't verify authentication with api: %s", status))
}
return e
}
func (self *Client) confirmCreds(creds *Credentials) error {
// Do this explictly so that we know we have a valid response
// Kubernetes doesn't provide any way for a caller
// to find out who it is, so we need to confirm the creds
// we got some other way.
err := self.fetchVersion(creds.GetHeader())
if err != nil {
return err
}
// If we are here we got a version for the api from the /api endpoint,
// using the credentials the user gave us.
// This happens when either
// a) /api is protected and the credentials are correct
// or
// b) /api is open in which case we have no idea if our creds were correct
// Confirming them is different for Basic or Bearer auth
var e error
if creds.UserName != "" {
e = self.confirmBasicAuth(creds)
} else {
creds.UserName = "Unknown"
e = self.confirmBearerAuth(creds)
}
creds.DisplayName = creds.UserName
return e
}
func (self *Client) fetchUserData(creds *Credentials) error {
resp, err := self.DoRequest("GET", self.userAPI, "users/~", creds, nil)
if err != nil {
return err
}
defer resp.Body.Close()
// 404 or 403 are both responses we can
// get when the oapi/users endpoint doesn't exists
notFound := resp.StatusCode == 404 || resp.StatusCode == 403
if notFound && self.requireOpenshift {
return errors.New("Couldn't connect: Incompatible API")
// This might be kubernetes, it doesn't have a way to
// get user data, if we have a username try to
// see if we can connect to it anyways
} else if notFound {
return self.confirmCreds(creds)
} else if resp.StatusCode != 200 {
return newAuthError(fmt.Sprintf("Couldn't get user data: %s", resp.Status))
}
self.isOpenshift = true
data := userInfo{}
deErr := json.NewDecoder(resp.Body).Decode(&data)
if deErr != nil {
return newAuthError(fmt.Sprintf("Couldn't get user json data: %s", deErr))
}
creds.UserName = data.MetaData.Name
if creds.UserName != "" {
creds.DisplayName = data.FullName
if creds.DisplayName == "" {
creds.DisplayName = creds.UserName
}
} else {
return newAuthError(fmt.Sprintf("Openshift user data wasn't valid: %v", data))
}
return nil
}
func (self *Client) DoRequest(method string, api string, resource string,
creds *Credentials, body []byte) (*http.Response, error) {
if self.host == "" {
return nil, errors.New("No kubernetes available")
}
authHeader := ""
if creds != nil {
authHeader = creds.GetHeader()
}
if self.version == "" {
err := self.fetchVersion(authHeader)
if err != nil {
return nil, err
}
}
path := fmt.Sprintf("%s/%s/%s/%s", self.host, api, self.version, resource)
resp, rErr := doRequest(self.client, method, path, authHeader, body)
if rErr != nil {
return nil, errors.New(fmt.Sprintf("Couldn't connect: %s", rErr))
}
return resp, nil
}
func (self *Client) Login(authLine string) (map[string]interface{}, error) {
parts := strings.SplitN(authLine, " ", 2)
if len(parts) == 0 {
return nil, newAuthError("Invalid Authorization line")
}
authData := ""
authType := parts[0]
if len(parts) == 2 {
authData = parts[1]
}
creds, err := NewCredentials(authType, authData)
if err == nil {
err = self.fetchUserData(creds)
}
if err != nil {
if creds != nil && creds.authType == "negotiate" {
return nil, newAuthError(fmt.Sprintf("Negotiate failed: %s", err))
}
return nil, err
}
// Login successfull, save creds
self.creds = creds
user_data := creds.GetApiUserMap()
cluster := make(map[string]interface{})
cluster["server"] = self.host
if self.caData != "" {
cluster["certificate-authority-data"] = self.caData
}
cluster["insecure-skip-tls-verify"] = self.insecure
clusters := namedArrayObject("cluster", "container-cluster", cluster)
context := make(map[string]interface{})
context["cluster"] = "container-cluster"
context["user"] = user_data["name"]
contexts := namedArrayObject("context", "container-context", context)
users := make([]map[string]interface{}, 1)
users[0] = user_data
login_data := make(map[string]interface{})
login_data["apiVersion"] = self.version
login_data["displayName"] = creds.DisplayName
login_data["current-context"] = "container-context"
login_data["clusters"] = clusters
login_data["contexts"] = contexts
login_data["users"] = users
return login_data, nil
}
func (self *Client) CleanUp() error {
if self.creds != nil && self.isOpenshift {
token := self.creds.GetToken()
if token != "" {
path := fmt.Sprintf("oauthaccesstokens/%s", token)
resp, err := self.DoRequest("DELETE", self.userAPI, path, self.creds, nil)
if err != nil {
return err
}
if resp.StatusCode != 200 {
log.Println(fmt.Sprintf("Invalid token cleanup response: %d", resp.StatusCode))
}
defer resp.Body.Close()
}
}
return nil
}
func NewClient() *Client {
var caData []byte = nil
var pool *x509.CertPool
ac := new(Client)
ac.insecure = false
ac.host = os.Getenv("KUBERNETES_SERVICE_HOST")
if ac.host != "" {
// assume we are always on https
ac.host = fmt.Sprintf("https://%s", ac.host)
port := os.Getenv("KUBERNETES_SERVICE_PORT")
if port != "" {
ac.host = fmt.Sprintf("%s:%s", ac.host, port)
}
}
ac.userAPI = os.Getenv("KUBERNETES_USER_API")
if ac.userAPI == "" {
ac.userAPI = "oapi"
}
ac.requireOpenshift, _ = strconv.ParseBool(os.Getenv("REGISTRY_ONLY"))
ac.insecure, _ = strconv.ParseBool(os.Getenv("KUBERNETES_INSECURE"))
if !ac.insecure {
data := os.Getenv("KUBERNETES_CA_DATA")
if data != "" {
caData = []byte(data)
}
if caData == nil {
var err error
caData, err = ioutil.ReadFile(CA_PATH)
if err != nil {
log.Println(fmt.Sprintf("Couldn't load CA data: %s", err))
}
}
if caData != nil {
pool = x509.NewCertPool()
pool.AppendCertsFromPEM(caData)
ac.caData = base64.StdEncoding.EncodeToString(caData)
}
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: pool, InsecureSkipVerify: ac.insecure},
}
ac.client = &http.Client{Transport: tr}
return ac
}

View File

@ -1,102 +0,0 @@
/*
* 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/>.
*/
package helpers
import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"strings"
)
type Credentials struct {
UserName string
DisplayName string
password string
bearerToken string
authHeader string
authType string
}
func (self *Credentials) GetHeader() string {
return self.authHeader
}
func (self *Credentials) GetToken() string {
return self.bearerToken
}
func (self *Credentials) GetApiUserMap() map[string]interface{} {
m := make(map[string]interface{})
name := self.UserName
if name == "" {
name = "cockpit-container-user"
}
user := make(map[string]string)
if self.password != "" {
user["password"] = self.password
user["username"] = self.UserName
} else {
user["token"] = self.bearerToken
}
m["name"] = self.UserName
m["user"] = user
return m
}
func NewCredentials(authType string, authData string) (*Credentials, error) {
cred := new(Credentials)
cred.authType = strings.ToLower(authType)
if cred.authType == "basic" {
raw, err := base64.StdEncoding.DecodeString(authData)
if err != nil {
return nil, errors.New(fmt.Sprintf("Couldn't decode basic header: %s", err))
}
parts := strings.SplitN(string(raw), ":", 2)
cred.UserName = parts[0]
if len(parts) > 1 {
cred.password = parts[1]
}
cred.authHeader = fmt.Sprintf("Basic %s", authData)
} else if cred.authType == "bearer" {
cred.bearerToken = authData
cred.authHeader = fmt.Sprintf("Bearer %s", cred.bearerToken)
} else if cred.authType == "negotiate" {
cred.UserName = "Unauthenticated"
} else {
return nil, errors.New(fmt.Sprintf("Unsuported authentication type %s", authType))
}
return cred, nil
}
func NewCredentialsForSystem() (*Credentials, error) {
token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return nil, err
}
return NewCredentials("bearer", string(token))
}

View File

@ -1,232 +0,0 @@
/*
* 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/>.
*/
package main
import (
"cockpit-kube-auth/helpers"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strconv"
"syscall"
"time"
)
type ChallengeResponse struct {
Cookie string `json:"cookie"`
Response string `json:"response"`
Command string `json:"command"`
}
func readSize(fd int) (int, error) {
single := make([]byte, 1)
sep := byte('\n')
var size int64 = 0
seen := 0
for true {
_, err := syscall.Read(fd, single)
if err != nil {
return -1, err
}
if single[0] == sep {
break
}
i, e := strconv.ParseInt(string(single), 10, 64)
if e != nil {
return -1, errors.New("Invalid frame: invalid size")
}
size = size * 10
size = size + i
seen++
if seen > 7 {
return -1, errors.New("Invalid frame: size too long")
}
}
return int(size), nil
}
func readFrame(fd int) ([]byte, error) {
size, sizeErr := readSize(fd)
if sizeErr != nil {
return nil, sizeErr
}
data := make([]byte, 0)
for size > 0 {
buffer := make([]byte, size)
i, err := syscall.Read(fd, buffer)
if err != nil {
return nil, err
}
if i == 0 {
break
}
size = size - i
data = append(data, buffer[:i]...)
}
if size > 0 {
return nil, errors.New(fmt.Sprintf("Invalid frame: Missing %d bytes", size))
}
return data, nil
}
func getCockpitControlMsg(iface interface{}) error {
buf, err := readFrame(syscall.Stdin)
if err == nil {
err = json.Unmarshal(buf, iface)
}
return err
}
func sendCockpitControlMsg(data interface{}) error {
response, respErr := json.Marshal(data)
if respErr != nil {
return respErr
}
_, err := syscall.Write(syscall.Stdout, []byte(fmt.Sprintf("%d\n\n", len(response)+1)))
if err == nil {
_, err = syscall.Write(syscall.Stdout, response)
}
return err
}
func sendAuthorization(login_data map[string]interface{}) error {
data := make(map[string]interface{})
data["command"] = "authorize"
data["challenge"] = "x-login-data"
data["cookie"] = "kube-auth-unused"
data["login-data"] = login_data
return sendCockpitControlMsg(data)
}
func sendInitProblem(err error) error {
log.Println(err)
errorType := "internal-error"
if _, ok := err.(*helpers.AuthError); ok {
errorType = "authentication-failed"
}
data := make(map[string]interface{})
data["command"] = "init"
data["problem"] = errorType
data["message"] = fmt.Sprintf("%s", err)
return sendCockpitControlMsg(data)
}
func challengeForAuthData() ([]byte, error) {
t := time.Now()
data := make(map[string]interface{})
data["command"] = "authorize"
data["challenge"] = "*"
data["cookie"] = fmt.Sprintf("cookie%d%d", os.Getpid(), t.Unix())
err := sendCockpitControlMsg(data)
if err != nil {
return nil, nil
}
r := ChallengeResponse{}
fetchErr := getCockpitControlMsg(&r)
if fetchErr != nil {
return nil, fetchErr
}
if r.Command != "authorize" {
return nil, errors.New(fmt.Sprintf("Got invalid command %s", r.Command))
}
return []byte(r.Response), nil
}
func runStub() int {
var wstatus syscall.WaitStatus
sysProcAttr := &syscall.SysProcAttr{
Pdeathsig: syscall.SIGTERM,
}
procAttr := &syscall.ProcAttr{
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
Sys: sysProcAttr,
}
pid, fork_err := syscall.ForkExec("/usr/libexec/cockpit-stub", nil, procAttr)
if fork_err != nil {
log.Fatal("Error forking process:", fork_err)
}
_, wait_err := syscall.Wait4(pid, &wstatus, 0, nil)
for wait_err == syscall.EINTR {
_, wait_err = syscall.Wait4(pid, &wstatus, 0, nil)
}
if wait_err != nil {
log.Fatal("Error waiting on bridge pid:", wait_err)
}
return wstatus.ExitStatus()
}
func main() {
authData, err := challengeForAuthData()
if err != nil {
log.Fatal("Error reading authentication data ", err)
}
client := helpers.NewClient()
response, loginErr := client.Login(string(authData))
if loginErr != nil {
err = sendInitProblem(loginErr)
} else {
err = sendAuthorization(response)
}
if err != nil {
log.Fatal("Error sending auth result", err)
}
if err == nil && loginErr == nil {
if os.Getenv("XDG_RUNTIME_DIR") == "" {
os.Setenv("XDG_RUNTIME_DIR", "/tmp")
}
status := runStub()
err = client.CleanUp()
if err != nil {
log.Fatal("Error deleting token", err)
}
os.Exit(status)
}
}

View File

@ -1,174 +0,0 @@
/*
* 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/>.
*/
package main
import (
"cockpit-kube-auth/helpers"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path"
"strconv"
"syscall"
"text/template"
)
var OAUTH_CLIENT_ID = "cockpit-kube-client"
var CONFIG_FILE = "/etc/cockpit/cockpit.conf"
var CONFIG_TEMPLATE = "cockpit.conf.tmpl"
var confDir = flag.String("config-dir", "/container", "Directory with cockpit-kube-launch config files")
func writeConfigFile(confData map[string]interface{}) {
tmpl, tErr := template.New("cockpit.conf.tmpl").ParseFiles(path.Join(*confDir, CONFIG_TEMPLATE))
if tErr != nil {
log.Fatalf("Invalid cockpit.conf template: %s", tErr)
}
f, fErr := os.Create(CONFIG_FILE)
if fErr != nil {
log.Fatalf("Could't open file config file: %s", fErr)
}
wErr := tmpl.Execute(f, confData)
if wErr != nil {
log.Fatalf("Could't write config file: %s", wErr)
}
}
func linkFiles(src string, target string) {
// Not using symlink because os.Symlink doesn't let you force.
out, err := exec.Command("ln", "-sf", src, target).CombinedOutput()
if err != nil {
log.Fatalf("Could't link %s to %s: %v: %s", src, target, err, out)
}
}
func setupCertificates() {
// Ensure path exits
dErr := os.MkdirAll("/etc/cockpit/ws-certs.d", os.ModeDir|0775)
if dErr != nil {
log.Fatalf("Couldn't create certificate directory %s", dErr)
}
// Finding these certificate files or setting ownership on
// the certificate may fail so execute combine and ensure
// and the check results without
exec.Command("/usr/sbin/remotectl", "certificate",
"/var/run/secrets/ws-certs.d/tls.crt",
"/var/run/secrets/ws-certs.d/tls.key").CombinedOutput()
exec.Command("/usr/sbin/remotectl", "certificate", "--ensure").CombinedOutput()
out, rErr := exec.Command("/usr/sbin/remotectl", "certificate").CombinedOutput()
if rErr != nil {
log.Fatalf("Failed to generate certificates %s %s", rErr, out)
}
}
func haveOpenShiftEndpoint() (bool, error) {
var isOpenShift bool = false
creds, err := helpers.NewCredentialsForSystem()
if err != nil {
return isOpenShift, err
}
client := helpers.NewClient()
resp, e := client.DoRequest("GET", "oapi", "", creds, nil)
if e != nil {
return isOpenShift, e
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
isOpenShift = true
}
return isOpenShift, nil
}
func main() {
flag.Parse()
var isOpenShift bool = false
args := []string{
"/usr/libexec/cockpit-ws",
}
insecure, _ := strconv.ParseBool(os.Getenv("COCKPIT_KUBE_INSECURE"))
if insecure {
args = append(args, "--no-tls")
}
setupCertificates()
name := "kubernetes"
isRegistry, _ := strconv.ParseBool(os.Getenv("REGISTRY_ONLY"))
oauth_url := os.Getenv("OPENSHIFT_OAUTH_PROVIDER_URL")
if oauth_url != "" {
isOpenShift = true
client_id := os.Getenv("OPENSHIFT_OAUTH_CLIENT_ID")
if client_id == "" {
client_id = OAUTH_CLIENT_ID
}
oauth_url = fmt.Sprintf("%s/oauth/authorize?client_id=%s&response_type=token",
oauth_url, client_id)
} else {
oauth_url = os.Getenv("OAUTH_PROVIDER_URL")
var osErr error = nil
isOpenShift, osErr = haveOpenShiftEndpoint()
if osErr != nil {
log.Printf("Error checking for openshift endpoint %s", osErr)
}
}
if isRegistry {
name = "registry"
} else if isOpenShift {
name = "openshift"
}
confData := make(map[string]interface{})
confData["login_command"] = "/usr/libexec/cockpit-kube-auth"
confData["oauth_url"] = oauth_url
confData["is_openshift"] = isOpenShift
confData["is_registry"] = isRegistry
confData["origins"] = os.Getenv("COCKPIT_KUBE_URL");
writeConfigFile(confData)
override := path.Join(*confDir, fmt.Sprintf("%s-override.json", name))
brand := path.Join(*confDir, fmt.Sprintf("%s-brand", name))
linkFiles(override, "/usr/share/cockpit/shell/override.json")
linkFiles(brand, "/container/os-release")
if isRegistry {
registry_override := path.Join(*confDir, "registry-dashboard-override.json")
linkFiles(registry_override, "/usr/share/cockpit/kubernetes/override.json")
linkFiles("/usr/share/cockpit/kubernetes/registry.html.gz",
"/usr/share/cockpit/kubernetes/index.html.gz")
} else {
linkFiles("/usr/share/cockpit/kubernetes/original-index.gz",
"/usr/share/cockpit/kubernetes/index.html.gz")
}
syscall.Exec(args[0], args, os.Environ())
}

View File

@ -1,216 +0,0 @@
#graphs {
display: block;
}
#graphs svg {
font-size: 10px;
}
#graphs ul {
padding-top: 15px;
margin-right: 10px;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis .domain,
.axis .tick line {
stroke: lightgrey;
opacity: 0.5;
}
.tick text {
fill: #545454;
font-size: 12px;
}
path.line {
stroke: steelblue;
fill: none;
stroke-width: 2;
}
path.line.highlight {
stroke-width: 4;
}
/* Page global stuff */
.container-fluid {
margin-top: 0px;
padding-left: 0px;
}
.container-more {
margin-top: 10px;
}
/* Dashboard */
#content {
margin-left: 110px;
margin-right: 0px;
min-height: 100%;
}
#content > .row {
margin-right: 0px;
}
#content > *:last-child {
padding-bottom: 20px;
}
/* When panel is editable, then buttons are visible */
.dashboard-list td button {
display: none;
}
.dashboard-list.editable td button {
display: block;
}
.dashboard-list.editable td.status > span {
display: none;
}
.dashboard-list.editable tr:hover td {
background-color: #FFF;
cursor: auto;
}
.dashboard-list td.status {
padding: 4px 10px 4px !important;
text-align: right;
white-space: nowrap;
vertical-align: middle;
width: 1%;
min-width: 50px;
}
.dashboard-list span.pficon,
.dashboard-list span.fa {
font-size: 18px;
color: black;
display: inline-block;
}
.dashboard-list span.spinner {
display: inline-block;
}
.dashboard-list span.fa-failed {
color: #af151a;
}
/* App deploy dialog */
#deploy-app-deploying > span {
margin-left: 7px;
}
#deploy-app-manifest-file {
opacity: 0.0001;
height: 1px;
}
#deploy-app-manifest-file-button {
width: 100%;
padding-right: 25px;
}
.deploy-app-type-text {
font-style: normal;
text-align: left;
}
.manifest_file {
font-style: normal;
text-align: left;
font-size: 11px;
}
.manifest_file_default {
text-align: left;
font-style: italic;
font-size: 11px;
}
.combobox_option {
width: 100%;
}
.deploy-dialog-aids {
margin-bottom: 0px;
}
/* Replicas dialog */
input.adjust-replica {
width: 70px;
}
#adjust-dialog .modal-dialog {
width: 400px;
}
#deploy-app-type-field > .bootstrap-select {
margin-bottom: 3px;
}
.app-name {
color: #333333;
margin-top: 10px;
text-align: left;
}
/* Used to hide angular elements until they are ready */
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
input.adjust-replica {
width: 200px;
}
.console-ct pre {
background-color: #fcfcfc;
border: 1px solid #cccccc;
}
pre.dialog {
height: 200px;
width: 500px;
border: 1px solid #cccccc;
}
.action-menu {
display: block;
float: right;
}
/* Input combo replacement */
.input-combo .input-group-btn {
position: static;
}
.input-combo .dropdown-menu {
left: 0;
right: 0;
}
/* overide since registry-image-widget sets
the last column to be aligned right
but we don't want that when we are
using colspan
*/
table.listing-ct > thead td[colspan]:last-child,
table.listing-ct > thead th[colspan]:last-child {
text-align: left;
}

View File

@ -1,19 +0,0 @@
.containers-listing {
width: 100%;
}
.containers-listing i.fa-cube,
.content-filter i.fa-cube {
position: relative;
font-size: 22px;
}
.containers-listing .console-ct {
min-height: 310px;
}
kubernetes-container-terminal .terminal-actions {
position: relative;
z-index: auto;
}

View File

@ -1,108 +0,0 @@
.container-cards-pf {
display: flex;
margin-top: 10px;
}
.card-pf {
flex-grow: 1;
margin: 10px;
display: flex;
flex-direction: column;
flex-basis: 25%;
}
.card-pf-aggregate-status .card-pf-body {
margin-bottom: 0px;
}
.card-pf-wide {
flex-basis: ~"calc(75% + 20px)";
}
.card-pf-double {
flex-basis: ~"calc(66% - 76px)";
}
.dashboard-cards {
width: 100%;
max-width: 1024px;
padding-right: 7px;
}
.card-pf-aggregate-status-notification .spinner {
display: inline-block;
margin-right: 7px;
}
.card-pf-aggregate-status-text {
font-size: 13px;
vertical-align: 3px;
}
.card-pf-heading {
margin-bottom: 0px;
button {
margin: 15px 0px;
}
i {
font-size: 24px;
line-height: 16px;
vertical-align: top;
padding-right: 5px;
}
}
.card-pf-footer {
min-height: 5em;
}
.card-pf-body {
margin-top: 10px;
margin-bottom: auto;
}
.card-pf-body.blank-slate-pf {
background: transparent;
padding-top: 20px;
}
.card-pf-body table.listing-ct {
margin-top: 0px;
width: 100%;
}
.card-pf-body table.listing-ct thead th {
border-top: none;
}
.card-pf-body table.listing-ct tbody:last-child {
border-bottom: none;
}
.card-pf-body table.listing-ct tbody:last-child tr:last-child {
border-bottom: none;
}
.pvc-notice {
border-top: 1px solid #d1d1d1;
padding-top: 5px;
}
.pvc-notice .pficon {
font-size: 18px;
margin-right: 5px;
color: #72767b;
}
code {
display: block;
white-space: pre;
font-size: 13px;
margin-bottom: 1em;
}
.form-table-ct label span[translate] {
vertical-align: top;
}

View File

@ -1,75 +0,0 @@
.details-listing {
min-width: 70% !important;
}
.details-listing i.pficon-service {
font-size: 30px;
line-height: 20px;
position: relative;
padding-right: 5px;
left: -5px;
}
.details-listing i.pficon-route {
position: relative;
font-size: 25px;
line-height: 20px;
left: -2px;
padding-right: 8px;
}
.details-listing i.fa-server {
position: relative;
top: 3px;
}
.content-filter i.pficon-service {
margin-right: 5px;
}
.details-listing i.pficon-replicator {
position: relative;
font-size: 28px;
top: -6px;
}
.content-filter i.pficon-replicator {
margin-right: 5px;
}
.content-filter i.pficon-container-node {
top: 3px;
margin-right: 5px;
}
.details-listing i.fa-cubes {
}
.details-listing i.fa-gear {
position: relative;
font-size: 25px;
top: -3px;
}
.details-listing dl dd dl.inline-dl {
padding-left: 15px;
}
.details-listing dl dd dl.inline-dl dt {
text-align: left;
}
.listing-ct-inline .console-ct > pre {
text-align: left;
margin: 0;
}
.type-filter .btn,
.type-filter .dropdown-menu {
font-size: 13px;
}
.type-filter .btn-group.open .dropdown-toggle {
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.125) inset;
}

View File

@ -1,12 +0,0 @@
.dropdown-menu {
li a:before {
font-family: FontAwesome;
content: "\f00c";
margin-right: 5px;
visibility: hidden;
}
li.checked a:before {
visibility: visible;
}
}

View File

@ -1,89 +0,0 @@
@filter-height: 50px;
.content-filter {
background-color: @sidebar-pf-bg;
padding: 10px 16px 10px 20px;
border-bottom: 1px @sidebar-pf-border-color solid;
position: fixed;
top: 0px;
right: 0px;
z-index: 1;
height: @filter-height;
h3 {
display: inline;
font-size: 18px;
line-height: 28px;
}
h3 i {
font-size: 24px;
position: relative;
padding-right: 3px;
}
i.pficon-image {
top: 3px;
}
i.fa {
margin-top: 2px;
font-size: 18px;
line-height: 28px;
}
i.fa-folder {
font-size: 22px;
}
a {
padding-left: 30px;
}
}
@media (min-width: 700px) {
.content-filter {
a.hidden-xs {
display: inline !important;
}
}
}
.content-filter + div {
padding-top: @filter-height;
}
.namespace-filter .btn,
.namespace-filter .dropdown-menu {
font-size: 13px;
}
.namespace-filter .btn-group.open .dropdown-toggle {
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.125) inset;
}
.namespace-filter .namespace-meta {
color: @metadata-color;
}
.filter-menu {
display: block;
.caret {
height: 10px;
}
}
.filter-menu > .btn + .dropdown-toggle {
padding: 3px 1px;
min-width: 0;
border-left: none;
height: 26px;
}
.filter-menu > .btn:first-child {
border-right: none;
height: 26px;
font-size: 15px;
}

View File

@ -1,66 +0,0 @@
@import (less) "../../../node_modules/registry-image-widgets/dist/image-widgets.css";
@tag-border: @link-color;
table.images-listing {
max-width: 2000px;
min-width: 85%;
tbody:hover {
background-color: #edf8ff;
}
thead {
th {
border-top: none;
}
}
tr.listing-ct-item {
td.tag-label {
vertical-align: top;
background: linear-gradient(90deg, transparent 10px, @tag-border 10px, @tag-border 12px, transparent 12px);
}
td.listing-ct-toggle:hover + td.tag-label {
background-color: #ededed
}
border-top: none;
border-bottom: none;
}
tbody.first {
tr.tag-item td {
padding-top: 20px;
}
}
tbody.last {
tr.listing-ct-item.tag-item td.tag-label {
background-size: 100% 20px;
background-repeat: no-repeat;
}
}
tr.imagestream-item {
border-top: 1px solid @gray-lighter;
th, td {
border-bottom: 2px solid @tag-border;
font-size: 14px;
padding-bottom: 5px;
}
}
tr.listing-ct-item tt {
color: @metadata-color;
}
}
registry-imagestream-listing {
table.listing-ct {
margin-top: -15px;
}
}

View File

@ -1,28 +0,0 @@
@import "variables.less";
@import (less) "../../../node_modules/angular/angular-csp.css";
@import (less) "../../lib/page.css";
@import "../../lib/listing.less";
@import (less) "../../lib/console.css";
@import (less) "../../lib/table.css";
/* Depends on number of items in the sidebar */
@sidebar-height-lg: 630px;
@import "app.less";
@import (less) "../../../node_modules/kubernetes-container-terminal/dist/container-terminal.css";
@import "containers.less";
@import "details.less";
@import (less) "../../../node_modules/kubernetes-topology-graph/dist/topology-graph.css";
@import "topology.less";
@import "revealable.less";
@import "dashboard.less";
@import "sidebar.less";
@import "filter.less";
@import "dropdown.less";
@import "tags.less";
@import "images.less";
@import "projects.less";
@import "volumes.less";
@import "nodes.less";

View File

@ -1,112 +0,0 @@
.node-status {
padding-right: 3px;
}
.node-alert {
margin-top: 20px;
}
div.nodes-delete ul {
padding-top: 10px;
}
div.nodes-delete ul li {
padding-bottom: 10px;
}
.nodes-heatmap-card {
flex-basis: 66%;
.card-pf-title {
margin-top: 10px;
}
.node-heatmap {
height: 90px;
}
ul.chart-legend {
margin-top: 30px;
}
li.chart-legend-item {
float: left;
display: inline;
}
li.chart-legend-item:first-of-type span.legend-pf-color-box{
margin-left: 0px;
}
li.chart-legend-item span.legend-pf-color-box {
margin-left: 5px;
}
rect {
cursor: pointer;
}
}
.nodes-os-card {
flex-basis: 33%;
#os-counts-graph {
height: 150px;
}
h2 {
text-align: center;
margin-bottom: 0;
}
@media (min-width: 992px) {
div.row {
display: flex;
align-items: center;
}
}
}
.chart-title {
text-anchor: middle;
dominant-baseline: middle;
}
.chart-focused {
opacity: 1;
cursor: pointer;
}
.chart-unfocused {
opacity: 0.3;
}
ul.chart-legend {
list-style-type: none;
margin-top: 5px;
padding: 0;
overflow: auto;
}
li.chart-legend-item {
cursor: pointer;
}
li.chart-legend-item span.legend-pf-color-box {
width: 11px;
height: 11px;
margin-right: 5px;
display: inline-block;
}
li.chart-legend-item span.legend-pf-text {
font-size: 11px;
font-weight: 400;
line-height: 11px;
margin-right: 5px;
}
.nodes-container-cards svg {
min-width: 90px;
min-height: 90px;
}

View File

@ -1,141 +0,0 @@
@tag-border: @link-color;
table.project-body {
margin-top: 0px;
thead.th {
border-top:0px;
font-size: 12px;
}
tr {
display: table-row;
vertical-align: inherit;
border-color: inherit;
}
tr.listing-ct-item {
border-top: 1px solid color("#eee");
border-bottom: 1px solid color("#eee");
cursor: pointer;
}
}
.close-icon {
color: #000000;
}
.user-membership-body {
overflow-y: auto;
overflow-x: hidden;
max-height: 200px;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
padding-top: 10px;
padding-bottom: 10px;
h4 {
color: #888;
margin-top: 0px;
margin-bottom: 5px;
font-size: 14px;
}
span {
display: block;
margin-left: 20px;
}
}
.user-body {
border: 0px;
table.listing-ct thead th {
border-bottom: 0px;
border-bottom: 1px solid color("#eee");
}
}
table.listing-ct > thead td {
padding-top: 0px;
}
.listing-ct-head .pficon-project {
margin-bottom: 10px;
}
span.project-description {
font-size: 13px;
font-weight: normal;
}
p.project-description {
margin-top: 1em;
}
a.input-icon {
position: absolute;
right: 18px;
}
.project-panel {
padding-right: 25px;
padding-top: 5px;
}
.project-panel-actions {
padding-right: 5px;
padding-top: 5px;
}
table.projects-listing {
max-width: 2000px;
min-width: 85%;
thead {
th {
border-top: 0px;
}
}
tr {
display: table-row;
vertical-align: inherit;
border-color: inherit;
}
.details-listing {
min-width: 70% !important;
}
tr.listing-ct-item {
border-top: 1px solid color("#eee");
border-bottom: 1px solid color("#eee");
cursor: pointer;
}
tr.inner-project-listing {
display: table-row;
}
tbody.first {
tr.tag-item td {
padding-top: 20px;
}
}
tbody.last {
tr.listing-ct-item.tag-item td:first-child {
background-size: 100% 20px;
background-repeat: no-repeat;
}
}
tr.tag-item td {
padding-bottom: 0px;
}
tr.listing-ct-item tt {
color: @metadata-color;
}
}

View File

@ -1,248 +0,0 @@
@import "variables.less";
@import (less) "../../../node_modules/angular/angular-csp.css";
@import (less) "../../lib/page.css";
@import "../../lib/listing.less";
@import (less) "../../lib/console.css";
@import (less) "../../lib/table.css";
@icon-sidebar-width: 100px;
@import "filter.less";
@import "dropdown.less";
@import "dashboard.less";
@import "images.less";
@import "projects.less";
@import "tags.less";
#content {
margin-right: 0px;
min-height: 100%;
padding-bottom: 20px;
}
.listing-ct-body dl {
margin-bottom: 13px;
}
/* Registry dashboard */
.card-pf-utilization .card-pf-title {
line-height: 1.1;
}
.dashboard-storage {
img {
display: block;
margin: 20px auto 0px auto;
}
}
.dashboard-images {
.card-pf-heading {
position: relative;
.card-pf-title {
max-width: 80%;
}
}
.card-pf-body {
min-height: 220px;
padding-bottom: 0px;
div {
padding: 7px 10px 10px 10px;
cursor: pointer;
}
div:hover {
background-color: @table-bg-hover;
}
ul {
background: linear-gradient(90deg, transparent 20px, @tag-border 20px, @tag-border 22px, transparent 22px);
background-size: 100% 20px;
background-repeat: no-repeat;
padding: 3px 0px 0px 20px;
margin-bottom: 0px;
line-height: 30px;
li {
background: linear-gradient(180deg, transparent 8px, @tag-border 8px, @tag-border 10px, transparent 10px);
background-repeat: no-repeat;
padding-left: 7px;
display: inline;
list-style: none;
}
li:first-child {
padding-left: 15px;
}
li.image-tag-truncated {
padding-left: 15px;
opacity: 0.5;
}
}
}
.namespace-filter {
margin-top: -3px;
}
.namespace-filter button {
margin-bottom: 0px;
}
.all-images {
display: block;
float: right;
margin-bottom: 10px;
}
.registry-imagestream-lock {
display: block;
float: right;
margin: 9px;
}
.table > tbody > tr > td {
border-top: none;
font-weight: bold;
padding: 0px;
}
.table a {
display: block;
color: inherit;
padding: 8px 15px;
}
dt {
font-size: 13px;
}
dt a {
text-decoration: none;
color: inherit;
}
dd {
float: right;
margin-left: 20px;
}
}
.dashboard-commands {
p {
font-size: 13px;
font-weight: bold;
}
}
.blank-slate-pf {
border: none;
}
#content {
margin-left: unit(@icon-sidebar-width, px);
}
.content-filter {
left: unit(@icon-sidebar-width - 10, px);
}
.content-filter + div {
padding-left: 10px;
padding-right: 17px;
}
/* A different sidebar */
.icon-sidebar {
width: unit(@icon-sidebar-width, px);
ul {
margin: 0;
padding: 0;
list-style: none;
width: unit(@icon-sidebar-width - 10, px);
height: 100%;
position: fixed;
background-color: @nav-pf-vertical-secondary-bg-color;
top: 0px;
z-index: 1;
li {
border-bottom: 1px solid @nav-pf-vertical-border-color;
display: block;
font-size: 13px;
a {
border-bottom: 0;
color: @nav-pf-vertical-secondary-color;
background-color: @nav-pf-vertical-secondary-bg-color;
padding: 15px 11px 15px 9px;
text-align: center;
display: block;
text-decoration: none;
position: relative;
i {
display: inline;
font-size: 20px;
line-height: 20px;
color: @nav-pf-vertical-icon-color;
}
i.pficon-image {
font-size: 24px;
line-height: 20px;
}
}
a:hover {
color: @nav-pf-vertical-secondary-active-color;
background-color: @nav-pf-vertical-secondary-active-bg-color;
i {
color: @nav-pf-vertical-active-icon-color;
}
}
}
li.active {
a {
color: @nav-pf-vertical-secondary-active-color;
background-color: @nav-pf-vertical-secondary-active-bg-color;
i {
color: @link-color;
}
}
a:after {
content: '';
width: 4px;
height: 100%;
left: 0;
top: 0;
display: block;
position: absolute;
background-color: @link-color;
}
}
}
}
/* overide since registry-image-widget sets
the last column to be aligned right
but we don't want that when we are
using colspan
*/
table.listing-ct > thead td[colspan]:last-child,
table.listing-ct > thead th[colspan]:last-child {
text-align: left;
}

View File

@ -1,46 +0,0 @@
.revealable {
white-space: nowrap !important;
text-overflow: ellipsis;
overflow: hidden !important;
}
.revealable.clickable {
cursor: pointer;
}
.revealed {
white-space: normal !important;
overflow: visible !important;
cursor: pointer;
overflow-wrap: break-word;
}
.masked {
color: transparent;
position: relative;
text-ellipsis: none !important;
cursor: pointer;
max-width: 1;
display: inline-block;
}
.masked:after {
content: "●●●●";
position: absolute;
left: 0px;
font-weight: bold;
letter-spacing: 2px;
padding-left: 2px;
color: black;
cursor: pointer;
line-height: 1.5;
opacity: 0.5;
}
.revealed .masked:after {
display: none;
}
.revealed .masked {
color: black;
}

View File

@ -1,126 +0,0 @@
/*
* Full large size sidebar.
*/
.nav-sidebar {
background: #393f44;
width: unit(@sidebar-width-xl, px);
height: 100%;
position: fixed;
top: 0px;
z-index: 1;
overflow-x: hidden;
overflow-y: auto;
border-right: 1px solid #292e34;
border-bottom: none;
border-top: none;
border-left: none;
bottom: 0;
a {
width: unit(@sidebar-width-xl, px) !important;
}
.list-group-item-value {
max-width: unit(@sidebar-width-xl - 60, px) !important;
padding-right: 10px !important;
}
}
.nav-item-pf-header {
color: #fff;
font-size: 16px;
margin: 18px 20px 10px 20px;
}
.nav-sidebar .list-group {
border: none;
}
.nav-sidebar .list-group-item {
background: inherit;
border: none;
}
.nav-sidebar .list-group-item.active {
background-color: #4d5258;
}
.nav-sidebar > .list-group > .list-group-item.active > a {
background-color: inherit;
}
.nav-sidebar .list-group-item a {
display: block;
color: #d1d1d1;
}
#content {
margin-left: unit(@sidebar-width-xl + 20, px);
}
.content-filter {
left: unit(@sidebar-width-xl, px);
}
/*
* Very small. Only icons
*/
@media (max-width: @screen-sm-min) {
.nav-sidebar {
.nav-item-pf-header {
display: none;
}
width: unit(@sidebar-width-sm, px);
.list-group-item-value {
display: none !important;
}
a {
padding: 8px !important;
height: 40px !important;
width: unit(@sidebar-width-sm, px) !important;
}
}
#content {
margin-left: unit(@sidebar-width-sm + 20, px);
}
.content-filter {
left: unit(@sidebar-width-sm, px);
}
}
/*
* Short window. Text next to icons.
*/
@media (min-width: @screen-sm-min) and (max-width: @screen-lg-min), (min-width: @screen-sm-min) and (max-height: @sidebar-height-lg) {
.nav-sidebar {
width: unit(@sidebar-width-md, px);
a {
padding: 7px 8px 5px 8px !important;
height: 40px !important;
width: unit(@sidebar-width-md, px) !important;
i {
float: none !important;
margin-right: 0px !important;
}
span {
display: inline !important;
font-size: 12px;
margin-left: 4px;
margin-top: 2px;
}
}
}
#content {
margin-left: unit(@sidebar-width-md + 20, px);
}
.content-filter {
left: unit(@sidebar-width-md, px);
}
}

View File

@ -1,54 +0,0 @@
.image-tag {
border: 2px solid @tag-border;
border-radius: 7px;
padding: 3px 5px;
white-space: nowrap;
background-color: @listing-ct-hover;
}
a.image-tag {
color: inherit;
}
.image-tag-editor {
-moz-appearance: textfield;
-webkit-appearance: textfield;
background-color: white;
background-color: -moz-field;
border: 1px solid darkgray;
box-shadow: 1px 1px 1px 0 lightgray inset;
font: -moz-field;
font: -webkit-small-control;
padding: 4px;
font-size: 13px;
max-width: 500px;
}
.image-tag-editor:focus {
border-color: #66afe9;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
}
.image-tag-editor .image-tag {
padding: 1px 5px;
margin: 2px 1px 2px 1px;
cursor: default;
display: inline-block;
}
.image-tag a {
color: #999;
position: relative;
padding-left: 4px;
font-size: 12px;
top: 1px;
}
.image-tag a:hover {
color: black;
text-decoration: none;
}

View File

@ -1,60 +0,0 @@
kubernetes-topology-graph {
margin-right: 25%;
margin-top: 55px;
padding-bottom: 0px !important;
overflow: hidden;
}
.kube-pane {
float: right;
width: 25%;
.sidebar-pf-right {
right: 0px;
}
}
.kube-pane .sidebar-pf {
position: fixed;
width: 25%;
height: 100%;
padding: 10px;
overflow: hidden;
}
.kube-pane .dl-horizontal {
}
.kube-pane .dl-horizontal dt {
text-align: left;
width: 90px;
overflow: hidden;
text-overflow: ellipsis;
}
.kube-pane .dl-horizontal dd {
margin-left: 100px;
margin-right: 30px;
overflow: hidden;
text-overflow: ellipsis;
}
.kube-pane .sidebar-help p {
margin-right: 30px;
margin-bottom: 30px;
}
.kube-pane .sidebar-help h3 {
padding-top: 7px;
padding-bottom: 10px;
}
kubernetes-topology-icon {
display: block;
float: left;
padding: 0 10px 5px 0;
}
.topology-actions {
float: right;
}

View File

@ -1,7 +0,0 @@
@import "../../lib/variables.less";
@sidebar-width-sm: 40;
@sidebar-width-md: 145;
@sidebar-width-xl: 185;
@command-bg-color: #eee;

View File

@ -1,19 +0,0 @@
label.inline {
margin-right: 16px;
}
div.modal-body ul.dialog-list-ct {
padding-top: 10px;
}
div.modal-body ul.dialog-list-ct li {
padding-bottom: 10px;
}
.pvc-listing button.btn-danger {
visibility: hidden;
}
.pvc-listing tr.listing-ct-item:hover button.btn-danger {
visibility: visible;
}

View File

@ -1,21 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h4 class="modal-title" translate>Add Group</h4>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td class="top">
<label class="control-label" for="group_name" translate>Name</label>
</td>
<td>
<input id="group_name" ng-model="fields.name" class="form-control" type="text" autofocus>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-primary" translate ng-click="complete(performCreate())">Create</button>
</div>
</modal-dialog>

View File

@ -1,63 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h4 class="modal-title">
<span translate>Add Member</span>
<a tabindex="0" role="button" popover-trigger="focus" popover-placement="bottom"
popover="A member is a user or group.">
<span class="fa fa-lg fa-info-circle"></span>
</a>
</h4>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td class="top">
<label class="control-label" for="add_member" translate>User or Group</label>
</td>
<td>
<div id="add_member_group" class="input-group bootstrap-select input-combo form-control" dropdown>
<input id="add_member_name" class="form-control" type="text" placeholder="Select or type a member"
ng-model="selected.memberName">
<span class="input-group-btn" dropdown="">
<button class="btn btn-default dropdown-toggle" dropdown-toggle>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-left">
<li ng-repeat="member in selected.getMembers() track by itemTracker(member)" ng-class="{active: member.metadata.name == selected.member}">
<a tabindex="0" ng-click="selected.member = member.metadata.name; selected.memberName = member.metadata.name; selected.memberObj = member"
value="{{ member.metadata.name }}">
{{ member.metadata.name }}
</a>
</li>
</ul>
</span>
</div>
</td>
</tr>
<tr>
<td class="top">
<label class="control-label" for="add_role" translate>Roles</label>
</td>
<td>
<div id="add_role" class="btn-group bootstrap-select form-control" dropdown>
<button class="btn btn-default dropdown-toggle" dropdown-toggle>
<span class="pull-left">{{ selected.displayRole }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="roleMp in selected.roles track by roleMp.displayRole" ng-class="{active: roleMp.displayRole == selected.displayRole}">
<a tabindex="0" ng-click="selected.displayRole = roleMp.displayRole; selected.ocRole = roleMp.ocRole" value="{{ roleMp.displayRole }}">
{{ roleMp.displayRole }}
</a>
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-primary" ng-click="complete(performCreate())" translate>Add</button>
</div>
</modal-dialog>

View File

@ -1,10 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h3 class="modal-title" translate>Add Role</h3>
</div>
<div class="modal-body" translate>Do you want to add the role '{{ fields.displayRole }}'?</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-primary" translate ng-click="complete(performCreate())">Add</button>
</div>
</modal-dialog>

View File

@ -1,29 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h4 class="modal-title" translate>Add User</h4>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td class="top">
<label class="control-label" for="user_name" translate>Name</label>
</td>
<td>
<input id="user_name" ng-model="fields.name" class="form-control" type="text" autofocus>
</td>
</tr>
<tr>
<td class="top">
<label class="control-label" for="identities" translate>Identity</label>
</td>
<td>
<input id="identities" ng-model="fields.identities" class="form-control" type="text">
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-primary" translate ng-click="complete(performCreate())">Add</button>
</div>
</modal-dialog>

View File

@ -1,9 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h3 class="modal-title" ng-if="!error" translate>Connection Settings</h3>
<h3 class="modal-title" ng-if="error" translate>Connection Error</h3>
</div>
<auth-form ng-if="error.problem != 'unknown-hostkey'"></auth-form>
<auth-rejected-cert ng-if="error.problem == 'unknown-hostkey'"></auth-rejected-cert>
</modal-dialog>

View File

@ -1,132 +0,0 @@
<div class="modal-body">
<table class="form-table-ct" ng-if="fields">
<tr ng-if="haveKubectl">
<td class="top">
<label class="control-label" translate>Cluster</label>
</td>
<td class="dialog-wrapper">
<div id="kubernetes-cluster" class="btn-group bootstrap-select form-control" dropdown>
<button class="btn btn-default dropdown-toggle" type="button" dropdown-toggle>
<span class="pull-left" ng-if="currentCluster.name">{{ currentCluster.name }}</span>
<span class="pull-left" ng-if="!currentCluster.name" translate>Add New Cluster</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="cluster in clusters" ng-class="{ checked: currentCluster.name == cluster.name }">
<a tabindex="0" ng-click="selectCluster(cluster)">{{cluster.name}}</a>
</li>
<li role="separator" class="divider"></li>
<li ng-class="{ checked: !currentCluster.name }">
<a tabindex="0" ng-click="selectCluster()" translate>Add New Cluster</a>
</li>
</ul>
</div>
</td>
</tr>
<tr>
<td class="top">
<label class="control-label" for="kubernetes-address"
translate>Address</label>
</td>
<td>
<input id="kubernetes-address" class="form-control" type="text"
ng-model="fields.address" placeholder="eg: https://localhost:6443">
</td>
</tr>
<tr>
<td></td>
<td>
<label>
<input id="kubernetes-skip-verify" type="checkbox" ng-model="fields.skipVerify">
<span translate>Skip Certificate Verification</span>
</label>
</td>
</tr>
<tr >
<td class="top">
</td>
<td>
<label>
<input id="kubernetes-requires-auth" type="checkbox" ng-checked="useAuth" ng-click="toggleAuth()">
<span translate>Requires Authentication</span>
</label>
</td>
</tr>
<tr ng-if="haveKubectl && useAuth">
<td class="top">
<label class="control-label" translate>User</label>
</td>
<td class="dialog-wrapper">
<div id="kubernetes-user" class="btn-group bootstrap-select form-control" dropdown>
<button class="btn btn-default dropdown-toggle" type="button" dropdown-toggle>
<span class="pull-left" ng-if="currentUser.name">{{ currentUser.name }}</span>
<span class="pull-left" ng-if="!currentUser.name" translate>Add New User</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="user in users" ng-class="{ checked: currentUser.name == user.name }">
<a tabindex="0" ng-click="selectUser(user)">{{user.name}}</a>
</li>
<li role="separator" class="divider"></li>
<li ng-class="{ checked: !currentUser.name }">
<a tabindex="0" ng-click="selectUser()" translate>Add New User</a>
</li>
</ul>
</div>
</td>
</tr>
<tr ng-if="useAuth && hasCert(currentUser)">
<td class="top">
<label class="control-label" translate>Authentication</label>
</td>
<td>
<span translate>Client Certificate</span>
</td>
</tr>
<tr ng-if="useAuth && (!currentUser || currentUser.user.username)">
<td class="top">
<label class="control-label" for="kubernetes-username"
translate>Username</label>
</td>
<td>
<input id="kubernetes-username" class="form-control" type="text"
ng-model="fields.username">
</td>
</tr>
<tr ng-if="useAuth && (!currentUser || currentUser.user.username)">
<td class="top">
<label class="control-label" for="kubernetes-password"
translate>Password</label>
</td>
<td>
<input id="kubernetes-password" class="form-control" type="password"
ng-model="fields.password">
</td>
</tr>
<tr ng-if="useAuth && currentUser.user.token">
<td class="top">
<label class="control-label" for="kubernetes-token"
translate>Token</label>
</td>
<td>
<input id="kubernetes-token" class="form-control" type="text"
ng-model="fields.token">
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" ng-click="$dismiss()" translate>Cancel</button>
<button ng-if="error.problem != 'untrusted-host'" class="btn btn-primary" ng-click="complete(update())">
<span translate>Connect</span>
</button>
</div>

View File

@ -1,35 +0,0 @@
<div class="modal-body">
<p translate>The server uses a certificate signed by an unknown authority.</p>
<p translate>You can bypass the certificate check, but any data you send to the server could be intercepted by others.</p>
<table class="form-table-ct">
<tr>
<td></td>
<td>
<label>
<input type="radio" ng-model="action" value="skip">
<span translate>Skip Certificate Verification</span>
</label>
</td>
</tr>
<tr ng-class="{hidden: !details}">
<td></td>
<td>
<label >
<input type="radio" ng-model="action" value="pem">
<span translate>Trust this certificate for this connection</span>
</label>
<pre class="dialog">{{ details }}</pre>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" ng-click="$dismiss()" translate>Cancel</button>
<button ng-if="error.problem != 'untrusted-host'" class="btn btn-primary" ng-click="complete(update())">
<span translate>Connect</span>
</button>
</div>

View File

@ -1,33 +0,0 @@
<div class="row">
<div class="col-sm-6">
<dl>
<dt ng-if="item !== container" translate>Container</dt>
<dd ng-if="item !== container">{{container.spec.name}}</dd>
<dt translate>Image</dt>
<dd>{{container.spec.image}}</dd>
<dt ng-if="container.status.imageID" translate>Image ID</dt>
<dd ng-if="container.status.imageID"><tt>{{container.status.imageID | kubeIdentifier }}</tt></dd>
<dt ng-if="container.status.containerID" translate>Container ID</dt>
<dd ng-if="container.status.containerID" revealable-text><tt>{{container.status.containerID | kubeIdentifier }}</tt></dd>
<dt translate>Ports</dt>
<dd>
<span ng-repeat="port in container.spec.ports">{{port.containerPort}}/{{port.protocol}}</span>
</dd>
<dt ng-repeat-start="(state, args) in container.status.state" translate>State</dt>
<dd>{{state}}</dd>
<dt translate>Since</dt>
<dd ng-repeat-end>{{args.startedAt}}</dd>
<dt ng-if="container.status" translate>Restart Count</dt>
<dd ng-if="container.status">{{container.status.restartCount}}</dd>
</dl>
</div>
<div class="col-sm-6">
<dl class="full-width">
<dt ng-if="container.spec.env.length" translate>Environment</dt>
<dd revealable-text
ng-repeat="env in container.spec.env"><tt>{{env.name}}=<span
ng-class="{masked: should_mask(env.name)}">{{env.value}}</span></tt>
</dd>
</dl>
</div>
</div>

View File

@ -1,23 +0,0 @@
<div class="listing-ct-inline" ng-if="container">
<h3 translate>Container</h3>
<div class="listing-ct-body container-fluid">
<kube-container-body>
</kube-container-body>
</div>
<h3 translate>Logs</h3>
<div class="container-fluid">
<kube-console pod="pod" container="container.spec.name">
</kube-console>
</div>
<h3 translate>Shell</h3>
<div class="container-fluid">
<kubernetes-container-terminal pod="pod" container="container.spec.name" ng-if="pod.status.phase == 'Running' && container.status.state.running">
</kubernetes-container-terminal>
</div>
</div>
<div class="listing-ct-inline" ng-if="!container">
<h3 translate>The container '{{ target }}' does not exist.</h3>
</div>

View File

@ -1,7 +0,0 @@
<div class="content-filter">
<h3 ng-if="container"><i class="fa fa-cube fa-fw"></i>{{ container.spec.name }}</h3>
<h3 ng-if="!container"><i class="fa fa-cube fa-fw"></i>{{ target }}</h3>
<a tabindex="0" ng-click="back()" translate class="hidden-xs">Show all Containers</a>
</div>
<div class="listing-ct-inline" container-page-inline></div>

View File

@ -1,23 +0,0 @@
<div class="listing-ct-head">
<ul class="nav nav-tabs nav-tabs-pf">
<li ng-class="{active: tab('main')}">
<a tabindex="0" ng-click="tab('main', $event)" translate>Container</a></li>
<li ng-class="{active: tab('logs')}">
<a tabindex="0" class="logs" ng-click="tab('logs', $event); tabbed = true" translate>Logs</a></li>
<li ng-class="{active: tab('shell')}"
ng-if="item.status.phase == 'Running' && container.status.state.running">
<a tabindex="0" class="shell" ng-click="tab('shell', $event); tabbed = true" translate>Shell</a></li>
</ul>
</div>
<div class="listing-ct-body" ng-show="tab('main')">
<kube-container-body></kube-container-body>
</div>
<div class="listing-ct-body" ng-show="tab('logs')">
<kube-console pod="item" container="container.spec.name" prevent="!tabbed">
</kube-console>
</div>
<div class="listing-ct-body" ng-show="tab('shell')"
ng-if="item.status.phase == 'Running' && container.status.state.running">
<kubernetes-container-terminal pod="item" container="container.spec.name" prevent="!tabbed">
</kubernetes-container-terminal>
</div>

View File

@ -1,43 +0,0 @@
<table listing-table class="listing-ct containers-listing">
<thead>
<tr ng-if="!listing.inline">
<td colspan="6">
<h3 translate>Containers</h3>
</td>
</tr>
<tr>
<tr>
<th></th>
<th translate>Name</th>
<th translate>Pod</th>
<th translate ng-if="settings.flavor == 'openshift'">Project</th>
<th translate ng-if="settings.flavor != 'openshift'">Namespace</th>
<th translate>Node</th>
<th translate>Status</th>
</tr>
</tr>
</thead>
<tbody data-ng-rubbish="" ng-if="0"
ng-repeat-start="item in pods | orderObjectBy:['metadata.namespace', 'metadata.name'] track by item.metadata.uid">
</tbody>
<tbody ng-repeat="container in containers(item) track by container.key"
ng-class="{open: listing.expanded(container.key)}">
<tr ng-click="listing.activate(container.key)" class="listing-ct-item">
<td ng-click="listing.toggle(container.key, $event)"
class="listing-ct-toggle">
<i class="fa fa-fw"></i>
</td>
<th>{{container.spec.name}}</th>
<td>{{item.metadata.name}}</td>
<td>{{item.metadata.namespace}}</td>
<td>{{item.spec.nodeName}}</td>
<td><span ng-repeat="(name, value) in container.status.state">{{name}}</span></td>
</tr>
<tr class="listing-ct-panel" ng-if="listing.expanded(container.key)">
<td listing-panel kind="Container" colspan="6" ng-init="id = container.key"></td>
</tr>
</tbody>
<tbody data-ng-rubbish="" ng-if="0" ng-repeat-end>
</tbody>
</table>

View File

@ -1,4 +0,0 @@
<filter-bar class="content-filter">
</filter-bar>
<div containers-listing></div>

View File

@ -1,215 +0,0 @@
<filter-bar class="content-filter">
<div class="content-filter-link pull-right" ng-if="settings.canChangeConnection">
<a tabindex="0" id="kubernetes-change-connection" ng-click="changeAuth()" title="Connection Settings">
<i class="fa fa-wrench"></i>
</a>
</div>
</filter-bar>
<div class="container-cards-pf dashboard-cards" ng-if="settings.flavor != 'openshift'">
<div class="card-pf card-pf-aggregate-status card-pf-accented dashboard-status">
<kubernetes-service-graph id="graphs">
</kubernetes-service-graph>
</div>
</div>
<div class="container-cards-pf dashboard-cards">
<div class="card-pf card-pf-aggregate-status card-pf-accented dashboard-status">
<h2 class="card-pf-title">{{pods.length}} <span translate>Pods</span></h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification" ng-if="status.pods.Pending.length"
title="Pending pods">
<span class="spinner spinner-sm"></span>{{status.pods.Pending.length}}
</span>
<span class="card-pf-aggregate-status-notification" ng-if="status.pods.Failed.length"
title="Failed pods">
<span class="pficon pficon-error-circle-o"></span>{{status.pods.Failed.length}}
</span>
<span class="card-pf-aggregate-status-notification" ng-if="status.pods.Unknown.length"
title="Pods with unknown status">
<span class="pficon pficon-warning-triangle-o"></span>{{status.pods.Unknown.length}}
</span>
<span class="card-pf-aggregate-status-notification"
ng-if="!status.pods.Pending.length && !status.pods.Failed.length && !status.pods.Unknown.length">
<span ng-if="pods.length > 0">
<span class="pficon pficon-ok"></span>
<span class="card-pf-aggregate-status-text" translate>All running</span>
</span>
<span ng-if="pods.length == 0">
<span class="card-pf-aggregate-status-text" translate>No pods deployed</span>
</span>
</span>
</p>
</div>
</div>
<div class="card-pf card-pf-aggregate-status card-pf-accented dashboard-status">
<h2 class="card-pf-title">{{volumes.length}} <span translate>Volumes</span></h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification" ng-if="status.volumes.Pending.length"
title="Pending volumes">
<span class="spinner spinner-sm"></span>{{status.volumes.Pending.length}}
</span>
<span class="card-pf-aggregate-status-notification" ng-if="status.volumes.Released.length"
title="Released volumes awaiting cleanup">
<span class="pficon pficon-warning-triangle-o"></span>{{status.volumes.Released.length}}
</span>
<span class="card-pf-aggregate-status-notification" ng-if="status.volumes.Failed.length"
title="Volumes that have failed">
<span class="pficon pficon-error-circle-o"></span>{{status.volumes.Failed.length}}
</span>
<span class="card-pf-aggregate-status-notification" ng-if="status.volumes.Available.length"
title="Available volumes not yet in use">
<span class="pficon pficon-ok"></span>{{status.volumes.Available.length}}
</span>
<span class="card-pf-aggregate-status-notification"
ng-if="!status.volumes.Pending.length && !status.volumes.Released.length && !status.volumes.Failed.length && !status.volumes.Available.length">
<span ng-if="volumes.length > 0">
<span class="pficon pficon-ok"></span><span class="card-pf-aggregate-status-text"
translate>All in use</span>
</span>
<span ng-if="volumes.length == 0">
<span class="card-pf-aggregate-status-text"
translate>No volumes in use</span>
</span>
</span>
</p>
<p class="pvc-notice" ng-if="status.volumes.PendingClaims.length">
<span class="card-pf-aggregate-status-notification">
<span class="pficon pficon-info"></span>
<a tabindex="0" href="#{{ viewUrl('volumes', true) }}">
<span class="card-pf-aggregate-status-text">{{status.volumes.PendingClaims.length}}</span>
<span class="card-pf-aggregate-status-text" translate>pending volume claims</span>
</a>
</span>
</p>
</div>
</div>
<div class="card-pf card-pf-aggregate-status card-pf-accented dashboard-status">
<h2 class="card-pf-title">{{nodes.length}} <span translate>Nodes</span></h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification" ng-if="status.nodes.Pending.length"
title="Pending nodes">
<span class="spinner spinner-sm"></span>{{status.nodes.Pending.length}}
</span>
<span class="card-pf-aggregate-status-notification" ng-if="status.nodes.Terminated.length"
title="Terminated nodes">
<span class="pficon pficon-warning-triangle-o"></span>{{status.nodes.Terminated.length}}
</span>
<span class="card-pf-aggregate-status-notification" ng-if="status.nodes.NotReady.length"
title="Nodes that are not ready">
<span class="pficon pficon-error-circle-o"></span>{{status.nodes.NotReady.length}}
</span>
<span class="card-pf-aggregate-status-notification" ng-if="status.nodes.OutOfDisk.length"
title="Out of disk space">
<span class="pficon pficon-volume"></span>{{status.nodes.OutOfDisk.length}}
</span>
<span class="card-pf-aggregate-status-notification"
ng-if="!status.nodes.Pending.length && !status.nodes.Terminated.length && !status.nodes.NotReady.length && !status.nodes.OutofDisk.length">
<span ng-if="nodes.length > 0">
<span class="pficon pficon-ok"></span><span class="card-pf-aggregate-status-text"
translate>All healthy</span>
</span>
<span ng-if="nodes.length == 0">
<span class="pficon pficon-warning-triangle-o"></span>
<span class="card-pf-aggregate-status-text" translate>No nodes in cluster</span>
</span>
</span>
</p>
</div>
</div>
</div>
<div class="container-cards-pf dashboard-cards">
<div class="card-pf card-pf-double" id="service-list">
<div class="card-pf-heading">
<div class="pull-right">
<button class="btn btn-default fa fa-check" id="services-enable-change" title="Change"
ng-click="toggleServiceChange()" ng-class="{ active: editServices }"></button>
<button class="btn btn-primary" id="deploy-app" ng-click="deploy(namespaces(), namespace)"
translate>Deploy</button>
</div>
<h2 class="card-pf-title" translate>Services</h2>
</div>
<div class="card-pf-body">
<div class="well blank-slate-pf spacious" ng-if="servicesState() == 'failed'">
<div class="blank-slate-pf-icon">
<i ng-if="failure.status == 403" class="fa fa-lock"></i>
<i ng-if="failure.status != 403" class="fa fa-exclamation-circle"></i>
</div>
<h3 translate>Could not list services</h3>
<p>{{failure.message}}</p>
</div>
<div class="well blank-slate-pf spacious" ng-if="servicesState() == 'empty'">
<div class="blank-slate-pf-icon">
<i class="fa fa-rocket"></i>
</div>
<h3>No services present</h3>
<p translate>You can deploy an application to your cluster.</p>
</div>
<table class="listing-ct dashboard-list" ng-if="servicesState() == 'ready'"
ng-class="{editable: editServices}">
<thead>
<tr>
<th translate>Name</th>
<th translate>Address</th>
<th translate>Containers</th>
<th translate ng-if="settings.flavor == 'openshift'">Project</th>
<th translate ng-if="settings.flavor != 'openshift'">Namespace</th>
<th class="status"></th>
</tr>
</thead>
<tbody>
<tr class="listing-ct-item"
ng-repeat="service in services track by service.metadata.uid"
data-name="{{service.metadata.name}}"
ng-class="{'highlight-ct': service.metadata.uid == highlighted}"
ng-click="jumpService($event, service)"
ng-mouseover="highlight(service.metadata.uid)" ng-mouseout="highlight(null)"
ng-if="!(service.metadata.namespace == 'default' && service.metadata.name == 'kubernetes')">
<td>{{service.metadata.name}}</td>
<td><kubernetes-address ng-init="item = service"></td>
<td class="containers">{{serviceContainers(service)}}</td>
<td>{{service.metadata.namespace}}</td>
<td class="status">
<button title="Adjust" class="btn btn-default adjust-service pficon pficon-edit"
data-id="{{id}}" ng-click="modifyService(service)"></button>
<span kubernetes-status-icon status="serviceStatus(service)"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-pf" id="node-list">
<div class="card-pf-heading">
<div class="pull-right">
<button title="Add Kubernetes Node" id="add-node" class="btn btn-primary fa fa-plus"
ng-click="addNode()"></button>
</div>
<h2 class="card-pf-title" translate>Nodes</h2>
</div>
<div class="card-pf-body">
<table class="listing-ct dashboard-list">
<thead>
<tr>
<th translate>Name</th>
<th translate>Containers</th>
<th class="status"></th>
</tr>
</thead>
<tbody>
<tr ng-click="navigateNode(node)" ng-repeat="node in nodes" class="listing-ct-item">
<td>{{node.metadata.name}}</td>
<td class="containers">{{nodeContainers(node)}}</td>
<td class="status">
<span kubernetes-status-icon status="nodeStatusIcon(node)"></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -1,2 +0,0 @@
<div class="listing-ct-body">
</div>

View File

@ -1,64 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h3 class="modal-title" translate>Deploy Application</h3>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td class="top">
<label class="control-label" for="deploy-app-type-label" translate>Type</label>
</td>
<td id="deploy-app-type-field" class="dialog-wrapper">
<div class="btn-group bootstrap-select deploy-type form-control" dropdown>
<button class="btn btn-default dropdown-toggle" type="button" dropdown-toggle>
<span class="pull-left">{{ selected.name }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="type in types" ng-class="{ checked: selected.name == type.name }">
<a tabindex="0" ng-click="select(type)">{{type.name}}</a>
</li>
</ul>
</div>
</td>
</tr>
<tr>
<td class="top">
<label class="control-label" for="deploy-app-file">{{ selected.name }}</label>
</td>
<td class="dialog-wrapper">
<div ng-if="selected.type == 'manifest'" file-button>
</div>
<input id="deploy-app-nulecule-image" class="form-control" type="text" placeholder="eg: docker/image"
ng-if="selected.type == 'nulecule'">
</td>
</tr>
<tr>
<td class="top">
<label ng-if="flavor == 'openshift'" class="control-label top" for="deploy-app-namespace" id="deploy-app-namespace-label">Project</label>
<label ng-if="flavor != 'openshift'" class="control-label top" for="deploy-app-namespace" id="deploy-app-namespace-label">Namespace</label>
</td>
<td>
<div class="input-group bootstrap-select input-combo form-control" id="deploy-app-namespace-group" dropdown>
<input id="deploy-app-namespace" class="form-control" type="text" placeholder="eg: myapplication" ng-model="fields.namespace">
<span class="input-group-btn" dropdown="">
<button class="btn btn-default dropdown-toggle" type="button" dropdown-toggle>
<span class="caret"></span>
</button>
<ul span ng-if="!namespace" class="dropdown-menu dropdown-menu-left">
<li ng-repeat="name in namespaces track by name" ng-class="{ checked: fields.namespace == name }">
<a tabindex="0" ng-click="fields.namespace = name">{{name}}</a>
</li>
</ul>
</span>
</div>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-primary" translate ng-click="complete(performDeploy())">Deploy</button>
</div>
</modal-dialog>

View File

@ -1,46 +0,0 @@
<div class="row">
<div class="col-sm-6">
<dl>
<dt ng-if="settings.flavor == 'openshift'" translate>Project</dt>
<dt ng-if="settings.flavor != 'openshift'" translate>Namespace</dt>
<dd>{{ item.metadata.namespace }}</dd>
<dt translate>Created</dt>
<dd title="{{ item.metadata.creationTimestamp }}">{{ item.metadata.creationTimestamp | dateRelative }}</dd>
<dt translate>Replicas</dt>
<dd>{{ item.spec.replicas }}</dd>
<dt ng-if="item.spec.strategy.type" translate>Strategy</dt>
<dd ng-if="item.spec.strategy.type">{{item.spec.strategy.type}}</dd>
<dt translate>Latest Version</dt>
<dd ng-if="item.status.latestVersion">{{ item.status.latestVersion }}</dd>
<dd ng-if="!item.status.latestVersion" translate>Not deployed</dd>
</dl>
</div>
<div class="col-sm-6">
<dl class="full-width">
<dt translate ng-if="item.status.details">Deployment Causes</dt>
<dd ng-if="item.status.details.message">{{ item.status.details.message }}</dd>
<dd ng-repeat="cause in item.status.details.causes">
{{ cause.type }}
<span ng-if="cause.imageTrigger.from">
- {{ cause.imageTrigger.from.name }}
<span>
</dd>
</dl>
<dl class="full-width">
<dt translate ng-if="item.spec.triggers">Triggers</dt>
<dd class="full" ng-repeat="trigger in item.spec.triggers">
{{ trigger.type }}
<span ng-if="trigger.imageChangeParams.from">
- {{ trigger.imageChangeParams.from.name }}
</span>
</dd>
</dl>
<dl class="full-width">
<dt translate>Labels</dt>
<dd ng-if="!item.metadata.labels">none</span>
<dd ng-repeat="(key, value) in item.metadata.labels">{{key}}={{value}}</dd>
</dl>
</div>
</div>

View File

@ -1,26 +0,0 @@
<div class="content-filter">
<div class="listing-ct-actions" ng-if="item">
<button title="Delete" ng-click="deleteItem(item)"
class="deployment-delete btn btn-danger btn-delete pficon pficon-delete delete-entity"></button>
</div>
<h3 ng-if="item"><i class="fa fa-gear"></i>{{ item.metadata.name }}</h3>
<h3 ng-if="!item"><i class="fa fa-gear"></i>{{ target }}</h3>
<a tabindex="0" ng-click="navigate(type)" translate class="hidden-xs">Show all Deployment Configs</a>
</div>
<div class="listing-ct-inline" ng-if="item">
<h3 translate>Deployment Config</h3>
<div class="listing-ct-body container-fluid" deploymentconfig-body></div>
<h3 translate>Template</h3>
<div class="listing-ct-body container-fluid">
<kube-pod-body ng-init="pod = item.spec.template">
</kube-pod-body>
<kube-container-body ng-repeat="container in containers(item.spec.template)">
</kube-container-body>
</div>
</div>
<div class="listing-ct-inline" ng-if="!item">
<h3 translate>The deployment config '{{ target }}' does not exist.</h3>
</div>

View File

@ -1,19 +0,0 @@
<div class="listing-ct-head">
<div class="listing-ct-actions">
<button title="Delete" class="deployment-delete btn btn-danger btn-delete pficon pficon-delete delete-entity "
ng-click="deleteItem(item)"></button>
</div>
<ul class="nav nav-tabs nav-tabs-pf">
<li ng-class="{active: tab('main')}">
<a tabindex="0" ng-click="tab('main', $event)" translate>Deployment Config</a></li>
<li ng-class="{active: tab('template')}" ng-if="item.spec.template">
<a tabindex="0" ng-click="tab('template', $event)" translate>Template</a></li>
</ul>
</div>
<div class="listing-ct-body" deploymentconfig-body ng-show="tab('main')"></div>
<div class="listing-ct-body" ng-show="tab('template')" ng-if="item.spec.template">
<kube-pod-body ng-init="pod = item.spec.template">
</kube-pod-body>
<kube-container-body ng-repeat="container in containers(item.spec.template)">
</kube-container-body>
</div>

View File

@ -1,196 +0,0 @@
<div class="content-filter">
<filter-project></filter-project>
<div class="btn-group bootstrap-select type-filter" dropdown>
<button class="btn btn-default dropdown-toggle" type="button" dropdown-toggle>
<span class="pull-left">
<span ng-if="showAll" translate>All Types</span>
<span ng-if="name"><span class="namespace-meta" translate>Type:</span> {{ name }}</span>
</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-class="{ checked: !type }">
<a tabindex="0" ng-click="navigate()" value="">All Types</a>
</li>
<li ng-repeat="(k, v) in names"
ng-class="{ checked: type == k }"
ng-if="!v.flavor || v.flavor == settings.flavor">
<a tabindex="0" ng-click="navigate(k)">{{ v.name }}</a>
</li>
</ul>
</div>
</div>
<div>
<table listing-table class="listing-ct details-listing">
<thead id="services" ng-if="showAll || services">
<tr>
<td colspan="5">
<h3 translate>Services</h3>
</td>
</tr>
<tr>
<th></th>
<th translate>Name</th>
<th translate ng-if="settings.flavor == 'openshift'">Project</th>
<th translate ng-if="settings.flavor != 'openshift'">Namespace</th>
<th translate>Addresses</th>
<th translate>State</th>
</tr>
</thead>
<tbody ng-repeat="item in services | orderObjectBy:['metadata.namespace', 'metadata.name'] as filtered_result track by itemIdentifier(item)" ng-init="id = itemIdentifier(item)"
data-id="{{id}}" ng-class="{open: listing.expanded(id)}">
<tr ng-click="listing.activate(id)" class="listing-ct-item">
<td ng-click="listing.toggle(id, $event)"
class="listing-ct-toggle">
<i class="fa fa-fw"></i>
</td>
<th>{{item.metadata.name}}</th>
<td>{{item.metadata.namespace}}</td>
<td><kubernetes-service-cluster></td>
<td>
<span ng-if="serviceEndpoint(item).subsets.length !== 0" translate>Ready</span>
<span ng-if="serviceEndpoint(item).subsets.length === 0" translate>Not Ready</span>
</td>
</tr>
<tr class="listing-ct-panel" ng-if="listing.expanded(id)">
<td listing-panel kind="Service" colspan="5">
</td>
</tr>
</tbody>
<thead id="routes" ng-if="(showAll || routes) && settings.flavor == 'openshift'">
<tr>
<td colspan="5">
<h3 translate>Routes</h3>
</td>
</tr>
<tr>
<th></th>
<th translate>Name</th>
<th translate ng-if="settings.flavor == 'openshift'">Project</th>
<th translate ng-if="settings.flavor != 'openshift'">Namespace</th>
<th translate>Host</th>
<th translate></th>
</tr>
</thead>
<tbody ng-repeat="item in routes | orderObjectBy:['metadata.namespace', 'metadata.name'] as filtered_result track by itemIdentifier(item)"
ng-init="id = itemIdentifier(item)" data-id="{{id}}"
ng-class="{open: listing.expanded(id)}">
<tr ng-click="listing.activate(id)" class="listing-ct-item">
<td ng-click="listing.toggle(id, $event)"
class="listing-ct-toggle">
<i class="fa fa-fw"></i>
</td>
<th>{{item.metadata.name}}</th>
<td>{{item.metadata.namespace}}</td>
<td>{{item.spec.host}}</td>
<td></td>
</tr>
<tr class="listing-ct-panel" ng-if="listing.expanded(id)">
<td listing-panel kind="Route" colspan="5"></td>
</tr>
</tbody>
<thead id="deployment-configs" ng-if="(showAll || deploymentconfigs) && settings.flavor == 'openshift'">
<tr>
<td colspan="5">
<h3 translate>Deployment Configs</h3>
</td>
</tr>
<tr>
<th></th>
<th translate>Name</th>
<th translate ng-if="settings.flavor == 'openshift'">Project</th>
<th translate ng-if="settings.flavor != 'openshift'">Namespace</th>
<th translate></th>
<th translate>Latest Version</th>
</tr>
</thead>
<tbody ng-repeat="item in deploymentconfigs | orderObjectBy:['metadata.namespace', 'metadata.name'] as filtered_result track by itemIdentifier(item)"
ng-init="id = itemIdentifier(item)" data-id="{{id}}"
ng-class="{open: listing.expanded(id)}">
<tr ng-click="listing.activate(id)" class="listing-ct-item">
<td ng-click="listing.toggle(id, $event)"
class="listing-ct-toggle">
<i class="fa fa-fw"></i>
</td>
<th>{{item.metadata.name}}</th>
<td>{{item.metadata.namespace}}</td>
<td></td>
<td ng-if="item.status.latestVersion">{{ item.status.latestVersion }}</td>
<td ng-if="!item.status.latestVersion" translate>Not deployed</td>
</tr>
<tr class="listing-ct-panel" ng-if="listing.expanded(id)">
<td listing-panel kind="DeploymentConfig" colspan="5"></td>
</tr>
</tbody>
<thead id="replication-controllers" ng-if="showAll || replicationcontrollers">
<tr>
<td colspan="5">
<h3 translate>Replication Controllers</h3>
</td>
</tr>
<tr>
<th></th>
<th translate>Name</th>
<th translate ng-if="settings.flavor == 'openshift'">Project</th>
<th translate ng-if="settings.flavor != 'openshift'">Namespace</th>
<th translate></th>
<th translate>Replicas</th>
</tr>
</thead>
<tbody ng-repeat="item in replicationcontrollers | orderObjectBy:['metadata.namespace', 'metadata.name'] as filtered_result track by itemIdentifier(item)"
ng-init="id = itemIdentifier(item)" data-id="{{id}}"
ng-class="{open: listing.expanded(id)}">
<tr ng-click="listing.activate(id)" class="listing-ct-item">
<td ng-click="listing.toggle(id, $event)"
class="listing-ct-toggle">
<i class="fa fa-fw"></i>
</td>
<th>{{item.metadata.name}}</th>
<td>{{item.metadata.namespace}}</td>
<td></td>
<td ng-if="item.spec.replicas == item.status.replicas">{{ item.spec.replicas }}</td>
<td ng-if="item.spec.replicas != item.status.replicas">{{ item.status.replicas }} of {{ item.spec.replicas }}</td>
</tr>
<tr class="listing-ct-panel" ng-if="listing.expanded(id)">
<td listing-panel kind="ReplicationController" colspan="5"></td>
</tr>
</tbody>
<thead id="pods" ng-if="showAll || pods">
<tr>
<td colspan="5">
<h3 translate>Pods</h3>
</td>
</tr>
<tr>
<th></th>
<th translate>Name</th>
<th translate ng-if="settings.flavor == 'openshift'">Project</th>
<th translate ng-if="settings.flavor != 'openshift'">Namespace</th>
<th translate>Address</th>
<th translate>State</th>
</tr>
</thead>
<tbody ng-repeat="item in pods | orderObjectBy:['metadata.namespace', 'metadata.name'] as filtered_result track by itemIdentifier(item)"
ng-init="id = itemIdentifier(item)" data-id="{{id}}"
ng-class="{open: listing.expanded(id)}">
<tr ng-click="listing.activate(id)" class="listing-ct-item">
<td ng-click="listing.toggle(id, $event)"
class="listing-ct-toggle">
<i class="fa fa-fw"></i>
</td>
<th>{{item.metadata.name}}</th>
<td>{{item.metadata.namespace}}</td>
<td>{{item.status.podIP}}</td>
<td>{{ podStatus(item) }}</td>
</tr>
<tr class="listing-ct-panel" ng-if="listing.expanded(id)">
<td listing-panel kind="Pod" colspan="5"></td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,6 +0,0 @@
<input id="deploy-app-manifest-file" type="file">
<button id="deploy-app-manifest-file-button" class="btn btn-default form-control" type="button"
ng-class="{manifest_file_default: !fields.filename, manifest_file: fields.filename}">
<span ng-if="!fields.filename" translate>Select Manifest File...</span>
<span ng-if="fields.filename">{{ fields.filename }}</span>
</button>

View File

@ -1,5 +0,0 @@
<ng-transclude>
</ng-transclude>
<filter-project>
</filter-project>

View File

@ -1,19 +0,0 @@
<div class="btn-group bootstrap-select namespace-filter" dropdown>
<button class="btn btn-default dropdown-toggle" type="button" dropdown-toggle>
<span class="pull-left">
<span ng-if="!filter.namespace()" translate>All Projects</span>
<span ng-if="filter.namespace()"><span class="namespace-meta" translate>Project:</span>
{{filter.namespace()}}</span>
</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-class="{ checked: !filter.namespace() }">
<a tabindex="0" ng-click="filter.namespace(null)" value="">All Projects</a>
</li>
<li ng-repeat="name in filter.namespaces() track by name"
ng-class="{ checked: name == filter.namespace() }">
<a tabindex="0" ng-click="filter.namespace(name)" value="{{name}}">{{name}}</a>
</li>
</ul>
</div>

View File

@ -1,23 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h4 class="modal-title" translate>Remove Group</h4>
</div>
<div class="modal-body">
<div ng-if="fields.members.length != 0">
<p>The '{{ fields.group.metadata.name }}' group will be deleted from projects:</p>
<div class="user-membership-body">
<div ng-repeat="member in getProjectsWithMember(fields.projects, fields.group.metadata.name) track by member.metadata.name">
<h4 translate ng-if="$first">Projects</h4>
<span>{{ member.metadata.name }}</span>
</div>
</div>
</div>
<div ng-if="fields.members.length == 0">
<p>The group '{{ fields.group.metadata.name }}' will be removed.</p>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-primary" ng-click="complete(performDelete(fields.group))" translate>Remove</button>
</div>
</modal-dialog>

View File

@ -1,48 +0,0 @@
<div class="content-filter">
<div class="listing-ct-actions" ng-if="group()">
<button class="btn btn-danger btn-delete pficon pficon-delete"
ng-click="removeGroup(projects(), group())"></button>
</div>
<h3 ng-if="group()">
<i class="pficon pficon-users"></i>
<span>{{ group().metadata.name }}</span>
</h3>
<h3 ng-if="!group()">
<i class="pficon pficon-users"></i>
<span>{{ groupName }}</span>
</h3>
<a tabindex="0" ng-href="#/projects" translate>Show all Projects</a>
</div>
<div class="project-panel" ng-if="group()">
<div>
<table listing-table class="listing-ct project-body" >
<thead>
<tr>
<th translate>Group Members</th>
<th translate></th>
</tr>
</thead>
<tbody colspan="3" ng-repeat="user in group().users" data-id="{{ user }}">
<tr class="listing-ct-item inner-project-listing">
<td ng-click="listing.activate('/users/' + user)"><i class=" pficon-user"></i> {{ user }}</span></td>
<td><a tabindex="0" class="pull-right close-icon" ng-click="removeUserFromGroup(user, group())">
<i translate class="pficon-close"></i>
</a></td>
</tr>
</tbody>
<thead>
<tr>
<td colspan="3">
<a tabindex="0" class="pull-left" ng-click="addUserToGroup(group())">
<i translate class="pficon pficon-add-circle-o"></i>
<span translate>Add Member</span>
</a>
</td>
</tr>
</thead>
</table>
</div>
</div>
<div class="listing-ct-inline" ng-if="!group()">
<h3 translate>The group '{{ groupName }}' does not exist.</h3>
</div>

View File

@ -1,36 +0,0 @@
<div class="listing-ct-head" ng-click="listing.collapse(id, $event)">
<div class="listing-ct-actions">
<button class="btn btn-danger btn-delete pficon pficon-delete" ng-click="removeGroup(projects(), group())"></button>
</div>
<h3><i class="pficon pficon-users"></i>{{ group().metadata.name }}</h3>
</div>
<div class="listing-ct-body project-panel" ng-show="tab('main')">
<div>
<table listing-table class="listing-ct project-body">
<thead >
<tr >
<th translate>Group Members</th>
<th translate></th>
</tr>
</thead>
<tbody colspan="3" ng-repeat="user in group().users">
<tr class="listing-ct-item inner-project-listing">
<td><i class=" pficon-user"></i> {{ user }}</span></td>
<td><a tabindex="0" class="pull-right close-icon">
<i translate class="pficon-close " ng-click="removeUserFromGroup(user, group())"></i>
</a></td>
</tr>
</tbody>
<thead>
<tr>
<td colspan="3">
<a tabindex="0" class="pull-left" disabled>
<i translate class="pficon pficon-add-circle-o"></i>
<span translate ng-click="addUserToGroup(group())">Add Member</span>
</a>
</td>
</tr>
</thead>
</table>
</div>
</div>

View File

@ -1,10 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h3 class="modal-title" translate>Remove image tag</h3>
</div>
<div class="modal-body" translate>Do you want to remove the image tagged as '{{stream.metadata.namespace}}/{{stream.metadata.name}}:{{tag.tag}}'?</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-danger" translate ng-click="complete(performDelete())">Delete</button>
</div>
</modal-dialog>

View File

@ -1,36 +0,0 @@
<div class="content-filter">
<div class="listing-ct-actions">
<button class="btn btn-danger btn-delete pficon pficon-delete"
ng-click="deleteTag(stream, tag)"></button>
</div>
<h3>
<i class="pficon pficon-image"></i>
<span>{{ stream.metadata.namespace }}/{{ stream.metadata.name}}:{{ tag.tag }}</span>
</h3>
<a ng-href="#/images/{{ stream.metadata.namespace }}/{{ stream.metadata.name }}"
translate class="hidden-xs">Show all images</a>
</div>
<div class="listing-ct-inline">
<h3 translate>Image</h3>
<div class="listing-ct-body">
<registry-image-body image="image" names="names">
</registry-image-body>
<registry-image-pull settings="settings" names="names">
</registry-image-pull>
</div>
<h3 translate>Container</h3>
<div class="listing-ct-body">
<registry-image-config image="image">
</registry-image-config>
</div>
<h3 translate>Metadata</h3>
<div class="listing-ct-body">
<registry-image-meta image="image">
</registry-image-meta>
<registry-image-layers image="image" layers="layers">
</registry-image-layers>
</div>
</div>

View File

@ -1,23 +0,0 @@
<filter-bar class="content-filter">
</filter-bar>
<div>
<table listing-table class="listing-ct images-listing">
<thead>
<tr ng-if="!listing.inline">
<td colspan="4">
<a tabindex="0" class="pull-right" ng-click="createImageStream()">
<i translate class="pficon pficon-add-circle-o"></i>
<span translate>New image stream</span>
</a>
<h3 translate>Images</h3>
</td>
</thead>
</table>
<registry-imagestream-listing imagestreams="imagestreams" tags-for-image="tagsForImage"
settings="settings" shared-images="sharedImages"
actions="actions" image-tag-names="imageTagNames">
</registry-imagestream-listing>
</div>

View File

@ -1,10 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h3 class="modal-title" translate>Delete image stream</h3>
</div>
<div class="modal-body" translate>Do you want to delete the '{{stream.metadata.namespace}}/{{stream.metadata.name}}' image stream?</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-danger" translate ng-click="complete(performDelete())">Delete</button>
</div>
</modal-dialog>

View File

@ -1,100 +0,0 @@
<modal-dialog>
<div class="modal-header">
<h3 class="modal-title" ng-if="!stream" translate>Create image stream</h3>
<h3 class="modal-title" ng-if="stream" translate>Change image stream</h3>
</div>
<div class="modal-body">
<table class="form-table-ct">
<tr>
<td>
<label class="control-label" for="imagestream-modify-name"
translate>Name</label>
</td>
<td>
<input id="imagestream-modify-name" class="form-control" type="text"
ng-if="!stream" ng-model="fields.name" placeholder="{{ placeholder }}" autofocus>
<span id="imagestream-modify-name" ng-if="stream">{{ fields.name }}</span>
</td>
</tr>
<tr>
<td class="top">
<label class="control-label" for="imagestream-modify-project-text"
translate>Project</label>
</td>
<td>
<div ng-if="!stream" id="imagestream-modify-project" class="input-group input-combo">
<input class="form-control" id="imagestream-modify-project-text" type="text"
ng-model="fields.project">
<span class="input-group-btn" dropdown>
<button class="btn btn-default dropdown-toggle" dropdown-toggle type="button">
<span class="caret"></span>
</button>
<ul dropdown-menu class="dropdown-menu" role="menu">
<li ng-repeat="name in projects() track by name"
ng-class="{ checked: name == fields.project }">
<a tabindex="0" ng-click="fields.project = name" value="{{name}}">{{ name }}</a>
</li>
</ul>
</span>
</div>
<span id="imagestream-modify-project" ng-if="stream">{{ fields.project }}</span>
</td>
</tr>
<tr>
<td class="top">
<label class="control-label" for="imagestream-modify-populate"
translate>Populate</label>
</td>
<td>
<div class="btn-group bootstrap-select form-control" dropdown
id="imagestream-modify-populate">
<button class="btn btn-default dropdown-toggle" dropdown-toggle>
<span class="pull-left">{{ labels.populate[fields.populate] }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="(value, label) in labels.populate"
ng-class="{checked: value == fields.populate}">
<a tabindex="0" ng-click="fields.populate = value" value="{{value}}">{{ label }}</a>
</li>
</ul>
</div>
</td>
</tr>
<tr ng-show="fields.populate != 'none'">
<td class="top">
<label class="control-label" for="imagestream-modify-pull"
translate>Pull from</label>
</td>
<td>
<input id="imagestream-modify-pull" class="form-control" type="text"
ng-model="fields.pull" placeholder="eg: docker.io/library/fedora">
</td>
</tr>
<tr ng-show="fields.populate == 'tags'">
<td class="top">
<label class="control-label" for="imagestream-modify-tags"
translate>Tags</label>
</td>
<td>
<div image-tag-editor id="imagestream-modify-tags" tags="fields.tags">
</div>
</td>
</tr>
<tr ng-show="fields.populate == 'tags'">
<td></td>
<td>
<label>
<input type="checkbox" ng-checked="hasInsecureTag(stream.spec)" ng-model="fields.insecure">
<span translate>Remote registry is insecure</span>
</label>
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button class="btn btn-default btn-cancel" translate>Cancel</button>
<button class="btn btn-primary" ng-if="stream" ng-click="complete(performModify())">Change</button>
<button class="btn btn-primary" ng-if="!stream" ng-click="complete(performCreate())">Create</button>
</div>
</modal-dialog>

Some files were not shown because too many files have changed in this diff Show More