mirror of https://github.com/portainer/portainer
commit
41a41cdf38
|
@ -2,5 +2,7 @@ FROM scratch
|
||||||
|
|
||||||
COPY dist /
|
COPY dist /
|
||||||
|
|
||||||
|
VOLUME /data
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
ENTRYPOINT ["/ui-for-docker"]
|
ENTRYPOINT ["/ui-for-docker"]
|
||||||
|
|
15
README.md
15
README.md
|
@ -37,11 +37,11 @@ $ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:23
|
||||||
|
|
||||||
**Supported Swarm version: 1.2.3**
|
**Supported Swarm version: 1.2.3**
|
||||||
|
|
||||||
You can access a specific view for you Swarm cluster by defining the `-swarm` flag:
|
You can access a specific view for you Swarm cluster by defining the `--swarm` flag:
|
||||||
|
|
||||||
```
|
```
|
||||||
# Connect to a tcp socket and enable Swarm:
|
# 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 -e http://<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.
|
*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.
|
||||||
|
@ -74,8 +74,9 @@ $ 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:
|
The following options are available for the `ui-for-docker` binary:
|
||||||
|
|
||||||
* `-endpoint`, `-e`: Docker deamon endpoint (default: *"/var/run/docker.sock"*)
|
* `--endpoint`, `-e`: Docker deamon endpoint (default: *"/var/run/docker.sock"*)
|
||||||
* `-bind`, `-p`: Address and port to serve UI For Docker (default: *":9000"*)
|
* `--bind`, `-p`: Address and port to serve UI For Docker (default: *":9000"*)
|
||||||
* `-assets`, `-a`: Path to the assets (default: *"."*)
|
* `--data`, `-d`: Path to the data folder (default: *"."*)
|
||||||
* `-swarm`, `-s`: Swarm cluster support (default: *false*)
|
* `--assets`, `-a`: Path to the assets (default: *"."*)
|
||||||
* `-hide-label`, `-l`: Hide containers with a specific label in the UI
|
* `--swarm`, `-s`: Swarm cluster support (default: *false*)
|
||||||
|
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
|
||||||
|
|
19
app/app.js
19
app/app.js
|
@ -10,11 +10,11 @@ angular.module('uifordocker', [
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'container',
|
'container',
|
||||||
'containers',
|
'containers',
|
||||||
|
'createContainer',
|
||||||
'docker',
|
'docker',
|
||||||
'images',
|
'images',
|
||||||
'image',
|
'image',
|
||||||
'pullImage',
|
'pullImage',
|
||||||
'startContainer',
|
|
||||||
'containerLogs',
|
'containerLogs',
|
||||||
'stats',
|
'stats',
|
||||||
'swarm',
|
'swarm',
|
||||||
|
@ -57,6 +57,21 @@ angular.module('uifordocker', [
|
||||||
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
||||||
controller: 'ContainerLogsController'
|
controller: 'ContainerLogsController'
|
||||||
})
|
})
|
||||||
|
.state('actions', {
|
||||||
|
abstract: true,
|
||||||
|
url: "/actions",
|
||||||
|
template: '<ui-view/>'
|
||||||
|
})
|
||||||
|
.state('actions.create', {
|
||||||
|
abstract: true,
|
||||||
|
url: "/create",
|
||||||
|
template: '<ui-view/>'
|
||||||
|
})
|
||||||
|
.state('actions.create.container', {
|
||||||
|
url: "/container",
|
||||||
|
templateUrl: 'app/components/createContainer/createcontainer.html',
|
||||||
|
controller: 'CreateContainerController'
|
||||||
|
})
|
||||||
.state('docker', {
|
.state('docker', {
|
||||||
url: '/docker/',
|
url: '/docker/',
|
||||||
templateUrl: 'app/components/docker/docker.html',
|
templateUrl: 'app/components/docker/docker.html',
|
||||||
|
@ -120,4 +135,4 @@ angular.module('uifordocker', [
|
||||||
.constant('DOCKER_ENDPOINT', 'dockerapi')
|
.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('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('CONFIG_ENDPOINT', '/config')
|
||||||
.constant('UI_VERSION', 'v1.0.4');
|
.constant('UI_VERSION', 'v1.1.0');
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-6 col-md-12 col-xs-12">
|
<div class="col-lg-6 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
<div ng-include="template" ng-controller="StartContainerController"></div>
|
|
||||||
|
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title="Container list">
|
<rd-header-title title="Container list">
|
||||||
<a data-toggle="tooltip" title="Refresh" ui-sref="containers" ui-sref-opts="{reload: true}">
|
<a data-toggle="tooltip" title="Refresh" ui-sref="containers" ui-sref-opts="{reload: true}">
|
||||||
|
@ -23,8 +21,8 @@
|
||||||
<button type="button" class="btn btn-primary" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount">Pause</button>
|
<button type="button" class="btn btn-primary" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount">Pause</button>
|
||||||
<button type="button" class="btn btn-primary" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount">Unpause</button>
|
<button type="button" class="btn btn-primary" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount">Unpause</button>
|
||||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||||
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#create-modal">Start a new container...</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<a class="btn btn-default" type="button" ui-sref="actions.create.container">Add container</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<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()"/><label for="displayAll">Display All</label>
|
||||||
|
@ -33,10 +31,17 @@
|
||||||
</rd-widget-taskbar>
|
</rd-widget-taskbar>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><label><input type="checkbox" ng-model="state.toggle" ng-change="toggleSelectAll()" /> Select</label></th>
|
<th></th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="containers" ng-click="order('State')">
|
||||||
|
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>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="containers" ng-click="order('Names')">
|
<a ui-sref="containers" ng-click="order('Names')">
|
||||||
Name
|
Name
|
||||||
|
@ -44,6 +49,20 @@
|
||||||
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="containers" ng-click="order('IP')">
|
||||||
|
IP Address
|
||||||
|
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th ng-if="swarm">
|
||||||
|
<a ui-sref="containers" ng-click="order('Host')">
|
||||||
|
Host
|
||||||
|
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'Host' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="containers" ng-click="order('Image')">
|
<a ui-sref="containers" ng-click="order('Image')">
|
||||||
Image
|
Image
|
||||||
|
@ -58,30 +77,18 @@
|
||||||
<span ng-show="sortType == 'Command' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Command' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
|
||||||
<a ui-sref="containers" ng-click="order('Created')">
|
|
||||||
Created
|
|
||||||
<span ng-show="sortType == 'Created' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
|
||||||
<span ng-show="sortType == 'Created' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<a ui-sref="containers" ng-click="order('Status')">
|
|
||||||
Status
|
|
||||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
|
||||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
|
<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><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
|
||||||
<td><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
<td><span class="label label-{{ container.State|containerstatusbadge }}">{{ container.State }}</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="swarm">{{ container|swarmhostname}}</td>
|
||||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||||
<td>{{ container.Command|truncate:40 }}</td>
|
<td>{{ container.Command|truncate:60 }}</td>
|
||||||
<td>{{ container.Created|getdate }}</td>
|
|
||||||
<td><span class="label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
angular.module('containers', [])
|
angular.module('containers', [])
|
||||||
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 'Config',
|
.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', 'Config', 'errorMsgFilter',
|
||||||
function ($scope, Container, Settings, Messages, ViewSpinner, Config) {
|
function ($scope, Container, Settings, Messages, ViewSpinner, Config, errorMsgFilter) {
|
||||||
|
|
||||||
$scope.state = {};
|
$scope.state = {};
|
||||||
$scope.state.displayAll = Settings.displayAll;
|
$scope.state.displayAll = Settings.displayAll;
|
||||||
$scope.sortType = 'Created';
|
$scope.sortType = 'State';
|
||||||
$scope.sortReverse = true;
|
$scope.sortReverse = true;
|
||||||
$scope.state.toggle = false;
|
|
||||||
$scope.state.selectedItemCount = 0;
|
$scope.state.selectedItemCount = 0;
|
||||||
|
|
||||||
$scope.order = function (sortType) {
|
$scope.order = function (sortType) {
|
||||||
|
@ -41,13 +40,12 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) {
|
||||||
};
|
};
|
||||||
angular.forEach(items, function (c) {
|
angular.forEach(items, function (c) {
|
||||||
if (c.Checked) {
|
if (c.Checked) {
|
||||||
|
counter = counter + 1;
|
||||||
if (action === Container.start) {
|
if (action === Container.start) {
|
||||||
Container.get({id: c.Id}, function (d) {
|
Container.get({id: c.Id}, function (d) {
|
||||||
c = d;
|
c = d;
|
||||||
counter = counter + 1;
|
|
||||||
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
|
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
|
||||||
Messages.send("Container " + msg, c.Id);
|
Messages.send("Container " + msg, c.Id);
|
||||||
var index = $scope.containers.indexOf(c);
|
|
||||||
complete();
|
complete();
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
Messages.error("Failure", e.data);
|
Messages.error("Failure", e.data);
|
||||||
|
@ -63,11 +61,24 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) {
|
||||||
complete();
|
complete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (action === Container.remove) {
|
||||||
|
action({id: c.Id}, function (d) {
|
||||||
|
var error = errorMsgFilter(d);
|
||||||
|
if (error) {
|
||||||
|
Messages.send("Error", "Unable to remove running container");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Messages.send("Container " + msg, c.Id);
|
||||||
|
}
|
||||||
|
complete();
|
||||||
|
}, function (e) {
|
||||||
|
Messages.error("Failure", e.data);
|
||||||
|
complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
counter = counter + 1;
|
|
||||||
action({id: c.Id}, function (d) {
|
action({id: c.Id}, function (d) {
|
||||||
Messages.send("Container " + msg, c.Id);
|
Messages.send("Container " + msg, c.Id);
|
||||||
var index = $scope.containers.indexOf(c);
|
|
||||||
complete();
|
complete();
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
Messages.error("Failure", e.data);
|
Messages.error("Failure", e.data);
|
||||||
|
@ -75,7 +86,6 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) {
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (counter === 0) {
|
if (counter === 0) {
|
||||||
|
@ -91,18 +101,6 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.toggleSelectAll = function () {
|
|
||||||
$scope.state.selectedItem = $scope.state.toggle;
|
|
||||||
angular.forEach($scope.state.filteredContainers, function (i) {
|
|
||||||
i.Checked = $scope.state.toggle;
|
|
||||||
});
|
|
||||||
if ($scope.state.toggle) {
|
|
||||||
$scope.state.selectedItemCount = $scope.state.filteredContainers.length;
|
|
||||||
} else {
|
|
||||||
$scope.state.selectedItemCount = 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.toggleGetAll = function () {
|
$scope.toggleGetAll = function () {
|
||||||
Settings.displayAll = $scope.state.displayAll;
|
Settings.displayAll = $scope.state.displayAll;
|
||||||
update({all: Settings.displayAll ? 1 : 0});
|
update({all: Settings.displayAll ? 1 : 0});
|
||||||
|
@ -151,9 +149,10 @@ function ($scope, Container, Settings, Messages, ViewSpinner, Config) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var hiddenLabels;
|
$scope.swarm = false;
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
hiddenLabels = c.hiddenLabels;
|
hiddenLabels = c.hiddenLabels;
|
||||||
|
$scope.swarm = c.swarm;
|
||||||
update({all: Settings.displayAll ? 1 : 0});
|
update({all: Settings.displayAll ? 1 : 0});
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,213 @@
|
||||||
|
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) {
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
alwaysPull: true
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.formValues = {
|
||||||
|
Console: 'none',
|
||||||
|
Volumes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.config = {
|
||||||
|
Env: [],
|
||||||
|
HostConfig: {
|
||||||
|
RestartPolicy: {
|
||||||
|
Name: 'no'
|
||||||
|
},
|
||||||
|
PortBindings: [],
|
||||||
|
Binds: [],
|
||||||
|
NetworkMode: 'bridge',
|
||||||
|
Privileged: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.resetVolumePath = function(index) {
|
||||||
|
$scope.formValues.Volumes[index].name = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addVolume = function() {
|
||||||
|
$scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, isPath: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeVolume = function(index) {
|
||||||
|
$scope.formValues.Volumes.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addEnvironmentVariable = function() {
|
||||||
|
$scope.config.Env.push({ name: '', value: ''});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeEnvironmentVariable = function(index) {
|
||||||
|
$scope.config.Env.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addPortBinding = function() {
|
||||||
|
$scope.config.HostConfig.PortBindings.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removePortBinding = function(index) {
|
||||||
|
$scope.config.HostConfig.PortBindings.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
Config.$promise.then(function (c) {
|
||||||
|
var swarm = c.swarm;
|
||||||
|
|
||||||
|
Volume.query({}, function (d) {
|
||||||
|
$scope.availableVolumes = d.Volumes;
|
||||||
|
}, function (e) {
|
||||||
|
Messages.error("Failure", e.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
Network.query({}, function (d) {
|
||||||
|
var networks = d;
|
||||||
|
if (swarm) {
|
||||||
|
networks = d.filter(function (network) {
|
||||||
|
if (network.Scope === 'global') {
|
||||||
|
return network;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
networks.push({Name: "bridge"});
|
||||||
|
networks.push({Name: "host"});
|
||||||
|
networks.push({Name: "none"});
|
||||||
|
}
|
||||||
|
$scope.availableNetworks = networks;
|
||||||
|
}, function (e) {
|
||||||
|
Messages.error("Failure", e.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createContainer(config) {
|
||||||
|
ViewSpinner.spin();
|
||||||
|
Container.create(config, function (d) {
|
||||||
|
if (d.Id) {
|
||||||
|
var reqBody = config.HostConfig || {};
|
||||||
|
reqBody.id = d.Id;
|
||||||
|
Container.start(reqBody, function (cd) {
|
||||||
|
ViewSpinner.stop();
|
||||||
|
Messages.send('Container Started', d.Id);
|
||||||
|
$state.go('containers', {}, {reload: true});
|
||||||
|
}, function (e) {
|
||||||
|
ViewSpinner.stop();
|
||||||
|
Messages.error('Error', errorMsgFilter(e));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ViewSpinner.stop();
|
||||||
|
Messages.error('Error', errorMsgFilter(d));
|
||||||
|
}
|
||||||
|
}, function (e) {
|
||||||
|
ViewSpinner.stop();
|
||||||
|
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) {
|
||||||
|
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||||
|
if (err) {
|
||||||
|
var detail = data[data.length - 1];
|
||||||
|
ViewSpinner.stop();
|
||||||
|
Messages.error('Error', detail.error);
|
||||||
|
} else {
|
||||||
|
createContainer(config);
|
||||||
|
}
|
||||||
|
}, function (e) {
|
||||||
|
ViewSpinner.stop();
|
||||||
|
Messages.error('Error', 'Unable to pull image ' + image);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparePortBindings(config) {
|
||||||
|
var bindings = {};
|
||||||
|
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
||||||
|
if (portBinding.hostPort && portBinding.containerPort) {
|
||||||
|
var key = portBinding.containerPort + "/" + portBinding.protocol;
|
||||||
|
bindings[key] = [{ HostPort: portBinding.hostPort }];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.HostConfig.PortBindings = bindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareConsole(config) {
|
||||||
|
var value = $scope.formValues.Console;
|
||||||
|
var openStdin = true;
|
||||||
|
var tty = true;
|
||||||
|
if (value === 'tty') {
|
||||||
|
openStdin = false;
|
||||||
|
} else if (value === 'interactive') {
|
||||||
|
tty = false;
|
||||||
|
} else if (value === 'none') {
|
||||||
|
openStdin = false;
|
||||||
|
tty = false;
|
||||||
|
}
|
||||||
|
config.OpenStdin = openStdin;
|
||||||
|
config.Tty = tty;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareEnvironmentVariables(config) {
|
||||||
|
var env = [];
|
||||||
|
config.Env.forEach(function (v) {
|
||||||
|
if (v.name && v.value) {
|
||||||
|
env.push(v.name + "=" + v.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.Env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareVolumes(config) {
|
||||||
|
var binds = [];
|
||||||
|
var volumes = {};
|
||||||
|
|
||||||
|
$scope.formValues.Volumes.forEach(function (volume) {
|
||||||
|
var name = volume.name;
|
||||||
|
var containerPath = volume.containerPath;
|
||||||
|
if (name && containerPath) {
|
||||||
|
var bind = name + ':' + containerPath;
|
||||||
|
volumes[containerPath] = {};
|
||||||
|
if (volume.readOnly) {
|
||||||
|
bind += ':ro';
|
||||||
|
}
|
||||||
|
binds.push(bind);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.HostConfig.Binds = binds;
|
||||||
|
config.Volumes = volumes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareConfiguration() {
|
||||||
|
var config = angular.copy($scope.config);
|
||||||
|
preparePortBindings(config);
|
||||||
|
prepareConsole(config);
|
||||||
|
prepareEnvironmentVariables(config);
|
||||||
|
prepareVolumes(config);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.create = function () {
|
||||||
|
var config = prepareConfiguration();
|
||||||
|
console.log(JSON.stringify(config, null, 4));
|
||||||
|
|
||||||
|
if ($scope.state.alwaysPull) {
|
||||||
|
pullImageAndCreateContainer(config);
|
||||||
|
} else {
|
||||||
|
createContainer(config);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}]);
|
|
@ -0,0 +1,312 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Create container"></rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
Containers > Add container
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<!-- name-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_name" class="col-sm-1 control-label text-left">Name</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<input type="text" class="form-control" ng-model="config.name" id="container_name" placeholder="e.g. myContainer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input -->
|
||||||
|
<!-- image input -->
|
||||||
|
<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>
|
||||||
|
<div class="col-sm-offset-1 col-sm-11">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="state.alwaysPull"> Always pull image before creating
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !image-input -->
|
||||||
|
<!-- restart-policy -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-1 control-label text-left">Restart policy</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<label class="radio-inline">
|
||||||
|
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="no">
|
||||||
|
Never
|
||||||
|
</label>
|
||||||
|
<label class="radio-inline">
|
||||||
|
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="always">
|
||||||
|
Always
|
||||||
|
</label>
|
||||||
|
<label class="radio-inline">
|
||||||
|
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure">
|
||||||
|
<span class="radio-value">On failure</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !restart-policy -->
|
||||||
|
<!-- port-mapping -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<span class="label label-default clickable" ng-click="addPortBinding()">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- port-mapping-input-list -->
|
||||||
|
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||||
|
<div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px;">
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">host</span>
|
||||||
|
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80">
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">container</span>
|
||||||
|
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
|
<select class="selectpicker form-control" ng-model="portBinding.protocol">
|
||||||
|
<option value="tcp">tcp</option>
|
||||||
|
<option value="udp">udp</option>
|
||||||
|
</select>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
|
||||||
|
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !port-mapping-input-list -->
|
||||||
|
</div>
|
||||||
|
<!-- !port-mapping -->
|
||||||
|
</form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body>
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="active clickable"><a data-target="#command" data-toggle="tab">Command</a></li>
|
||||||
|
<li class="clickable"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||||
|
<li class="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||||
|
<li class="clickable"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- tab-content -->
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- tab-command -->
|
||||||
|
<div class="tab-pane active" id="command">
|
||||||
|
<form class="form-horizontal" style="margin-top: 15px;">
|
||||||
|
<!-- command-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_command" class="col-sm-1 control-label text-left">Command</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !command-input -->
|
||||||
|
<!-- entrypoint-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_entrypoint" class="col-sm-1 control-label text-left">Entry Point</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="text" class="form-control" ng-model="config.Entrypoint" id="container_entrypoint" placeholder="e.g. /bin/sh -c">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !entrypoint-input -->
|
||||||
|
<!-- workdir-user-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_workingdir" class="col-sm-1 control-label text-left">Working Dir</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" class="form-control" ng-model="config.WorkingDir" id="container_workingdir" placeholder="e.g. /myapp">
|
||||||
|
</div>
|
||||||
|
<label for="container_user" class="col-sm-1 control-label text-left">User</label>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<input type="text" class="form-control" ng-model="config.User" id="container_user" placeholder="e.g. nginx">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !workdir-user-input -->
|
||||||
|
<!-- console -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_console" class="col-sm-1 control-label text-left">Console</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<label class="radio-inline">
|
||||||
|
<input type="radio" name="container_console" ng-model="formValues.Console" value="both">
|
||||||
|
Interactive & TTY <span class="small text-muted">(-i -t)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<label class="radio-inline">
|
||||||
|
<input type="radio" name="container_console" ng-model="formValues.Console" value="interactive">
|
||||||
|
Interactive <span class="small text-muted">(-i)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-offset-1 col-sm-11">
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<label class="radio-inline">
|
||||||
|
<input type="radio" name="container_console" ng-model="formValues.Console" value="tty">
|
||||||
|
TTY <span class="small text-muted">(-t)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<label class="radio-inline">
|
||||||
|
<input type="radio" name="container_console" ng-model="formValues.Console" value="none">
|
||||||
|
None
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !console -->
|
||||||
|
<!-- environment-variables -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<span class="label label-default clickable" ng-click="addEnvironmentVariable()">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- environment-variable-input-list -->
|
||||||
|
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||||
|
<div ng-repeat="variable in config.Env" style="margin-top: 2px;">
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">name</span>
|
||||||
|
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">value</span>
|
||||||
|
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
|
||||||
|
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !environment-variable-input-list -->
|
||||||
|
</div>
|
||||||
|
<!-- !environment-variables -->
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- !tab-command -->
|
||||||
|
<!-- tab-volume -->
|
||||||
|
<div class="tab-pane" id="volumes">
|
||||||
|
<form class="form-horizontal" style="margin-top: 15px;">
|
||||||
|
<!-- volumes -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
|
||||||
|
<div class="col-sm-11">
|
||||||
|
<span class="label label-default clickable" ng-click="addVolume()">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- volumes-input-list -->
|
||||||
|
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||||
|
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;">
|
||||||
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="volume.readOnly"> Read-only
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon"><input type="checkbox" ng-model="volume.isPath" ng-click="resetVolumePath($index)">Path</span>
|
||||||
|
<select class="selectpicker form-control" ng-model="volume.name" ng-if="!volume.isPath">
|
||||||
|
<option selected disabled hidden value="">Select a volume</option>
|
||||||
|
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||||
|
</select>
|
||||||
|
<input ng-if="volume.isPath" type="text" class="form-control" ng-model="volume.name" placeholder="e.g. /path/on/host">
|
||||||
|
</div>
|
||||||
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
|
<span class="input-group-addon">container</span>
|
||||||
|
<input type="text" class="form-control" ng-model="volume.containerPath" placeholder="e.g. /path/in/container">
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-default" type="button" ng-click="removeVolume($index)">
|
||||||
|
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !volumes-input-list -->
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<!-- !volumes -->
|
||||||
|
</div>
|
||||||
|
<!-- !tab-volume -->
|
||||||
|
<!-- tab-network -->
|
||||||
|
<div class="tab-pane" id="network">
|
||||||
|
<form class="form-horizontal" style="margin-top: 15px;">
|
||||||
|
<!-- network-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select class="selectpicker form-control" ng-model="config.HostConfig.NetworkMode">
|
||||||
|
<option selected disabled hidden value="">Select a network</option>
|
||||||
|
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !network-input -->
|
||||||
|
<!-- hostname-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_hostname" class="col-sm-1 control-label text-left">Hostname</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="text" class="form-control" ng-model="config.Hostname" id="container_hostname" placeholder="e.g. web01">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !hostname-input -->
|
||||||
|
<!-- domainname-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_domainname" class="col-sm-1 control-label text-left">Domain Name</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="text" class="form-control" ng-model="config.Domainname" id="container_domainname" placeholder="e.g. example.com">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !domainname -->
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- !tab-network -->
|
||||||
|
<!-- tab-security -->
|
||||||
|
<div class="tab-pane" id="security">
|
||||||
|
<form class="form-horizontal" style="margin-top: 15px;">
|
||||||
|
<!-- privileged-mode -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="config.HostConfig.Privileged"> Privileged mode
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !privileged-mode -->
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- !tab-security -->
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||||
|
<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>
|
||||||
|
</div>
|
|
@ -1,163 +0,0 @@
|
||||||
angular.module('startContainer', ['ui.bootstrap'])
|
|
||||||
.controller('StartContainerController', ['$scope', '$state', 'Container', 'Messages', 'containernameFilter', 'errorMsgFilter', 'ViewSpinner',
|
|
||||||
function ($scope, $state, Container, Messages, containernameFilter, errorMsgFilter, ViewSpinner) {
|
|
||||||
$scope.template = 'app/components/startContainer/startcontainer.html';
|
|
||||||
|
|
||||||
Container.query({all: 1}, function (d) {
|
|
||||||
$scope.containerNames = d.map(function (container) {
|
|
||||||
return containernameFilter(container);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.config = {
|
|
||||||
Env: [],
|
|
||||||
Labels: [],
|
|
||||||
Volumes: [],
|
|
||||||
SecurityOpts: [],
|
|
||||||
HostConfig: {
|
|
||||||
PortBindings: [],
|
|
||||||
Binds: [],
|
|
||||||
Links: [],
|
|
||||||
Dns: [],
|
|
||||||
DnsSearch: [],
|
|
||||||
VolumesFrom: [],
|
|
||||||
CapAdd: [],
|
|
||||||
CapDrop: [],
|
|
||||||
Devices: [],
|
|
||||||
LxcConf: [],
|
|
||||||
ExtraHosts: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.menuStatus = {
|
|
||||||
containerOpen: true,
|
|
||||||
hostConfigOpen: false
|
|
||||||
};
|
|
||||||
|
|
||||||
function failedRequestHandler(e, Messages) {
|
|
||||||
Messages.error('Error', errorMsgFilter(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
function rmEmptyKeys(col) {
|
|
||||||
for (var key in col) {
|
|
||||||
if (col[key] === null || col[key] === undefined || col[key] === '' || ($.isPlainObject(col[key]) && $.isEmptyObject(col[key])) || col[key].length === 0) {
|
|
||||||
delete col[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNames(arr) {
|
|
||||||
return arr.map(function (item) {
|
|
||||||
return item.name;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.create = function () {
|
|
||||||
// Copy the config before transforming fields to the remote API format
|
|
||||||
$('#create-modal').modal('hide');
|
|
||||||
ViewSpinner.spin();
|
|
||||||
|
|
||||||
var config = angular.copy($scope.config);
|
|
||||||
|
|
||||||
if (config.Cmd && config.Cmd[0] === "[") {
|
|
||||||
config.Cmd = angular.fromJson(config.Cmd);
|
|
||||||
} else if (config.Cmd) {
|
|
||||||
config.Cmd = config.Cmd.split(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Env = config.Env.map(function (envar) {
|
|
||||||
return envar.name + '=' + envar.value;
|
|
||||||
});
|
|
||||||
var labels = {};
|
|
||||||
config.Labels = config.Labels.forEach(function(label) {
|
|
||||||
labels[label.key] = label.value;
|
|
||||||
});
|
|
||||||
config.Labels = labels;
|
|
||||||
|
|
||||||
config.Volumes = getNames(config.Volumes);
|
|
||||||
config.SecurityOpts = getNames(config.SecurityOpts);
|
|
||||||
|
|
||||||
config.HostConfig.VolumesFrom = getNames(config.HostConfig.VolumesFrom);
|
|
||||||
config.HostConfig.Binds = getNames(config.HostConfig.Binds);
|
|
||||||
config.HostConfig.Links = getNames(config.HostConfig.Links);
|
|
||||||
config.HostConfig.Dns = getNames(config.HostConfig.Dns);
|
|
||||||
config.HostConfig.DnsSearch = getNames(config.HostConfig.DnsSearch);
|
|
||||||
config.HostConfig.CapAdd = getNames(config.HostConfig.CapAdd);
|
|
||||||
config.HostConfig.CapDrop = getNames(config.HostConfig.CapDrop);
|
|
||||||
config.HostConfig.LxcConf = config.HostConfig.LxcConf.reduce(function (prev, cur, idx) {
|
|
||||||
prev[cur.name] = cur.value;
|
|
||||||
return prev;
|
|
||||||
}, {});
|
|
||||||
config.HostConfig.ExtraHosts = config.HostConfig.ExtraHosts.map(function (entry) {
|
|
||||||
return entry.host + ':' + entry.ip;
|
|
||||||
});
|
|
||||||
|
|
||||||
var ExposedPorts = {};
|
|
||||||
var PortBindings = {};
|
|
||||||
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
|
||||||
var intPort = portBinding.intPort + "/tcp";
|
|
||||||
if (portBinding.protocol === "udp") {
|
|
||||||
intPort = portBinding.intPort + "/udp";
|
|
||||||
}
|
|
||||||
var binding = {
|
|
||||||
HostIp: portBinding.ip,
|
|
||||||
HostPort: portBinding.extPort
|
|
||||||
};
|
|
||||||
if (portBinding.intPort) {
|
|
||||||
ExposedPorts[intPort] = {};
|
|
||||||
if (intPort in PortBindings) {
|
|
||||||
PortBindings[intPort].push(binding);
|
|
||||||
} else {
|
|
||||||
PortBindings[intPort] = [binding];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Messages.send('Warning', 'Internal port must be specified for PortBindings');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
config.ExposedPorts = ExposedPorts;
|
|
||||||
config.HostConfig.PortBindings = PortBindings;
|
|
||||||
|
|
||||||
// Remove empty fields from the request to avoid overriding defaults
|
|
||||||
rmEmptyKeys(config.HostConfig);
|
|
||||||
rmEmptyKeys(config);
|
|
||||||
|
|
||||||
var ctor = Container;
|
|
||||||
var s = $scope;
|
|
||||||
Container.create(config, function (d) {
|
|
||||||
if (d.Id) {
|
|
||||||
var reqBody = config.HostConfig || {};
|
|
||||||
reqBody.id = d.Id;
|
|
||||||
ctor.start(reqBody, function (cd) {
|
|
||||||
if (cd.id) {
|
|
||||||
ViewSpinner.stop();
|
|
||||||
Messages.send('Container Started', d.Id);
|
|
||||||
$state.go('container', {id: d.Id}, {reload: true});
|
|
||||||
} else {
|
|
||||||
ViewSpinner.stop();
|
|
||||||
failedRequestHandler(cd, Messages);
|
|
||||||
ctor.remove({id: d.Id}, function () {
|
|
||||||
Messages.send('Container Removed', d.Id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, function (e) {
|
|
||||||
ViewSpinner.stop();
|
|
||||||
failedRequestHandler(e, Messages);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ViewSpinner.stop();
|
|
||||||
failedRequestHandler(d, Messages);
|
|
||||||
}
|
|
||||||
}, function (e) {
|
|
||||||
ViewSpinner.stop();
|
|
||||||
failedRequestHandler(e, Messages);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.addEntry = function (array, entry) {
|
|
||||||
array.push(entry);
|
|
||||||
};
|
|
||||||
$scope.rmEntry = function (array, entry) {
|
|
||||||
var idx = array.indexOf(entry);
|
|
||||||
array.splice(idx, 1);
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -1,444 +0,0 @@
|
||||||
<div id="create-modal" class="modal fade">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
|
||||||
<h3>Start a new container</h3>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form role="form">
|
|
||||||
<accordion close-others="true">
|
|
||||||
<accordion-group heading="Container options" is-open="menuStatus.containerOpen">
|
|
||||||
<fieldset>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Image:</label>
|
|
||||||
<input type="text" placeholder='ubuntu:latest'
|
|
||||||
ng-model="config.Image" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Cmd:</label>
|
|
||||||
<input type="text" placeholder='["/bin/echo", "Hello world"]'
|
|
||||||
ng-model="config.Cmd" class="form-control"/>
|
|
||||||
<small>Input commands as a raw string or JSON array</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Entrypoint:</label>
|
|
||||||
<input type="text" ng-model="config.Entrypoint" class="form-control"
|
|
||||||
placeholder="./entrypoint.sh"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Name:</label>
|
|
||||||
<input type="text" ng-model="config.name" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Hostname:</label>
|
|
||||||
<input type="text" ng-model="config.Hostname" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Domainname:</label>
|
|
||||||
<input type="text" ng-model="config.Domainname" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>User:</label>
|
|
||||||
<input type="text" ng-model="config.User" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Memory:</label>
|
|
||||||
<input type="number" ng-model="config.Memory" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Volumes:</label>
|
|
||||||
<div ng-repeat="volume in config.Volumes">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<input type="text" ng-model="volume.name" class="form-control"
|
|
||||||
placeholder="/var/data"/>
|
|
||||||
<a href="" ng-click="rmEntry(config.Volumes, volume)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.Volumes, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>MemorySwap:</label>
|
|
||||||
<input type="number" ng-model="config.MemorySwap" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>CpuShares:</label>
|
|
||||||
<input type="number" ng-model="config.CpuShares" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Cpuset:</label>
|
|
||||||
<input type="text" ng-model="config.Cpuset" class="form-control"
|
|
||||||
placeholder="1,2"/>
|
|
||||||
<small>Input as comma-separated list of numbers</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>WorkingDir:</label>
|
|
||||||
<input type="text" ng-model="config.WorkingDir" class="form-control"
|
|
||||||
placeholder="/app"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>MacAddress:</label>
|
|
||||||
<input type="text" ng-model="config.MacAddress" class="form-control"
|
|
||||||
placeholder="12:34:56:78:9a:bc"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="networkDisabled">NetworkDisabled:</label>
|
|
||||||
<input id="networkDisabled" type="checkbox"
|
|
||||||
ng-model="config.NetworkDisabled"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="tty">Tty:</label>
|
|
||||||
<input id="tty" type="checkbox" ng-model="config.Tty"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="openStdin">OpenStdin:</label>
|
|
||||||
<input id="openStdin" type="checkbox" ng-model="config.OpenStdin"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="stdinOnce">StdinOnce:</label>
|
|
||||||
<input id="stdinOnce" type="checkbox" ng-model="config.StdinOnce"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>SecurityOpts:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="opt in config.SecurityOpts">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<input type="text" ng-model="opt.name" class="form-control"
|
|
||||||
placeholder="label:type:svirt_apache"/>
|
|
||||||
<a href="" ng-click="rmEntry(config.SecurityOpts, opt)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.SecurityOpts, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Env:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="envar in config.Env">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="sr-only">Variable Name:</label>
|
|
||||||
<input type="text" ng-model="envar.name" class="form-control"
|
|
||||||
placeholder="NAME"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="sr-only">Variable Value:</label>
|
|
||||||
<input type="text" ng-model="envar.value" class="form-control"
|
|
||||||
placeholder="value"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<a href="" ng-click="rmEntry(config.Env, envar)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.Env, {name: '', value: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Labels:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="label in config.Labels">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="sr-only">Key:</label>
|
|
||||||
<input type="text" ng-model="label.key" class="form-control"
|
|
||||||
placeholder="key"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="sr-only">Value:</label>
|
|
||||||
<input type="text" ng-model="label.value" class="form-control"
|
|
||||||
placeholder="value"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<a href="" ng-click="rmEntry(config.Labels, label)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.Labels, {key: '', value: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</accordion-group>
|
|
||||||
<accordion-group heading="HostConfig options" is-open="menuStatus.hostConfigOpen">
|
|
||||||
<fieldset>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Binds:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="bind in config.HostConfig.Binds">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<input type="text" ng-model="bind.name" class="form-control"
|
|
||||||
placeholder="/host:/container"/>
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.Binds, bind)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.Binds, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Links:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="link in config.HostConfig.Links">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<input type="text" ng-model="link.name" class="form-control"
|
|
||||||
placeholder="web:db">
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.Links, link)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.Links, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Dns:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="entry in config.HostConfig.Dns">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<input type="text" ng-model="entry.name" class="form-control"
|
|
||||||
placeholder="8.8.8.8"/>
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.Dns, entry)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.Dns, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>DnsSearch:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="entry in config.HostConfig.DnsSearch">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<input type="text" ng-model="entry.name" class="form-control"
|
|
||||||
placeholder="example.com"/>
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.DnsSearch, entry)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.DnsSearch, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>CapAdd:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="entry in config.HostConfig.CapAdd">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<input type="text" ng-model="entry.name" class="form-control"
|
|
||||||
placeholder="cap_sys_admin"/>
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.CapAdd, entry)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.CapAdd, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>CapDrop:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="entry in config.HostConfig.CapDrop">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<input type="text" ng-model="entry.name" class="form-control"
|
|
||||||
placeholder="cap_sys_admin"/>
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.CapDrop, entry)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.CapDrop, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>NetworkMode:</label>
|
|
||||||
<input type="text" ng-model="config.HostConfig.NetworkMode"
|
|
||||||
class="form-control" placeholder="bridge"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="publishAllPorts">PublishAllPorts:</label>
|
|
||||||
<input id="publishAllPorts" type="checkbox"
|
|
||||||
ng-model="config.HostConfig.PublishAllPorts"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="privileged">Privileged:</label>
|
|
||||||
<input id="privileged" type="checkbox"
|
|
||||||
ng-model="config.HostConfig.Privileged"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>VolumesFrom:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="volume in config.HostConfig.VolumesFrom">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<select ng-model="volume.name"
|
|
||||||
ng-options="name for name in containerNames track by name"
|
|
||||||
class="form-control">
|
|
||||||
</select>
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.VolumesFrom, volume)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.VolumesFrom, {name: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>RestartPolicy:</label>
|
|
||||||
<select ng-model="config.HostConfig.RestartPolicy.name">
|
|
||||||
<option value="">disabled</option>
|
|
||||||
<option value="always">always</option>
|
|
||||||
<option value="on-failure">on-failure</option>
|
|
||||||
</select>
|
|
||||||
<label>MaximumRetryCount:</label>
|
|
||||||
<input type="number"
|
|
||||||
ng-model="config.HostConfig.RestartPolicy.MaximumRetryCount"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>ExtraHosts:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="entry in config.HostConfig.ExtraHosts">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="sr-only">Hostname:</label>
|
|
||||||
<input type="text" ng-model="entry.host" class="form-control"
|
|
||||||
placeholder="hostname"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="sr-only">IP Address:</label>
|
|
||||||
<input type="text" ng-model="entry.ip" class="form-control"
|
|
||||||
placeholder="127.0.0.1"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.ExtraHosts, entry)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.ExtraHosts, {host: '', ip: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>LxcConf:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="entry in config.HostConfig.LxcConf">
|
|
||||||
<div class="form-group form-inline">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="sr-only">Name:</label>
|
|
||||||
<input type="text" ng-model="entry.name" class="form-control"
|
|
||||||
placeholder="lxc.utsname"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="sr-only">Value:</label>
|
|
||||||
<input type="text" ng-model="entry.value" class="form-control"
|
|
||||||
placeholder="docker"/>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.LxcConf, entry)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.LxcConf, {name: '', value: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Devices:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="device in config.HostConfig.Devices">
|
|
||||||
<div class="form-group form-inline inline-four">
|
|
||||||
<label class="sr-only">PathOnHost:</label>
|
|
||||||
<input type="text" ng-model="device.PathOnHost" class="form-control"
|
|
||||||
placeholder="PathOnHost"/>
|
|
||||||
<label class="sr-only">PathInContainer:</label>
|
|
||||||
<input type="text" ng-model="device.PathInContainer" class="form-control"
|
|
||||||
placeholder="PathInContainer"/>
|
|
||||||
<label class="sr-only">CgroupPermissions:</label>
|
|
||||||
<input type="text" ng-model="device.CgroupPermissions" class="form-control"
|
|
||||||
placeholder="CgroupPermissions"/>
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.Devices, device)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.Devices, { PathOnHost: '', PathInContainer: '', CgroupPermissions: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>PortBindings:</label>
|
|
||||||
|
|
||||||
<div ng-repeat="portBinding in config.HostConfig.PortBindings">
|
|
||||||
<div class="form-group form-inline inline-four">
|
|
||||||
<label class="sr-only">Host IP:</label>
|
|
||||||
<input type="text" ng-model="portBinding.ip" class="form-control"
|
|
||||||
placeholder="Host IP Address"/>
|
|
||||||
<label class="sr-only">Host Port:</label>
|
|
||||||
<input type="text" ng-model="portBinding.extPort" class="form-control"
|
|
||||||
placeholder="Host Port"/>
|
|
||||||
<label class="sr-only">Container port:</label>
|
|
||||||
<input type="text" ng-model="portBinding.intPort" class="form-control"
|
|
||||||
placeholder="Container Port"/>
|
|
||||||
<select ng-model="portBinding.protocol">
|
|
||||||
<option value="">tcp</option>
|
|
||||||
<option value="udp">udp</option>
|
|
||||||
</select>
|
|
||||||
<a href="" ng-click="rmEntry(config.HostConfig.PortBindings, portBinding)">
|
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="" ng-click="addEntry(config.HostConfig.PortBindings, {ip: '', extPort: '', intPort: ''})">
|
|
||||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</accordion-group>
|
|
||||||
</accordion>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<a href="" class="btn btn-primary btn-lg" ng-click="create()">Create</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -137,7 +137,7 @@
|
||||||
<td>{{ node.ip }}</td>
|
<td>{{ node.ip }}</td>
|
||||||
<td>{{ node.containers }}</td>
|
<td>{{ node.containers }}</td>
|
||||||
<td>{{ node.version }}</td>
|
<td>{{ node.version }}</td>
|
||||||
<td><span class="label label-{{ node.status|statusbadge }}">{{ node.status }}</span></td>
|
<td><span class="label label-{{ node.status|nodestatusbadge }}">{{ node.status }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -52,11 +52,10 @@ angular.module('swarm', [])
|
||||||
node.ip = info[offset][1];
|
node.ip = info[offset][1];
|
||||||
node.id = info[offset + 1][1];
|
node.id = info[offset + 1][1];
|
||||||
node.status = info[offset + 2][1];
|
node.status = info[offset + 2][1];
|
||||||
node.containers = info[offset + 2][1];
|
node.containers = info[offset + 3][1];
|
||||||
node.cpu = info[offset + 3][1];
|
node.cpu = info[offset + 4][1];
|
||||||
node.memory = info[offset + 4][1];
|
node.memory = info[offset + 5][1];
|
||||||
node.labels = info[offset + 5][1];
|
node.labels = info[offset + 6][1];
|
||||||
node.error = info[offset + 6][1];
|
|
||||||
node.version = info[offset + 8][1];
|
node.version = info[offset + 8][1];
|
||||||
$scope.swarm.Status.push(node);
|
$scope.swarm.Status.push(node);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
angular.module('dockerui.filters', [])
|
angular.module('dockerui.filters', [])
|
||||||
.filter('truncate', function () {
|
.filter('truncate', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (text, length, end) {
|
return function (text, length, end) {
|
||||||
if (isNaN(length)) {
|
if (isNaN(length)) {
|
||||||
|
@ -17,21 +17,30 @@ angular.module('dockerui.filters', [])
|
||||||
return String(text).substring(0, length - end.length) + end;
|
return String(text).substring(0, length - end.length) + end;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('statusbadge', function () {
|
.filter('containerstatusbadge', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (text) {
|
return function (text) {
|
||||||
if (text === 'Ghost') {
|
if (text === 'paused') {
|
||||||
return 'important';
|
|
||||||
} else if (text === 'Unhealthy') {
|
|
||||||
return 'danger';
|
|
||||||
} else if (text.indexOf('Exit') !== -1 && text !== 'Exit 0') {
|
|
||||||
return 'warning';
|
return 'warning';
|
||||||
|
} else if (text === 'created') {
|
||||||
|
return 'info';
|
||||||
|
} else if (text === 'exited') {
|
||||||
|
return 'danger';
|
||||||
}
|
}
|
||||||
return 'success';
|
return 'success';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('trimcontainername', function () {
|
.filter('nodestatusbadge', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (text) {
|
||||||
|
if (text === 'Unhealthy') {
|
||||||
|
return 'danger';
|
||||||
|
}
|
||||||
|
return 'success';
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter('trimcontainername', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (name) {
|
return function (name) {
|
||||||
if (name) {
|
if (name) {
|
||||||
|
@ -39,8 +48,14 @@ angular.module('dockerui.filters', [])
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('getstatetext', function () {
|
.filter('capitalize', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (text) {
|
||||||
|
return _.capitalize(text);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter('getstatetext', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (state) {
|
return function (state) {
|
||||||
if (state === undefined) {
|
if (state === undefined) {
|
||||||
|
@ -57,8 +72,8 @@ angular.module('dockerui.filters', [])
|
||||||
}
|
}
|
||||||
return 'Stopped';
|
return 'Stopped';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('getstatelabel', function () {
|
.filter('getstatelabel', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (state) {
|
return function (state) {
|
||||||
if (state === undefined) {
|
if (state === undefined) {
|
||||||
|
@ -73,8 +88,8 @@ angular.module('dockerui.filters', [])
|
||||||
}
|
}
|
||||||
return 'label-default';
|
return 'label-default';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('humansize', function () {
|
.filter('humansize', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (bytes) {
|
return function (bytes) {
|
||||||
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
@ -86,15 +101,27 @@ angular.module('dockerui.filters', [])
|
||||||
var decimalPlaces = (i < 1) ? 0 : (i - 1);
|
var decimalPlaces = (i < 1) ? 0 : (i - 1);
|
||||||
return value.toFixed(decimalPlaces) + ' ' + sizes[[i]];
|
return value.toFixed(decimalPlaces) + ' ' + sizes[[i]];
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('containername', function () {
|
.filter('containername', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (container) {
|
return function (container) {
|
||||||
var name = container.Names[0];
|
var name = container.Names[0];
|
||||||
return name.substring(1, name.length);
|
return name.substring(1, name.length);
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('repotag', function () {
|
.filter('swarmcontainername', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (container) {
|
||||||
|
return _.split(container.Names[0], '/')[2];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter('swarmhostname', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (container) {
|
||||||
|
return _.split(container.Names[0], '/')[1];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter('repotag', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (image) {
|
return function (image) {
|
||||||
if (image.RepoTags && image.RepoTags.length > 0) {
|
if (image.RepoTags && image.RepoTags.length > 0) {
|
||||||
|
@ -106,16 +133,16 @@ angular.module('dockerui.filters', [])
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('getdate', function () {
|
.filter('getdate', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (data) {
|
return function (data) {
|
||||||
//Multiply by 1000 for the unix format
|
//Multiply by 1000 for the unix format
|
||||||
var date = new Date(data * 1000);
|
var date = new Date(data * 1000);
|
||||||
return date.toDateString();
|
return date.toDateString();
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter('errorMsg', function () {
|
.filter('errorMsg', function () {
|
||||||
return function (object) {
|
return function (object) {
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
var msg = '';
|
var msg = '';
|
||||||
|
@ -125,4 +152,4 @@ angular.module('dockerui.filters', [])
|
||||||
}
|
}
|
||||||
return msg;
|
return msg;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -86,10 +86,10 @@ angular.module('dockerui.services', ['ngResource', 'ngSanitize'])
|
||||||
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
||||||
create: {
|
create: {
|
||||||
method: 'POST', isArray: true, transformResponse: [function f(data) {
|
method: 'POST', isArray: true, transformResponse: [function f(data) {
|
||||||
var str = data.replace(/\n/g, " ").replace(/\}\W*\{/g, "}, {");
|
var str = "[" + data.replace(/\n/g, " ").replace(/\}\s*\{/g, "}, {") + "]";
|
||||||
return angular.fromJson("[" + str + "]");
|
return angular.fromJson(str);
|
||||||
}],
|
}],
|
||||||
params: {action: 'create', fromImage: '@fromImage', repo: '@repo', tag: '@tag', registry: '@registry'}
|
params: {action: 'create', fromImage: '@fromImage', tag: '@tag'}
|
||||||
},
|
},
|
||||||
insert: {method: 'POST', params: {id: '@id', action: 'insert'}},
|
insert: {method: 'POST', params: {id: '@id', action: 'insert'}},
|
||||||
push: {method: 'POST', params: {id: '@id', action: 'push'}},
|
push: {method: 'POST', params: {id: '@id', action: 'push'}},
|
||||||
|
|
|
@ -10,11 +10,10 @@ function ImageViewModel(data) {
|
||||||
|
|
||||||
function ContainerViewModel(data) {
|
function ContainerViewModel(data) {
|
||||||
this.Id = data.Id;
|
this.Id = data.Id;
|
||||||
|
this.State = data.State;
|
||||||
|
this.Names = data.Names;
|
||||||
|
this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress;
|
||||||
this.Image = data.Image;
|
this.Image = data.Image;
|
||||||
this.Command = data.Command;
|
this.Command = data.Command;
|
||||||
this.Created = data.Created;
|
|
||||||
this.SizeRw = data.SizeRw;
|
|
||||||
this.Status = data.Status;
|
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
this.Names = data.Names;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,3 +146,22 @@
|
||||||
.header_title_content {
|
.header_title_content {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-horizontal .control-label.text-left{
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
margin-top: 1px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="radio"] {
|
||||||
|
margin-top: 1px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "uifordocker",
|
"name": "uifordocker",
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||||
"authors": [
|
"authors": [
|
||||||
"Michael Crosby <crosbymichael@gmail.com>",
|
"Michael Crosby <crosbymichael@gmail.com>",
|
||||||
|
|
12
dockerui.go
12
dockerui.go
|
@ -21,6 +21,7 @@ var (
|
||||||
endpoint = kingpin.Flag("endpoint", "Dockerd endpoint").Default("/var/run/docker.sock").Short('e').String()
|
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()
|
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()
|
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()
|
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'))
|
labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l'))
|
||||||
authKey []byte
|
authKey []byte
|
||||||
|
@ -117,7 +118,7 @@ func createUnixHandler(e string) http.Handler {
|
||||||
return &UnixHandler{e}
|
return &UnixHandler{e}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createHandler(dir string, e string, c Config) http.Handler {
|
func createHandler(dir string, d string, e string, c Config) http.Handler {
|
||||||
var (
|
var (
|
||||||
mux = http.NewServeMux()
|
mux = http.NewServeMux()
|
||||||
fileHandler = http.FileServer(http.Dir(dir))
|
fileHandler = http.FileServer(http.Dir(dir))
|
||||||
|
@ -137,11 +138,12 @@ func createHandler(dir string, e string, c Config) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use existing csrf authKey if present or generate a new one.
|
// Use existing csrf authKey if present or generate a new one.
|
||||||
dat, err := ioutil.ReadFile(authKeyFile)
|
var authKeyPath = d + "/" + authKeyFile
|
||||||
|
dat, err := ioutil.ReadFile(authKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
authKey = securecookie.GenerateRandomKey(32)
|
authKey = securecookie.GenerateRandomKey(32)
|
||||||
err := ioutil.WriteFile(authKeyFile, authKey, 0644)
|
err := ioutil.WriteFile(authKeyPath, authKey, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("unable to persist auth key", err)
|
fmt.Println("unable to persist auth key", err)
|
||||||
}
|
}
|
||||||
|
@ -171,7 +173,7 @@ func csrfWrapper(h http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
kingpin.Version("1.0.4")
|
kingpin.Version("1.1.0")
|
||||||
kingpin.Parse()
|
kingpin.Parse()
|
||||||
|
|
||||||
configuration := Config{
|
configuration := Config{
|
||||||
|
@ -179,7 +181,7 @@ func main() {
|
||||||
HiddenLabels: *labels,
|
HiddenLabels: *labels,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := createHandler(*assets, *endpoint, configuration)
|
handler := createHandler(*assets, *data, *endpoint, configuration)
|
||||||
if err := http.ListenAndServe(*addr, handler); err != nil {
|
if err := http.ListenAndServe(*addr, handler); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -260,14 +260,14 @@ module.exports = function (grunt) {
|
||||||
command: [
|
command: [
|
||||||
'docker stop ui-for-docker',
|
'docker stop ui-for-docker',
|
||||||
'docker rm ui-for-docker',
|
'docker rm ui-for-docker',
|
||||||
'docker run --privileged -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker'
|
'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data -v /var/run/docker.sock:/var/run/docker.sock --name ui-for-docker ui-for-docker -d /data'
|
||||||
].join(';')
|
].join(';')
|
||||||
},
|
},
|
||||||
runSwarm: {
|
runSwarm: {
|
||||||
command: [
|
command: [
|
||||||
'docker stop ui-for-docker',
|
'docker stop ui-for-docker',
|
||||||
'docker rm ui-for-docker',
|
'docker rm ui-for-docker',
|
||||||
'docker run --privileged -d -p 9000:9000 --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 -swarm'
|
'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'
|
||||||
].join(';')
|
].join(';')
|
||||||
},
|
},
|
||||||
cleanImages: {
|
cleanImages: {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"author": "Michael Crosby & Kevan Ahlquist",
|
"author": "Michael Crosby & Kevan Ahlquist",
|
||||||
"name": "uifordocker",
|
"name": "uifordocker",
|
||||||
"homepage": "https://github.com/kevana/ui-for-docker",
|
"homepage": "https://github.com/kevana/ui-for-docker",
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:kevana/ui-for-docker.git"
|
"url": "git@github.com:kevana/ui-for-docker.git"
|
||||||
|
|
|
@ -15,20 +15,6 @@ describe('filters', function () {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('statusbadge', function () {
|
|
||||||
it('should be "important" when input is "Ghost"', inject(function (statusbadgeFilter) {
|
|
||||||
expect(statusbadgeFilter('Ghost')).toBe('important');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should be "success" when input is "Exit 0"', inject(function (statusbadgeFilter) {
|
|
||||||
expect(statusbadgeFilter('Exit 0')).toBe('success');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should be "warning" when exit code is non-zero', inject(function (statusbadgeFilter) {
|
|
||||||
expect(statusbadgeFilter('Exit 1')).toBe('warning');
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getstatetext', function () {
|
describe('getstatetext', function () {
|
||||||
|
|
||||||
it('should return an empty string when state is undefined', inject(function (getstatetextFilter) {
|
it('should return an empty string when state is undefined', inject(function (getstatetextFilter) {
|
||||||
|
|
Loading…
Reference in New Issue