mirror of https://github.com/portainer/portainer
feat(ui): add the ability to pull an image from a selection of registry
parent
0350daca8d
commit
d4ca060945
17
README.md
17
README.md
|
@ -1,5 +1,7 @@
|
||||||
# Cloudinovasi UI for Docker
|
# Cloudinovasi UI for Docker
|
||||||
|
|
||||||
|
This UI is dedicated to CloudInovasi internal usage.
|
||||||
|
|
||||||
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
|
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
|
||||||
|
|
||||||
![Dashboard](/dashboard.png)
|
![Dashboard](/dashboard.png)
|
||||||
|
@ -56,7 +58,7 @@ $ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/
|
||||||
|
|
||||||
### Hide containers with specific labels
|
### 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.
|
You can hide specific containers in the containers view by using the `--hide-label` or `-l` options and specifying a label.
|
||||||
|
|
||||||
For example, take a container started with the label `owner=acme`:
|
For example, take a container started with the label `owner=acme`:
|
||||||
|
|
||||||
|
@ -70,6 +72,16 @@ You can hide it in the view by starting the ui with:
|
||||||
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -l owner=acme
|
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -l owner=acme
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom Docker registries support
|
||||||
|
|
||||||
|
You can specify the support of others registries than DockerHub by using the `--registries` or `-r` options and specifying a registry using the format *REGISTRY_NAME=REGISTRY_ADDRESS*.
|
||||||
|
|
||||||
|
For example, if I want the registry 'myCustomRegistry' pointing to *myregistry.domain.com:5000* available in the UI:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -r myCustomRegistry=myregistry.domain.com:5000
|
||||||
|
```
|
||||||
|
|
||||||
### Available options
|
### Available options
|
||||||
|
|
||||||
The following options are available for the `ui-for-docker` binary:
|
The following options are available for the `ui-for-docker` binary:
|
||||||
|
@ -79,4 +91,5 @@ The following options are available for the `ui-for-docker` binary:
|
||||||
* `--data`, `-d`: Path to the data folder (default: *"."*)
|
* `--data`, `-d`: Path to the data folder (default: *"."*)
|
||||||
* `--assets`, `-a`: Path to the assets (default: *"."*)
|
* `--assets`, `-a`: Path to the assets (default: *"."*)
|
||||||
* `--swarm`, `-s`: Swarm cluster support (default: *false*)
|
* `--swarm`, `-s`: Swarm cluster support (default: *false*)
|
||||||
* `--hide-label`, `-l`: Hide containers with a specific label in the UI
|
* `--hide-label`, `-l`: Hide containers with a specific label in the UI (format *LABEL_NAME=LABEL_VALUE*)
|
||||||
|
* `--registries`, `-r`: Available registries in the UI (format *REGISTRY_NAME=REGISTRY_ADDRESS*)
|
||||||
|
|
|
@ -8,9 +8,12 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Console: 'none',
|
Console: 'none',
|
||||||
Volumes: []
|
Volumes: [],
|
||||||
|
AvailableRegistries: [],
|
||||||
|
Registry: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.imageConfig = {};
|
||||||
$scope.config = {
|
$scope.config = {
|
||||||
Env: [],
|
Env: [],
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
|
@ -51,6 +54,8 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
var swarm = c.swarm;
|
var swarm = c.swarm;
|
||||||
|
|
||||||
|
$scope.formValues.AvailableRegistries = c.registries;
|
||||||
|
|
||||||
Volume.query({}, function (d) {
|
Volume.query({}, function (d) {
|
||||||
var persistedVolumes = d.Volumes.filter(function (volume) {
|
var persistedVolumes = d.Volumes.filter(function (volume) {
|
||||||
if (volume.Driver === 'local-persist') {
|
if (volume.Driver === 'local-persist') {
|
||||||
|
@ -105,22 +110,9 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createImageConfig(imageName) {
|
|
||||||
var imageNameAndTag = imageName.split(':');
|
|
||||||
var imageConfig = {
|
|
||||||
fromImage: imageNameAndTag[0],
|
|
||||||
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
|
||||||
};
|
|
||||||
return imageConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
function pullImageAndCreateContainer(config) {
|
function pullImageAndCreateContainer(config) {
|
||||||
$('#createContainerSpinner').show();
|
$('#createContainerSpinner').show();
|
||||||
|
Image.create($scope.imageConfig, function (data) {
|
||||||
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');
|
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||||
if (err) {
|
if (err) {
|
||||||
var detail = data[data.length - 1];
|
var detail = data[data.length - 1];
|
||||||
|
@ -135,6 +127,28 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
console.log(JSON.stringify(imageConfig, null, 4));
|
||||||
|
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||||
|
$scope.imageConfig = imageConfig;
|
||||||
|
}
|
||||||
|
|
||||||
function preparePortBindings(config) {
|
function preparePortBindings(config) {
|
||||||
var bindings = {};
|
var bindings = {};
|
||||||
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
||||||
|
@ -194,6 +208,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages, e
|
||||||
|
|
||||||
function prepareConfiguration() {
|
function prepareConfiguration() {
|
||||||
var config = angular.copy($scope.config);
|
var config = angular.copy($scope.config);
|
||||||
|
prepareImageConfig(config);
|
||||||
preparePortBindings(config);
|
preparePortBindings(config);
|
||||||
prepareConsole(config);
|
prepareConsole(config);
|
||||||
prepareEnvironmentVariables(config);
|
prepareEnvironmentVariables(config);
|
||||||
|
|
|
@ -18,11 +18,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
<!-- !name-input -->
|
||||||
<!-- image input -->
|
<!-- image-and-registry-inputs -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
|
||||||
<div class="col-sm-11">
|
<div class="col-sm-7">
|
||||||
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="ubuntu:trusty">
|
<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">
|
||||||
|
<select class="selectpicker form-control" ng-model="formValues.Registry">
|
||||||
|
<option value="">Docker Hub</option>
|
||||||
|
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-offset-1 col-sm-11">
|
<div class="col-sm-offset-1 col-sm-11">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
|
@ -32,7 +39,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !image-input -->
|
<!-- !image-and-registry-inputs -->
|
||||||
<!-- restart-policy -->
|
<!-- restart-policy -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-1 control-label text-left">Restart policy</label>
|
<label class="col-sm-1 control-label text-left">Restart policy</label>
|
||||||
|
|
|
@ -15,14 +15,21 @@
|
||||||
</rd-widget-header>
|
</rd-widget-header>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<!-- name-input -->
|
<!-- name-and-registry-inputs -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
|
<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">
|
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty">
|
||||||
</div>
|
</div>
|
||||||
|
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<select class="selectpicker form-control" ng-model="config.Registry">
|
||||||
|
<option value="">Docker Hub</option>
|
||||||
|
<option ng-repeat="registry in availableRegistries" ng-value="registry.value">{{ registry.name }}</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<!-- !name-input -->
|
</div>
|
||||||
|
<!-- !name-and-registry-inputs -->
|
||||||
<!-- tag-note -->
|
<!-- tag-note -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('images', [])
|
angular.module('images', [])
|
||||||
.controller('ImagesController', ['$scope', '$state', 'Image', 'Messages',
|
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages',
|
||||||
function ($scope, $state, Image, Messages) {
|
function ($scope, $state, Config, Image, Messages) {
|
||||||
$scope.state = {};
|
$scope.state = {};
|
||||||
$scope.sortType = 'RepoTags';
|
$scope.sortType = 'RepoTags';
|
||||||
$scope.sortReverse = true;
|
$scope.sortReverse = true;
|
||||||
|
@ -8,7 +8,8 @@ function ($scope, $state, Image, Messages) {
|
||||||
$scope.state.selectedItemCount = 0;
|
$scope.state.selectedItemCount = 0;
|
||||||
|
|
||||||
$scope.config = {
|
$scope.config = {
|
||||||
Image: ''
|
Image: '',
|
||||||
|
Registry: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.order = function(sortType) {
|
$scope.order = function(sortType) {
|
||||||
|
@ -35,10 +36,14 @@ function ($scope, $state, Image, Messages) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function createImageConfig(imageName) {
|
function createImageConfig(imageName, registry) {
|
||||||
var imageNameAndTag = imageName.split(':');
|
var imageNameAndTag = imageName.split(':');
|
||||||
|
var image = imageNameAndTag[0];
|
||||||
|
if (registry) {
|
||||||
|
image = registry + '/' + imageNameAndTag[0];
|
||||||
|
}
|
||||||
var imageConfig = {
|
var imageConfig = {
|
||||||
fromImage: imageNameAndTag[0],
|
fromImage: image,
|
||||||
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
||||||
};
|
};
|
||||||
return imageConfig;
|
return imageConfig;
|
||||||
|
@ -47,7 +52,8 @@ function ($scope, $state, Image, Messages) {
|
||||||
$scope.pullImage = function() {
|
$scope.pullImage = function() {
|
||||||
$('#pullImageSpinner').show();
|
$('#pullImageSpinner').show();
|
||||||
var image = _.toLower($scope.config.Image);
|
var image = _.toLower($scope.config.Image);
|
||||||
var imageConfig = createImageConfig(image);
|
var registry = $scope.config.Registry;
|
||||||
|
var imageConfig = createImageConfig(image, registry);
|
||||||
Image.create(imageConfig, function (data) {
|
Image.create(imageConfig, function (data) {
|
||||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -104,5 +110,9 @@ function ($scope, $state, Image, Messages) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Config.$promise.then(function (c) {
|
||||||
|
$scope.availableRegistries = c.registries;
|
||||||
fetchImages();
|
fetchImages();
|
||||||
|
});
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -24,6 +24,7 @@ var (
|
||||||
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').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'))
|
||||||
|
registries = LabelParser(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
|
||||||
authKey []byte
|
authKey []byte
|
||||||
authKeyFile = "authKey.dat"
|
authKeyFile = "authKey.dat"
|
||||||
)
|
)
|
||||||
|
@ -35,6 +36,7 @@ type UnixHandler struct {
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Swarm bool `json:"swarm"`
|
Swarm bool `json:"swarm"`
|
||||||
HiddenLabels Labels `json:"hiddenLabels"`
|
HiddenLabels Labels `json:"hiddenLabels"`
|
||||||
|
Registries Labels `json:"registries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Label struct {
|
type Label struct {
|
||||||
|
@ -47,7 +49,7 @@ type Labels []Label
|
||||||
func (l *Labels) Set(value string) error {
|
func (l *Labels) Set(value string) error {
|
||||||
parts := strings.SplitN(value, "=", 2)
|
parts := strings.SplitN(value, "=", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return fmt.Errorf("expected HEADER=VALUE got '%s'", value)
|
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
|
||||||
}
|
}
|
||||||
label := new(Label)
|
label := new(Label)
|
||||||
label.Name = parts[0]
|
label.Name = parts[0]
|
||||||
|
@ -179,6 +181,7 @@ func main() {
|
||||||
configuration := Config{
|
configuration := Config{
|
||||||
Swarm: *swarm,
|
Swarm: *swarm,
|
||||||
HiddenLabels: *labels,
|
HiddenLabels: *labels,
|
||||||
|
Registries: *registries,
|
||||||
}
|
}
|
||||||
|
|
||||||
handler := createHandler(*assets, *data, *endpoint, configuration)
|
handler := createHandler(*assets, *data, *endpoint, configuration)
|
||||||
|
|
|
@ -267,7 +267,7 @@ 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 /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.11:4000 --swarm -d /data'
|
'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 -r local=192.168.2.193:5000'
|
||||||
].join(';')
|
].join(';')
|
||||||
},
|
},
|
||||||
cleanImages: {
|
cleanImages: {
|
||||||
|
|
Loading…
Reference in New Issue