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:
parent
3118819204
commit
819cc0db22
|
@ -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
|
||||
|
|
|
@ -154,7 +154,6 @@ WEBPACK_PACKAGES = \
|
|||
dashboard \
|
||||
docker \
|
||||
kdump \
|
||||
kubernetes \
|
||||
machines \
|
||||
networkmanager \
|
||||
pcp \
|
||||
|
|
|
@ -48,7 +48,6 @@ Vagrant.configure(2) do |config|
|
|||
etcd \
|
||||
firewalld \
|
||||
git \
|
||||
kubernetes \
|
||||
NetworkManager \
|
||||
pcp \
|
||||
qemu \
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"version": "@VERSION@",
|
||||
"requires": {
|
||||
"cockpit": "137.x"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"index": {
|
||||
"label": "Cluster",
|
||||
"order": 20,
|
||||
"icon": "pficon-cluster"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"dashboard": {
|
||||
"registry": {
|
||||
"label": "Image Registry",
|
||||
"order": 30,
|
||||
"icon": "pficon-registry"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
};
|
||||
}]);
|
||||
}());
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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"
|
||||
};
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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;
|
||||
};
|
||||
});
|
||||
}());
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
}());
|
|
@ -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;
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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'
|
||||
};
|
||||
}
|
||||
);
|
||||
}());
|
|
@ -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
|
@ -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
|
@ -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;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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
|
@ -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
|
@ -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";
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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);
|
||||
};
|
||||
}]);
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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
|
@ -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']);
|
||||
}());
|
|
@ -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",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
]);
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]);
|
|
@ -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();
|
|
@ -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();
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
]);
|
|
@ -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;
|
||||
}
|
||||
]);
|
||||
}());
|
|
@ -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
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
@import "../../lib/variables.less";
|
||||
|
||||
@sidebar-width-sm: 40;
|
||||
@sidebar-width-md: 145;
|
||||
@sidebar-width-xl: 185;
|
||||
|
||||
@command-bg-color: #eee;
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,4 +0,0 @@
|
|||
<filter-bar class="content-filter">
|
||||
</filter-bar>
|
||||
|
||||
<div containers-listing></div>
|
|
@ -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>
|
|
@ -1,2 +0,0 @@
|
|||
<div class="listing-ct-body">
|
||||
</div>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,5 +0,0 @@
|
|||
<ng-transclude>
|
||||
</ng-transclude>
|
||||
|
||||
<filter-project>
|
||||
</filter-project>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue