Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85140c7dcf | ||
|
|
30e9a604cd | ||
|
|
8c769148ad | ||
|
|
b857970236 | ||
|
|
23bff41304 | ||
|
|
8243326692 | ||
|
|
52d953a1c2 | ||
|
|
e145d82947 | ||
|
|
25df1fe26c | ||
|
|
8464faa2a1 | ||
|
|
c8a5b82c89 | ||
|
|
00b2c92e39 | ||
|
|
0796778d17 | ||
|
|
1eae1c03f0 | ||
|
|
a9209da167 | ||
|
|
43c2f14289 | ||
|
|
f378d56543 | ||
|
|
3b0d726c2a | ||
|
|
71c091ae0d | ||
|
|
1fb008212a | ||
|
|
e67e20ce18 | ||
|
|
c74e8fc732 | ||
|
|
29358e5744 |
49
README.md
49
README.md
@@ -10,6 +10,12 @@ UI For Docker is a web interface for the Docker Remote API. The goal is to prov
|
||||
* Minimal dependencies - I really want to keep this project a pure html/js app.
|
||||
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
|
||||
|
||||
## Supported Docker versions
|
||||
|
||||
The current Docker version support policy is the following: `N` to `N-2` included where `N` is the latest version.
|
||||
|
||||
At the moment, the following versions are supported: 1.9, 1.10 & 1.11.
|
||||
|
||||
## Run
|
||||
|
||||
### Quickstart
|
||||
@@ -26,11 +32,16 @@ The `--privileged` flag is required for hosts using SELinux.
|
||||
|
||||
By default UI For Docker connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`.
|
||||
|
||||
You can use the `-e` flag to change this socket:
|
||||
You can use the `--host`, `-H` flags to change this socket:
|
||||
|
||||
```
|
||||
# Connect to a tcp socket:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:2375
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
|
||||
```
|
||||
|
||||
```
|
||||
# Connect to another unix socket:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
|
||||
```
|
||||
|
||||
### Swarm support
|
||||
@@ -41,7 +52,7 @@ You can access a specific view for you Swarm cluster by defining the `--swarm` f
|
||||
|
||||
```
|
||||
# Connect to a tcp socket and enable Swarm:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://<SWARM_HOST>:<SWARM_PORT> --swarm
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
|
||||
```
|
||||
|
||||
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment.
|
||||
@@ -54,6 +65,24 @@ UI For Docker listens on port 9000 by default. If you run UI For Docker inside a
|
||||
$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui
|
||||
```
|
||||
|
||||
### Access a Docker engine protected via TLS
|
||||
|
||||
Ensure that you have access to the CA, the cert and the public key used to access your Docker engine.
|
||||
|
||||
These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container:
|
||||
|
||||
```
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
|
||||
```
|
||||
|
||||
You can also use the `--tlscacert`, `--tlscert` and `--tlskey` flags if you want to change the default path to the CA, certificate and key file respectively:
|
||||
|
||||
```
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify --tlscacert /certs/myCa.pem --tlscert /certs/myCert.pem --tlskey /certs/myKey.pem
|
||||
```
|
||||
|
||||
*Note*: Replace `/path/to/certs` to the path to the certificate files on your disk.
|
||||
|
||||
### Hide containers with specific labels
|
||||
|
||||
You can hide specific containers in the containers view by using the `-hide-label` or `-l` options and specifying a label.
|
||||
@@ -74,9 +103,13 @@ $ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docke
|
||||
|
||||
The following options are available for the `ui-for-docker` binary:
|
||||
|
||||
* `--endpoint`, `-e`: Docker deamon endpoint (default: *"/var/run/docker.sock"*)
|
||||
* `--bind`, `-p`: Address and port to serve UI For Docker (default: *":9000"*)
|
||||
* `--data`, `-d`: Path to the data folder (default: *"."*)
|
||||
* `--assets`, `-a`: Path to the assets (default: *"."*)
|
||||
* `--swarm`, `-s`: Swarm cluster support (default: *false*)
|
||||
* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`)
|
||||
* `--bind`, `-p`: Address and port to serve UI For Docker (default: `":9000"`)
|
||||
* `--data`, `-d`: Path to the data folder (default: `"."`)
|
||||
* `--assets`, `-a`: Path to the assets (default: `"."`)
|
||||
* `--swarm`, `-s`: Swarm cluster support (default: `false`)
|
||||
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
|
||||
* `--tlsverify`: TLS support (default: `false`)
|
||||
* `--tlscacert`: Path to the CA (default `/certs/ca.pem`)
|
||||
* `--tlscert`: Path to the TLS certificate file (default `/certs/cert.pem`)
|
||||
* `--tlskey`: Path to the TLS key (default `/certs/key.pem`)
|
||||
|
||||
@@ -144,4 +144,4 @@ angular.module('uifordocker', [
|
||||
.constant('DOCKER_ENDPOINT', 'dockerapi')
|
||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('CONFIG_ENDPOINT', '/config')
|
||||
.constant('UI_VERSION', 'v1.3.0');
|
||||
.constant('UI_VERSION', 'v1.5.0');
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('State')">
|
||||
<a ui-sref="containers" ng-click="order('Status')">
|
||||
State
|
||||
<span ng-show="sortType == 'State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
@@ -52,7 +52,7 @@
|
||||
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<th ng-if="state.displayIP">
|
||||
<a ui-sref="containers" ng-click="order('IP')">
|
||||
IP Address
|
||||
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
@@ -85,10 +85,10 @@
|
||||
<tbody>
|
||||
<tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
|
||||
<td><span class="label label-{{ container.State|containerstatusbadge }}">{{ container.State }}</span></td>
|
||||
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
|
||||
<td ng-if="swarm"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
|
||||
<td ng-if="!swarm"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
||||
<td>{{ container.IP ? container.IP : '-' }}</td>
|
||||
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
||||
<td ng-if="swarm">{{ container|swarmhostname}}</td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
<td>{{ container.Command|truncate:60 }}</td>
|
||||
|
||||
@@ -4,6 +4,7 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
|
||||
|
||||
$scope.state = {};
|
||||
$scope.state.displayAll = Settings.displayAll;
|
||||
$scope.state.displayIP = false;
|
||||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = true;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
@@ -22,7 +23,11 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) {
|
||||
containers = hideContainers(d);
|
||||
}
|
||||
$scope.containers = containers.map(function (container) {
|
||||
return new ContainerViewModel(container);
|
||||
var model = new ContainerViewModel(container);
|
||||
if (model.IP) {
|
||||
$scope.state.displayIP = true;
|
||||
}
|
||||
return model;
|
||||
});
|
||||
$('#loadContainersSpinner').hide();
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<!-- !name-input -->
|
||||
<!-- image-and-registry-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="container_image" class="col-sm-1 control-label text-left">Name</label>
|
||||
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,18 @@ angular.module('createNetwork', [])
|
||||
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'errorMsgFilter',
|
||||
function ($scope, $state, Messages, Network, errorMsgFilter) {
|
||||
$scope.formValues = {
|
||||
DriverOptions: []
|
||||
DriverOptions: [],
|
||||
Subnet: '',
|
||||
Gateway: ''
|
||||
};
|
||||
|
||||
$scope.config = {
|
||||
Driver: 'bridge',
|
||||
CheckDuplicate: true,
|
||||
Internal: false
|
||||
Internal: false,
|
||||
IPAM: {
|
||||
Config: []
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addDriverOption = function() {
|
||||
@@ -36,6 +41,17 @@ function ($scope, $state, Messages, Network, errorMsgFilter) {
|
||||
});
|
||||
}
|
||||
|
||||
function prepareIPAMConfiguration(config) {
|
||||
if ($scope.formValues.Subnet) {
|
||||
var ipamConfig = {};
|
||||
ipamConfig.Subnet = $scope.formValues.Subnet;
|
||||
if ($scope.formValues.Gateway) {
|
||||
ipamConfig.Gateway = $scope.formValues.Gateway ;
|
||||
}
|
||||
config.IPAM.Config.push(ipamConfig);
|
||||
}
|
||||
}
|
||||
|
||||
function prepareDriverOptions(config) {
|
||||
var options = {};
|
||||
$scope.formValues.DriverOptions.forEach(function (option) {
|
||||
@@ -46,6 +62,7 @@ function ($scope, $state, Messages, Network, errorMsgFilter) {
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
prepareIPAMConfiguration(config);
|
||||
prepareDriverOptions(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- subnet-gateway-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="network_subnet" class="col-sm-1 control-label text-left">Subnet</label>
|
||||
<div class="col-sm-5">
|
||||
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
|
||||
</div>
|
||||
<label for="network_gateway" class="col-sm-1 control-label text-left">Gateway</label>
|
||||
<div class="col-sm-5">
|
||||
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !subnet-gateway-inputs -->
|
||||
<!-- driver-input -->
|
||||
<div class="form-group">
|
||||
<label for="network_driver" class="col-sm-1 control-label text-left">Driver</label>
|
||||
|
||||
@@ -1,76 +1,131 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Home"></rd-header-title>
|
||||
<rd-header-title title="Home">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Dashboard</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
<div class="title">{{ containerData.total }}</div>
|
||||
<div class="comment">Containers</div>
|
||||
<rd-widget-header icon="fa-cogs" title="Node info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ infoData.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker version</td>
|
||||
<td>{{ infoData.ServerVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU</td>
|
||||
<td>{{ infoData.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory</td>
|
||||
<td>{{ infoData.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon green pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
<div class="title">{{ containerData.running }}</div>
|
||||
<div class="comment">Running</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon red pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
<div class="title">{{ containerData.stopped }}</div>
|
||||
<div class="comment">Stopped</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon gray pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
<div class="title">{{ containerData.ghost }}</div>
|
||||
<div class="comment">Ghost</div>
|
||||
<rd-widget-header icon="fa-cogs" title="Cluster info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td>{{ infoData.SystemStatus[3][1] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swarm version</td>
|
||||
<td>{{ infoData.ServerVersion|swarmversion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total CPU</td>
|
||||
<td>{{ infoData.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td>{{ infoData.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Containers created"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<canvas id="containers-started-chart" width="770" height="230">
|
||||
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
|
||||
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
</canvas>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
|
||||
<a ui-sref="containers">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div><i class="fa fa-heartbeat text-icon green-icon"></i>{{ containerData.running }} running</div>
|
||||
<div><i class="fa fa-heartbeat text-icon red-icon"></i>{{ containerData.stopped }} stopped</div>
|
||||
</div>
|
||||
<div class="title">{{ containerData.total }}</div>
|
||||
<div class="comment">Containers</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-clone" title="Images created"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<canvas id="images-created-chart" width="770" height="230">
|
||||
<p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a
|
||||
href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
|
||||
</canvas>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
|
||||
<a ui-sref="images">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-clone"></i>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div><i class="fa fa-pie-chart text-icon"></i>{{ imageData.size|humansize }}</div>
|
||||
</div>
|
||||
<div class="title">{{ imageData.total }}</div>
|
||||
<div class="comment">Images</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
|
||||
<a ui-sref="volumes">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-cubes"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-if="infoData.Driver">
|
||||
<div><i class="fa fa-hdd-o text-icon"></i>{{ infoData.Driver }} driver</div>
|
||||
</div>
|
||||
<div class="title">{{ volumeData.total }}</div>
|
||||
<div class="comment">Volumes</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
|
||||
<a ui-sref="networks">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-sitemap"></i>
|
||||
</div>
|
||||
<div class="title">{{ networkData.total }}</div>
|
||||
<div class="comment">Networks</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,83 @@
|
||||
angular.module('dashboard', [])
|
||||
.controller('DashboardController', ['$scope', 'Config', 'Container', 'Image', 'Settings', 'LineChart',
|
||||
function ($scope, Config, Container, Image, Settings, LineChart) {
|
||||
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'Image', 'Network', 'Volume', 'Info',
|
||||
function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
|
||||
|
||||
$scope.containerData = {};
|
||||
|
||||
var buildCharts = function (data) {
|
||||
$scope.containerData.total = data.length;
|
||||
LineChart.build('#containers-started-chart', data, function (c) {
|
||||
return new Date(c.Created * 1000).toLocaleDateString();
|
||||
});
|
||||
var s = $scope;
|
||||
Image.query({}, function (d) {
|
||||
s.totalImages = d.length;
|
||||
LineChart.build('#images-created-chart', d, function (c) {
|
||||
return new Date(c.Created * 1000).toLocaleDateString();
|
||||
});
|
||||
});
|
||||
$scope.containerData = {
|
||||
total: 0
|
||||
};
|
||||
$scope.imageData = {
|
||||
total: 0
|
||||
};
|
||||
$scope.networkData = {
|
||||
total: 0
|
||||
};
|
||||
$scope.volumeData = {
|
||||
total: 0
|
||||
};
|
||||
|
||||
function prepareContainerData(d) {
|
||||
var running = 0;
|
||||
var stopped = 0;
|
||||
|
||||
var containers = d;
|
||||
if (hiddenLabels) {
|
||||
containers = hideContainers(d);
|
||||
}
|
||||
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var item = containers[i];
|
||||
if (item.Status.indexOf('Up') !== -1) {
|
||||
running += 1;
|
||||
} else if (item.Status.indexOf('Exit') !== -1) {
|
||||
stopped += 1;
|
||||
}
|
||||
}
|
||||
$scope.containerData.running = running;
|
||||
$scope.containerData.stopped = stopped;
|
||||
$scope.containerData.total = containers.length;
|
||||
}
|
||||
|
||||
function prepareImageData(d) {
|
||||
var images = d;
|
||||
var totalImageSize = 0;
|
||||
for (var i = 0; i < images.length; i++) {
|
||||
var item = images[i];
|
||||
totalImageSize += item.VirtualSize;
|
||||
}
|
||||
$scope.imageData.total = images.length;
|
||||
$scope.imageData.size = totalImageSize;
|
||||
}
|
||||
|
||||
function prepareVolumeData(d) {
|
||||
var volumes = d.Volumes;
|
||||
$scope.volumeData.total = volumes.length;
|
||||
}
|
||||
|
||||
function prepareNetworkData(d) {
|
||||
var networks = d;
|
||||
$scope.networkData.total = networks.length;
|
||||
}
|
||||
|
||||
function prepareInfoData(d) {
|
||||
var info = d;
|
||||
$scope.infoData = info;
|
||||
}
|
||||
|
||||
function fetchDashboardData() {
|
||||
Container.query({all: 1}, function (d) {
|
||||
var running = 0;
|
||||
var ghost = 0;
|
||||
var stopped = 0;
|
||||
|
||||
var containers = d;
|
||||
if (hiddenLabels) {
|
||||
containers = hideContainers(d);
|
||||
}
|
||||
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var item = containers[i];
|
||||
if (item.Status === "Ghost") {
|
||||
ghost += 1;
|
||||
} else if (item.Status.indexOf('Exit') !== -1) {
|
||||
stopped += 1;
|
||||
} else {
|
||||
running += 1;
|
||||
}
|
||||
}
|
||||
$scope.containerData.running = running;
|
||||
$scope.containerData.stopped = stopped;
|
||||
$scope.containerData.ghost = ghost;
|
||||
|
||||
buildCharts(containers);
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all([
|
||||
Container.query({all: 1}).$promise,
|
||||
Image.query({}).$promise,
|
||||
Volume.query({}).$promise,
|
||||
Network.query({}).$promise,
|
||||
Info.get({}).$promise
|
||||
]).then(function (d) {
|
||||
prepareContainerData(d[0]);
|
||||
prepareImageData(d[1]);
|
||||
prepareVolumeData(d[2]);
|
||||
prepareNetworkData(d[3]);
|
||||
prepareInfoData(d[4]);
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,6 +97,7 @@ function ($scope, Config, Container, Image, Settings, LineChart) {
|
||||
};
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
hiddenLabels = c.hiddenLabels;
|
||||
fetchDashboardData();
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Cluster status"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-object-group" title="Engine status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()" /> Select</label></th>
|
||||
@@ -76,7 +76,7 @@
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="images" ng-click="order('RepoTags')">
|
||||
Repository
|
||||
Tags
|
||||
<span ng-show="sortType == 'RepoTags' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'RepoTags' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
@@ -101,7 +101,9 @@
|
||||
<tr ng-repeat="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
|
||||
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
|
||||
<td>{{ image|repotag }}</td>
|
||||
<td>
|
||||
<span class="label label-primary image-tag" ng-repeat="tag in (image|repotags)">{{ tag }}</span>
|
||||
</td>
|
||||
<td>{{ image.VirtualSize|humansize }}</td>
|
||||
<td>{{ image.Created|getdate }}</td>
|
||||
</tr>
|
||||
|
||||
@@ -9,7 +9,7 @@ function ($scope, $state, Image, Messages) {
|
||||
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Registry: '',
|
||||
Registry: ''
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></th>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()"/> Select</label></th>
|
||||
|
||||
@@ -8,7 +8,7 @@ angular
|
||||
icon: '@'
|
||||
},
|
||||
transclude: true,
|
||||
template: '<div class="widget-header"><div class="row"><div class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </div><div class="pull-right col-xs-6 col-sm-4" ng-transclude></div></div></div>',
|
||||
template: '<div class="widget-header"><div class="row"><span class="pull-left"><i class="fa" ng-class="icon"></i> {{title}} </span><span class="pull-right col-xs-6 col-sm-4" ng-transclude></span></div></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
|
||||
@@ -21,16 +21,31 @@ angular.module('dockerui.filters', [])
|
||||
.filter('containerstatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
if (text === 'paused') {
|
||||
var status = _.toLower(text);
|
||||
if (status.indexOf('paused') !== -1) {
|
||||
return 'warning';
|
||||
} else if (text === 'created') {
|
||||
} else if (status.indexOf('created') !== -1) {
|
||||
return 'info';
|
||||
} else if (text === 'exited') {
|
||||
} else if (status.indexOf('exited') !== -1) {
|
||||
return 'danger';
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
})
|
||||
.filter('containerstatus', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
var status = _.toLower(text);
|
||||
if (status.indexOf('paused') !== -1) {
|
||||
return 'paused';
|
||||
} else if (status.indexOf('created') !== -1) {
|
||||
return 'created';
|
||||
} else if (status.indexOf('exited') !== -1) {
|
||||
return 'stopped';
|
||||
}
|
||||
return 'running';
|
||||
};
|
||||
})
|
||||
.filter('nodestatusbadge', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
@@ -115,23 +130,29 @@ angular.module('dockerui.filters', [])
|
||||
return _.split(container.Names[0], '/')[2];
|
||||
};
|
||||
})
|
||||
.filter('swarmversion', function () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
return _.split(text, '/')[1];
|
||||
};
|
||||
})
|
||||
.filter('swarmhostname', function () {
|
||||
'use strict';
|
||||
return function (container) {
|
||||
return _.split(container.Names[0], '/')[1];
|
||||
};
|
||||
})
|
||||
.filter('repotag', function () {
|
||||
.filter('repotags', function () {
|
||||
'use strict';
|
||||
return function (image) {
|
||||
if (image.RepoTags && image.RepoTags.length > 0) {
|
||||
var tag = image.RepoTags[0];
|
||||
if (tag === '<none>:<none>') {
|
||||
tag = '';
|
||||
return [];
|
||||
}
|
||||
return tag;
|
||||
return image.RepoTags;
|
||||
}
|
||||
return '';
|
||||
return [];
|
||||
};
|
||||
})
|
||||
.filter('getdate', function () {
|
||||
|
||||
@@ -10,9 +10,12 @@ function ImageViewModel(data) {
|
||||
|
||||
function ContainerViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.State = data.State;
|
||||
this.Status = data.Status;
|
||||
this.Names = data.Names;
|
||||
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
|
||||
// Unavailable in Docker < 1.10
|
||||
if (data.NetworkSettings) {
|
||||
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
|
||||
}
|
||||
this.Image = data.Image;
|
||||
this.Command = data.Command;
|
||||
this.Checked = false;
|
||||
|
||||
@@ -165,3 +165,23 @@ input[type="radio"] {
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.text-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.green-icon {
|
||||
color: #23ae89;
|
||||
}
|
||||
|
||||
.red-icon {
|
||||
color: #ae2323;
|
||||
}
|
||||
|
||||
.image-tag {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.widget .widget-body table tbody .image-tag {
|
||||
font-size: 90% !important;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "uifordocker",
|
||||
"version": "1.3.0",
|
||||
"version": "1.5.0",
|
||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||
"authors": [
|
||||
"Michael Crosby <crosbymichael@gmail.com>",
|
||||
|
||||
93
dockerui.go
93
dockerui.go
@@ -15,15 +15,21 @@ import (
|
||||
"fmt"
|
||||
"github.com/gorilla/securecookie"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
var (
|
||||
endpoint = kingpin.Flag("endpoint", "Dockerd endpoint").Default("/var/run/docker.sock").Short('e').String()
|
||||
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
|
||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
||||
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
|
||||
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
||||
labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
||||
endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String()
|
||||
addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String()
|
||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
||||
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
|
||||
swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool()
|
||||
tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool()
|
||||
tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String()
|
||||
tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String()
|
||||
tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String()
|
||||
labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
||||
authKey []byte
|
||||
authKeyFile = "authKey.dat"
|
||||
)
|
||||
@@ -32,6 +38,13 @@ type UnixHandler struct {
|
||||
path string
|
||||
}
|
||||
|
||||
type TlsFlags struct {
|
||||
tls bool
|
||||
caPath string
|
||||
certPath string
|
||||
keyPath string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Swarm bool `json:"swarm"`
|
||||
HiddenLabels Labels `json:"hiddenLabels"`
|
||||
@@ -106,35 +119,70 @@ func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) {
|
||||
json.NewEncoder(w).Encode(c)
|
||||
}
|
||||
|
||||
func createTcpHandler(e string) http.Handler {
|
||||
u, err := url.Parse(e)
|
||||
func createTcpHandler(u *url.URL) http.Handler {
|
||||
u.Scheme = "http";
|
||||
return httputil.NewSingleHostReverseProxy(u)
|
||||
}
|
||||
|
||||
func createTlsConfig(tlsFlags TlsFlags) *tls.Config {
|
||||
cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return httputil.NewSingleHostReverseProxy(u)
|
||||
caCert, err := ioutil.ReadFile(tlsFlags.caPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
return tlsConfig;
|
||||
}
|
||||
|
||||
func createTcpHandlerWithTLS(u *url.URL, tlsFlags TlsFlags) http.Handler {
|
||||
u.Scheme = "https";
|
||||
var tlsConfig = createTlsConfig(tlsFlags)
|
||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
func createUnixHandler(e string) http.Handler {
|
||||
return &UnixHandler{e}
|
||||
}
|
||||
|
||||
func createHandler(dir string, d string, e string, c Config) http.Handler {
|
||||
func createHandler(dir string, d string, e string, c Config, tlsFlags TlsFlags) http.Handler {
|
||||
var (
|
||||
mux = http.NewServeMux()
|
||||
fileHandler = http.FileServer(http.Dir(dir))
|
||||
h http.Handler
|
||||
)
|
||||
|
||||
if strings.Contains(e, "http") {
|
||||
h = createTcpHandler(e)
|
||||
} else {
|
||||
if _, err := os.Stat(e); err != nil {
|
||||
u, perr := url.Parse(e)
|
||||
if perr != nil {
|
||||
log.Fatal(perr)
|
||||
}
|
||||
if u.Scheme == "tcp" {
|
||||
if tlsFlags.tls {
|
||||
h = createTcpHandlerWithTLS(u, tlsFlags)
|
||||
} else {
|
||||
h = createTcpHandler(u)
|
||||
}
|
||||
} else if u.Scheme == "unix" {
|
||||
var socketPath = u.Path
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatalf("unix socket %s does not exist", e)
|
||||
log.Fatalf("unix socket %s does not exist", socketPath)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
h = createUnixHandler(e)
|
||||
h = createUnixHandler(socketPath)
|
||||
} else {
|
||||
log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", e)
|
||||
}
|
||||
|
||||
// Use existing csrf authKey if present or generate a new one.
|
||||
@@ -173,7 +221,7 @@ func csrfWrapper(h http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
func main() {
|
||||
kingpin.Version("1.3.0")
|
||||
kingpin.Version("1.5.0")
|
||||
kingpin.Parse()
|
||||
|
||||
configuration := Config{
|
||||
@@ -181,7 +229,14 @@ func main() {
|
||||
HiddenLabels: *labels,
|
||||
}
|
||||
|
||||
handler := createHandler(*assets, *data, *endpoint, configuration)
|
||||
tlsFlags := TlsFlags{
|
||||
tls: *tlsverify,
|
||||
caPath: *tlscacert,
|
||||
certPath: *tlscert,
|
||||
keyPath: *tlskey,
|
||||
}
|
||||
|
||||
handler := createHandler(*assets, *data, *endpoint, configuration, tlsFlags)
|
||||
if err := http.ListenAndServe(*addr, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
14
gruntFile.js
14
gruntFile.js
@@ -40,6 +40,7 @@ module.exports = function (grunt) {
|
||||
grunt.registerTask('run', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:run']);
|
||||
grunt.registerTask('run-swarm', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']);
|
||||
grunt.registerTask('run-dev', ['if:binaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']);
|
||||
grunt.registerTask('run-ssl', ['if:binaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']);
|
||||
grunt.registerTask('clear', ['clean:app']);
|
||||
|
||||
// Print a timestamp (useful for when watching)
|
||||
@@ -224,6 +225,10 @@ module.exports = function (grunt) {
|
||||
buildSwarm: {
|
||||
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:runSwarm', 'shell:cleanImages']
|
||||
},
|
||||
buildSsl: {
|
||||
files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'],
|
||||
tasks: ['build', 'shell:buildImage', 'shell:runSsl', 'shell:cleanImages']
|
||||
}
|
||||
},
|
||||
jshint: {
|
||||
@@ -267,7 +272,14 @@ module.exports = function (grunt) {
|
||||
command: [
|
||||
'docker stop ui-for-docker',
|
||||
'docker rm ui-for-docker',
|
||||
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 --swarm -d /data'
|
||||
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:4000 --swarm -d /data'
|
||||
].join(';')
|
||||
},
|
||||
runSsl: {
|
||||
command: [
|
||||
'docker stop ui-for-docker',
|
||||
'docker rm ui-for-docker',
|
||||
'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data -v /tmp/docker-ssl:/certs --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:2376 -d /data --tlsverify'
|
||||
].join(';')
|
||||
},
|
||||
cleanImages: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Michael Crosby & Kevan Ahlquist",
|
||||
"name": "uifordocker",
|
||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||
"version": "1.3.0",
|
||||
"version": "1.5.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:kevana/ui-for-docker.git"
|
||||
|
||||
Reference in New Issue
Block a user