Compare commits

...

23 Commits
1.2.0 ... 1.4.0

Author SHA1 Message Date
Anthony Lapenna
00b2c92e39 Merge branch 'develop' of github.com:cloud-inovasi/cloudinovasi-ui into develop 2016-07-13 14:54:35 +12:00
Anthony Lapenna
0796778d17 Merge tag '1.4.0' into develop
Release 1.4.0
2016-07-13 14:54:07 +12:00
Anthony Lapenna
1eae1c03f0 Merge branch 'release/1.4.0' 2016-07-13 14:54:03 +12:00
Anthony Lapenna
a9209da167 chore(version): bump version number 2016-07-13 14:53:24 +12:00
Anthony Lapenna
43c2f14289 docs(docker): add info about Docker version support (#64) 2016-07-13 14:47:24 +12:00
Anthony Lapenna
f378d56543 fix(ui): fix bad name for image field in container creation view 2016-07-13 13:47:15 +12:00
Anthony Lapenna
3b0d726c2a feat(dockerui): Docker CLI compliant flags (#67) 2016-07-13 12:44:31 +12:00
Anthony Lapenna
71c091ae0d feat(ui): docker 1.9 support (#65) 2016-07-13 10:53:03 +12:00
Anthony Lapenna
1fb008212a feat(dockerui): add support for TLS enabled engines (#63) 2016-07-12 20:31:11 +12:00
Anthony Lapenna
e67e20ce18 feat(network): add the ability to specify a subnet/gateway when creating a network (#53) 2016-07-08 17:12:33 +12:00
Anthony Lapenna
c74e8fc732 style(lint): fix jshint issue 2016-07-08 16:20:31 +12:00
Anthony Lapenna
29358e5744 Merge tag '1.3.0' into develop
Release 1.3.0
2016-07-08 16:06:58 +12:00
Anthony Lapenna
b59c102098 Merge branch 'release/1.3.0' 2016-07-08 16:06:53 +12:00
Anthony Lapenna
afaa1433ff chore(version): bump version number 2016-07-08 16:06:46 +12:00
Anthony Lapenna
ca27e7f27a fix(containerCreation): fix an issue when creating an image from a custom registry without automatic pulling (#50) 2016-07-08 15:40:13 +12:00
Anthony Lapenna
d124c21d1b feat(ui): add the ability to create a container from an image in a custom registry (#49) 2016-07-08 12:52:26 +12:00
Anthony Lapenna
d2fb2cb863 feat(ui): add the ability to pull an image from a private registry (#47) 2016-07-08 11:57:24 +12:00
Anthony Lapenna
06f54e300c fix(ui): hidden containers (using label) are now removed from dashboard and swarm view (#46) 2016-07-07 15:37:09 +12:00
Anthony Lapenna
21c1778822 feat(ui): default to display all containers (#45) 2016-07-07 14:31:16 +12:00
Anthony Lapenna
092d866c73 fix(ui): fix display issue with multiple nodes in Swarm view (#44) 2016-07-07 13:22:31 +12:00
Anthony Lapenna
50391c87e2 feat(ui): replace ViewSpinner with JQuery animations (#43) 2016-07-07 13:17:44 +12:00
Anthony Lapenna
d227bdfc75 refactor(ui): remove useless controller declarations 2016-07-06 17:42:56 +12:00
Anthony Lapenna
4ba6286c97 Merge tag '1.2.0' into develop
Release 1.2.0
2016-07-06 16:41:33 +12:00
32 changed files with 415 additions and 220 deletions

View File

@@ -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`)

View File

@@ -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.2.0');
.constant('UI_VERSION', 'v1.4.0');

View File

@@ -1,5 +1,7 @@
<rd-header>
<rd-header-title title="Container details"></rd-header-title>
<rd-header-title title="Container details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
</rd-header-content>

View File

@@ -1,6 +1,6 @@
angular.module('container', [])
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', 'ViewSpinner', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, ViewSpinner, $timeout) {
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, $timeout) {
$scope.changes = [];
$scope.editEnv = false;
$scope.editPorts = false;
@@ -11,7 +11,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
var update = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
$scope.container.edit = false;
@@ -61,7 +61,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
$scope.newCfg.Binds.push(bind);
});
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
@@ -69,13 +69,13 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
} else {
Messages.error("Failure", e.data);
}
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
});
};
$scope.start = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.start({
id: $scope.container.Id,
HostConfig: $scope.container.HostConfig
@@ -89,7 +89,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.stop = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.stop({id: $stateParams.id}, function (d) {
update();
Messages.send("Container stopped", $stateParams.id);
@@ -100,7 +100,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.kill = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.kill({id: $stateParams.id}, function (d) {
update();
Messages.send("Container killed", $stateParams.id);
@@ -111,7 +111,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.commit = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
update();
Messages.send("Container commited", $stateParams.id);
@@ -121,7 +121,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
});
};
$scope.pause = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.pause({id: $stateParams.id}, function (d) {
update();
Messages.send("Container paused", $stateParams.id);
@@ -132,7 +132,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.unpause = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.unpause({id: $stateParams.id}, function (d) {
update();
Messages.send("Container unpaused", $stateParams.id);
@@ -143,7 +143,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.remove = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.remove({id: $stateParams.id}, function (d) {
update();
$state.go('containers', {}, {reload: true});
@@ -155,7 +155,7 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.restart = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.restart({id: $stateParams.id}, function (d) {
update();
Messages.send("Container restarted", $stateParams.id);
@@ -170,10 +170,10 @@ function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Ima
};
$scope.getChanges = function () {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.changes({id: $stateParams.id}, function (d) {
$scope.changes = d;
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
});
};

View File

@@ -1,6 +1,6 @@
angular.module('containerLogs', [])
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpinner) {
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container',
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
$scope.state = {};
$scope.state.displayTimestampsOut = false;
$scope.state.displayTimestampsErr = false;
@@ -8,24 +8,24 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container, ViewSpi
$scope.stderr = '';
$scope.tailLines = 2000;
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
});
function getLogs() {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
getLogsStdout();
getLogsStderr();
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}
function getLogsStderr() {

View File

@@ -1,5 +1,7 @@
<rd-header>
<rd-header-title title="Container logs"></rd-header-title>
<rd-header-title title="Container logs">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
</rd-header-content>

View File

@@ -10,6 +10,9 @@
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Containers">
<div class="pull-right">
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">
@@ -25,7 +28,7 @@
<a class="btn btn-default" type="button" ui-sref="actions.create.container">Add container</a>
</div>
<div class="pull-right">
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()"/><label for="displayAll">Display All</label>
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div>
</rd-widget-taskbar>
@@ -36,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>
@@ -49,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>
@@ -82,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>

View File

@@ -1,9 +1,10 @@
angular.module('containers', [])
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 'Config', 'errorMsgFilter',
function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFilter) {
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
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;
@@ -14,7 +15,7 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
};
var update = function (data) {
ViewSpinner.spin();
$('#loadContainersSpinner').show();
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
@@ -22,19 +23,23 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
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;
});
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
});
};
var batch = function (items, action, msg) {
ViewSpinner.spin();
$('#loadContainersSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
update({all: Settings.displayAll ? 1 : 0});
}
};
@@ -89,7 +94,7 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFi
}
});
if (counter === 0) {
ViewSpinner.stop();
$('#loadContainersSpinner').hide();
}
};

View File

@@ -1,6 +1,6 @@
angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, ViewSpinner, errorMsgFilter) {
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, errorMsgFilter) {
$scope.state = {
alwaysPull: true
@@ -8,9 +8,12 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
$scope.formValues = {
Console: 'none',
Volumes: []
Volumes: [],
Registry: ''
};
$scope.imageConfig = {};
$scope.config = {
Env: [],
HostConfig: {
@@ -80,59 +83,67 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
});
function createContainer(config) {
ViewSpinner.spin();
$('#createContainerSpinner').show();
Container.create(config, function (d) {
if (d.Id) {
var reqBody = config.HostConfig || {};
reqBody.id = d.Id;
Container.start(reqBody, function (cd) {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}, function (e) {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(e));
});
} else {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(e));
});
}
function createImageConfig(imageName) {
var imageNameAndTag = imageName.split(':');
var imageConfig = {
fromImage: imageNameAndTag[0],
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
function pullImageAndCreateContainer(config) {
ViewSpinner.spin();
var image = _.toLower(config.Image);
var imageConfig = createImageConfig(image);
Image.create(imageConfig, function (data) {
$('#createContainerSpinner').show();
Image.create($scope.imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', detail.error);
} else {
createContainer(config);
}
}, function (e) {
ViewSpinner.stop();
$('#createContainerSpinner').hide();
Messages.error('Error', 'Unable to pull image ' + image);
});
}
function createImageConfig(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
fromImage: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
function prepareImageConfig(config) {
var image = _.toLower(config.Image);
var registry = $scope.formValues.Registry;
var imageConfig = createImageConfig(image, registry);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
$scope.imageConfig = imageConfig;
}
function preparePortBindings(config) {
var bindings = {};
config.HostConfig.PortBindings.forEach(function (portBinding) {
@@ -192,6 +203,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, V
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareImageConfig(config);
preparePortBindings(config);
prepareConsole(config);
prepareEnvironmentVariables(config);

View File

@@ -18,11 +18,15 @@
</div>
</div>
<!-- !name-input -->
<!-- image input -->
<!-- image-and-registry-inputs -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="ubuntu:trusty">
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
</div>
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
@@ -32,7 +36,7 @@
</div>
</div>
</div>
<!-- !image-input -->
<!-- !image-and-registry-inputs -->
<!-- restart-policy -->
<div class="form-group">
<label class="col-sm-1 control-label text-left">Restart policy</label>
@@ -306,6 +310,9 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="containers">Cancel</a>
</div>

View File

@@ -1,14 +1,19 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Messages, Network, ViewSpinner, errorMsgFilter) {
.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() {
@@ -20,22 +25,33 @@ function ($scope, $state, Messages, Network, ViewSpinner, errorMsgFilter) {
};
function createNetwork(config) {
ViewSpinner.spin();
$('#createNetworkSpinner').show();
Network.create(config, function (d) {
if (d.Id) {
Messages.send("Network created", d.Id);
ViewSpinner.stop();
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
} else {
ViewSpinner.stop();
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', errorMsgFilter(d));
}
}, function (e) {
ViewSpinner.stop();
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', e.data);
});
}
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, ViewSpinner, errorMsgFilter) {
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
prepareDriverOptions(config);
return config;
}

View File

@@ -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>
@@ -74,6 +86,9 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createNetworkSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="networks">Cancel</a>
</div>

View File

@@ -1,6 +1,6 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages', 'ViewSpinner', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, ViewSpinner, errorMsgFilter) {
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, $state, Volume, Messages, errorMsgFilter) {
$scope.formValues = {
DriverOptions: []
@@ -19,18 +19,18 @@ function ($scope, $state, Volume, Messages, ViewSpinner, errorMsgFilter) {
};
function createVolume(config) {
ViewSpinner.spin();
$('#createVolumeSpinner').show();
Volume.create(config, function (d) {
if (d.Name) {
Messages.send("Volume created", d.Name);
ViewSpinner.stop();
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
} else {
ViewSpinner.stop();
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', errorMsgFilter(d));
}
}, function (e) {
ViewSpinner.stop();
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', e.data);
});
}

View File

@@ -63,6 +63,9 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
<div>
<i id="createVolumeSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
</div>
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="volumes">Cancel</a>
</div>

View File

@@ -1,5 +1,6 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', 'Container', 'Image', 'Settings', 'LineChart', function ($scope, Container, Image, Settings, LineChart) {
.controller('DashboardController', ['$scope', 'Config', 'Container', 'Image', 'Settings', 'LineChart',
function ($scope, Config, Container, Image, Settings, LineChart) {
$scope.containerData = {};
@@ -17,30 +18,52 @@ angular.module('dashboard', [])
});
};
Container.query({all: 1}, function (d) {
var running = 0;
var ghost = 0;
var stopped = 0;
function fetchDashboardData() {
Container.query({all: 1}, function (d) {
var running = 0;
var ghost = 0;
var stopped = 0;
// TODO: centralize that
var containers = d.filter(function (container) {
return container.Image !== 'swarm';
});
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;
var containers = d;
if (hiddenLabels) {
containers = hideContainers(d);
}
}
$scope.containerData.running = running;
$scope.containerData.stopped = stopped;
$scope.containerData.ghost = ghost;
buildCharts(containers);
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);
});
}
var hideContainers = function (containers) {
return containers.filter(function (container) {
var filterContainer = false;
hiddenLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
Config.$promise.then(function (c) {
hiddenLabels = c.hiddenLabels;
fetchDashboardData();
});
}]);

View File

@@ -4,7 +4,6 @@
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
</rd-header-title>
<rd-header-content>Images</rd-header-content>
</rd-header>
@@ -15,14 +14,18 @@
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
</div>
</div>
<!-- !name-input -->
<!-- !name-and-registry-inputs -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
@@ -33,6 +36,7 @@
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="pullImage()">Pull</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
@@ -45,6 +49,9 @@
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-clone" title="Images">
<div class="pull-right">
<i id="loadImagesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">

View File

@@ -1,6 +1,6 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Image', 'ViewSpinner', 'Messages',
function ($scope, $state, Image, ViewSpinner, Messages) {
.controller('ImagesController', ['$scope', '$state', 'Image', 'Messages',
function ($scope, $state, Image, Messages) {
$scope.state = {};
$scope.sortType = 'Created';
$scope.sortReverse = true;
@@ -8,7 +8,8 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
$scope.state.selectedItemCount = 0;
$scope.config = {
Image: ''
Image: '',
Registry: ''
};
$scope.order = function(sortType) {
@@ -35,42 +36,47 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
}
};
function createImageConfig(imageName) {
function createImageConfig(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
fromImage: imageNameAndTag[0],
fromImage: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
$scope.pullImage = function() {
ViewSpinner.spin();
$('#pullImageSpinner').show();
var image = _.toLower($scope.config.Image);
var imageConfig = createImageConfig(image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = createImageConfig(image, registry);
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
ViewSpinner.stop();
$('#pullImageSpinner').hide();
Messages.error('Error', detail.error);
} else {
ViewSpinner.stop();
$('#pullImageSpinner').hide();
$state.go('images', {}, {reload: true});
}
}, function (e) {
ViewSpinner.stop();
$('#pullImageSpinner').hide();
Messages.error('Error', 'Unable to pull image ' + image);
});
};
$scope.removeAction = function () {
ViewSpinner.spin();
$('#loadImagesSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
}
};
angular.forEach($scope.images, function (i) {
@@ -85,6 +91,7 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
complete();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadImagesSpinner').hide();
complete();
});
}
@@ -92,15 +99,14 @@ function ($scope, $state, Image, ViewSpinner, Messages) {
};
function fetchImages() {
ViewSpinner.spin();
Image.query({}, function (d) {
$scope.images = d.map(function (item) {
return new ImageViewModel(item);
});
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadImagesSpinner').hide();
});
}

View File

@@ -1,5 +1,7 @@
<rd-header>
<rd-header-title title="Network details"></rd-header-title>
<rd-header-title title="Network details">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
</rd-header-content>

View File

@@ -1,37 +1,37 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', 'Network', 'ViewSpinner', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, ViewSpinner, Messages, $state, $stateParams, errorMsgFilter) {
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
$scope.disconnect = function disconnect(networkId, containerId) {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) {
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.send("Container disconnected", containerId);
$state.go('network', {id: $stateParams.id}, {reload: true});
}, function (e) {
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.error("Failure", e.data);
});
};
$scope.remove = function remove(networkId) {
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.send("Network removed", "");
$state.go('networks', {});
}, function (e) {
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
Messages.error("Failure", e.data);
});
};
ViewSpinner.spin();
$('#loadingViewSpinner').show();
Network.get({id: $stateParams.id}, function (d) {
$scope.network = d;
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadingViewSpinner').hide();
});
}]);

View File

@@ -1,5 +1,3 @@
<div ng-include="template" ng-controller="CreateNetworkController"></div>
<rd-header>
<rd-header-title title="Network list">
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
@@ -9,10 +7,12 @@
<rd-header-content>Networks</rd-header-content>
</rd-header>
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-sitemap" title="Networks">
<div class="pull-right">
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">

View File

@@ -1,6 +1,6 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', 'Network', 'ViewSpinner', 'Messages', 'errorMsgFilter',
function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
.controller('NetworksController', ['$scope', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, Network, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.toggle = false;
@@ -33,12 +33,12 @@ function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
};
$scope.removeAction = function () {
ViewSpinner.spin();
$('#loadNetworksSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
}
};
angular.forEach($scope.networks, function (network) {
@@ -58,13 +58,13 @@ function ($scope, Network, ViewSpinner, Messages, errorMsgFilter) {
};
function fetchNetworks() {
ViewSpinner.spin();
$('#loadNetworksSpinner').show();
Network.query({}, function (d) {
$scope.networks = d;
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadNetworksSpinner').hide();
});
}
fetchNetworks();

View File

@@ -54,10 +54,6 @@
<td>Nodes</td>
<td>{{ swarm.Nodes }}</td>
</tr>
<tr>
<td>Containers</td>
<td>{{ info.Containers }}</td>
</tr>
<tr>
<td>Images</td>
<td>{{ info.Images }}</td>
@@ -108,13 +104,6 @@
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Containers')">
Containers
<span ng-show="sortType == 'Containers' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Containers' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="swarm" ng-click="order('Engine')">
Engine
@@ -135,7 +124,6 @@
<tr ng-repeat="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse))">
<td>{{ node.name }}</td>
<td>{{ node.ip }}</td>
<td>{{ node.containers }}</td>
<td>{{ node.version }}</td>
<td><span class="label label-{{ node.status|nodestatusbadge }}">{{ node.status }}</span></td>
</tr>

View File

@@ -42,7 +42,7 @@ angular.module('swarm', [])
var node_offset = 4;
for (i = 0; i < node_count; i++) {
extractNodeInfo(info, node_offset);
node_offset += 10;
node_offset += 9;
}
}

View File

@@ -1,5 +1,3 @@
<div ng-include="template" ng-controller="CreateVolumeController"></div>
<rd-header>
<rd-header-title title="Volume list">
<a data-toggle="tooltip" title="Refresh" ui-sref="volumes" ui-sref-opts="{reload: true}">
@@ -12,6 +10,9 @@
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes">
<div class="pull-right">
<i id="loadVolumesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
<div class="pull-left">

View File

@@ -1,6 +1,6 @@
angular.module('volumes', [])
.controller('VolumesController', ['$scope', 'Volume', 'ViewSpinner', 'Messages', 'errorMsgFilter',
function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
.controller('VolumesController', ['$scope', 'Volume', 'Messages', 'errorMsgFilter',
function ($scope, Volume, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.toggle = false;
$scope.state.selectedItemCount = 0;
@@ -32,12 +32,12 @@ function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
};
$scope.removeAction = function () {
ViewSpinner.spin();
$('#loadVolumesSpinner').show();
var counter = 0;
var complete = function () {
counter = counter - 1;
if (counter === 0) {
ViewSpinner.stop();
$('#loadVolumesSpinner').hide();
}
};
angular.forEach($scope.volumes, function (volume) {
@@ -57,13 +57,13 @@ function ($scope, Volume, ViewSpinner, Messages, errorMsgFilter) {
};
function fetchVolumes() {
ViewSpinner.spin();
$('#loadVolumesSpinner').show();
Volume.query({}, function (d) {
$scope.volumes = d.Volumes;
ViewSpinner.stop();
$('#loadVolumesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
ViewSpinner.stop();
$('#loadVolumesSpinner').hide();
});
}
fetchVolumes();

View File

@@ -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) {

View File

@@ -153,27 +153,13 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
}
var firstLoad = (localStorage.getItem('firstLoad') || 'true') === 'true';
return {
displayAll: false,
displayAll: true,
endpoint: DOCKER_ENDPOINT,
uiVersion: UI_VERSION,
url: url,
firstLoad: firstLoad
};
}])
.factory('ViewSpinner', function ViewSpinnerFactory() {
'use strict';
var spinner = new Spinner();
var target = document.getElementById('view');
return {
spin: function () {
spinner.spin(target);
},
stop: function () {
spinner.stop();
}
};
})
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
'use strict';
return {

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
{
"name": "uifordocker",
"version": "1.2.0",
"version": "1.4.0",
"homepage": "https://github.com/kevana/ui-for-docker",
"authors": [
"Michael Crosby <crosbymichael@gmail.com>",
@@ -34,12 +34,11 @@
"angular-resource": "~1.5.0",
"angular-ui-select": "~0.17.1",
"bootstrap": "~3.3.6",
"font-awesome": "~4.5.0",
"font-awesome": "~4.6.3",
"jquery": "1.11.1",
"jquery.gritter": "1.7.4",
"lodash": "4.12.0",
"rdash-ui": "1.0.*",
"spin.js": "1.3"
},
"resolutions": {
"angular": "1.5.5"

View File

@@ -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.2.0")
kingpin.Version("1.4.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)
}

View File

@@ -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)
@@ -69,7 +70,6 @@ module.exports = function (grunt) {
'bower_components/jquery/dist/jquery.min.js',
'assets/js/jquery.gritter.js', // Using custom version to fix error in minified build due to "use strict"
'bower_components/bootstrap/dist/js/bootstrap.min.js',
'bower_components/spin.js/spin.js',
'bower_components/Chart.js/Chart.min.js',
'bower_components/lodash/dist/lodash.min.js',
'bower_components/oboe/dist/oboe-browser.js',
@@ -225,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: {
@@ -268,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: {

View File

@@ -2,7 +2,7 @@
"author": "Michael Crosby & Kevan Ahlquist",
"name": "uifordocker",
"homepage": "https://github.com/kevana/ui-for-docker",
"version": "1.2.0",
"version": "1.4.0",
"repository": {
"type": "git",
"url": "git@github.com:kevana/ui-for-docker.git"