diff --git a/README.md b/README.md index ce20e8267..ce004a2a2 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,16 @@ The `--privileged` flag is required for hosts using SELinux. By default UI For Docker connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`. -You can use the `-e` flag to change this socket: +You can use the `--host`, `-H` flags to change this socket: ``` # Connect to a tcp socket: -$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://127.0.0.1:2375 +$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375 +``` + +``` +# Connect to another unix socket: +$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock ``` ### Swarm support @@ -41,7 +46,7 @@ You can access a specific view for you Swarm cluster by defining the `--swarm` f ``` # Connect to a tcp socket and enable Swarm: -$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -e http://: --swarm +$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://: --swarm ``` *NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment. @@ -54,6 +59,24 @@ UI For Docker listens on port 9000 by default. If you run UI For Docker inside a $ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui ``` +### Access a Docker engine protected via TLS + +Ensure that you have access to the CA, the cert and the public key used to access your Docker engine. + +These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container: + +``` +$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify +``` + +You can also use the `--tlscacert`, `--tlscert` and `--tlskey` flags if you want to change the default path to the CA, certificate and key file respectively: + +``` +$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify --tlscacert /certs/myCa.pem --tlscert /certs/myCert.pem --tlskey /certs/myKey.pem +``` + +*Note*: Replace `/path/to/certs` to the path to the certificate files on your disk. + ### Hide containers with specific labels You can hide specific containers in the containers view by using the `-hide-label` or `-l` options and specifying a label. @@ -74,9 +97,13 @@ $ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docke The following options are available for the `ui-for-docker` binary: -* `--endpoint`, `-e`: Docker deamon endpoint (default: *"/var/run/docker.sock"*) -* `--bind`, `-p`: Address and port to serve UI For Docker (default: *":9000"*) -* `--data`, `-d`: Path to the data folder (default: *"."*) -* `--assets`, `-a`: Path to the assets (default: *"."*) -* `--swarm`, `-s`: Swarm cluster support (default: *false*) +* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`) +* `--bind`, `-p`: Address and port to serve UI For Docker (default: `":9000"`) +* `--data`, `-d`: Path to the data folder (default: `"."`) +* `--assets`, `-a`: Path to the assets (default: `"."`) +* `--swarm`, `-s`: Swarm cluster support (default: `false`) * `--hide-label`, `-l`: Hide containers with a specific label in the UI +* `--tlsverify`: TLS support (default: `false`) +* `--tlscacert`: Path to the CA (default `/certs/ca.pem`) +* `--tlscert`: Path to the TLS certificate file (default `/certs/cert.pem`) +* `--tlskey`: Path to the TLS key (default `/certs/key.pem`) diff --git a/app/app.js b/app/app.js index 42126a588..586194607 100644 --- a/app/app.js +++ b/app/app.js @@ -144,4 +144,4 @@ angular.module('uifordocker', [ .constant('DOCKER_ENDPOINT', 'dockerapi') .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243 .constant('CONFIG_ENDPOINT', '/config') - .constant('UI_VERSION', 'v1.3.0'); + .constant('UI_VERSION', 'v1.4.0'); diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index 36b26fa8f..0a7ca115a 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -39,7 +39,7 @@ - + State @@ -52,7 +52,7 @@ - + IP Address @@ -85,10 +85,10 @@ - {{ container.State }} + {{ container.Status|containerstatus }} {{ container|swarmcontainername}} {{ container|containername}} - {{ container.IP ? container.IP : '-' }} + {{ container.IP ? container.IP : '-' }} {{ container|swarmhostname}} {{ container.Image }} {{ container.Command|truncate:60 }} diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index 76fc50428..b7662e243 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -4,6 +4,7 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) { $scope.state = {}; $scope.state.displayAll = Settings.displayAll; + $scope.state.displayIP = false; $scope.sortType = 'State'; $scope.sortReverse = true; $scope.state.selectedItemCount = 0; @@ -22,7 +23,11 @@ function ($scope, Container, Settings, Messages, Config, errorMsgFilter) { containers = hideContainers(d); } $scope.containers = containers.map(function (container) { - return new ContainerViewModel(container); + var model = new ContainerViewModel(container); + if (model.IP) { + $scope.state.displayIP = true; + } + return model; }); $('#loadContainersSpinner').hide(); }); diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index 75d60d8ba..c6bd3d04f 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -20,7 +20,7 @@
- +
diff --git a/app/components/createNetwork/createNetworkController.js b/app/components/createNetwork/createNetworkController.js index f24622f07..56ffb79f6 100644 --- a/app/components/createNetwork/createNetworkController.js +++ b/app/components/createNetwork/createNetworkController.js @@ -2,13 +2,18 @@ angular.module('createNetwork', []) .controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network', 'errorMsgFilter', function ($scope, $state, Messages, Network, errorMsgFilter) { $scope.formValues = { - DriverOptions: [] + DriverOptions: [], + Subnet: '', + Gateway: '' }; $scope.config = { Driver: 'bridge', CheckDuplicate: true, - Internal: false + Internal: false, + IPAM: { + Config: [] + } }; $scope.addDriverOption = function() { @@ -36,6 +41,17 @@ function ($scope, $state, Messages, Network, errorMsgFilter) { }); } + function prepareIPAMConfiguration(config) { + if ($scope.formValues.Subnet) { + var ipamConfig = {}; + ipamConfig.Subnet = $scope.formValues.Subnet; + if ($scope.formValues.Gateway) { + ipamConfig.Gateway = $scope.formValues.Gateway ; + } + config.IPAM.Config.push(ipamConfig); + } + } + function prepareDriverOptions(config) { var options = {}; $scope.formValues.DriverOptions.forEach(function (option) { @@ -46,6 +62,7 @@ function ($scope, $state, Messages, Network, errorMsgFilter) { function prepareConfiguration() { var config = angular.copy($scope.config); + prepareIPAMConfiguration(config); prepareDriverOptions(config); return config; } diff --git a/app/components/createNetwork/createnetwork.html b/app/components/createNetwork/createnetwork.html index f6f332270..31ed20d37 100644 --- a/app/components/createNetwork/createnetwork.html +++ b/app/components/createNetwork/createnetwork.html @@ -18,6 +18,18 @@
+ +
+ +
+ +
+ +
+ +
+
+
diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index d53ab25fc..5eac079dd 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -9,7 +9,7 @@ function ($scope, $state, Image, Messages) { $scope.config = { Image: '', - Registry: '', + Registry: '' }; $scope.order = function(sortType) { diff --git a/app/shared/filters.js b/app/shared/filters.js index 4b5b93700..f1b40abfb 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -21,16 +21,31 @@ angular.module('dockerui.filters', []) .filter('containerstatusbadge', function () { 'use strict'; return function (text) { - if (text === 'paused') { + var status = _.toLower(text); + if (status.indexOf('paused') !== -1) { return 'warning'; - } else if (text === 'created') { + } else if (status.indexOf('created') !== -1) { return 'info'; - } else if (text === 'exited') { + } else if (status.indexOf('exited') !== -1) { return 'danger'; } return 'success'; }; }) +.filter('containerstatus', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + if (status.indexOf('paused') !== -1) { + return 'paused'; + } else if (status.indexOf('created') !== -1) { + return 'created'; + } else if (status.indexOf('exited') !== -1) { + return 'stopped'; + } + return 'running'; + }; +}) .filter('nodestatusbadge', function () { 'use strict'; return function (text) { diff --git a/app/shared/viewmodel.js b/app/shared/viewmodel.js index 901a2fd86..1b222692e 100644 --- a/app/shared/viewmodel.js +++ b/app/shared/viewmodel.js @@ -10,9 +10,12 @@ function ImageViewModel(data) { function ContainerViewModel(data) { this.Id = data.Id; - this.State = data.State; + this.Status = data.Status; this.Names = data.Names; - this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress; + // Unavailable in Docker < 1.10 + if (data.NetworkSettings) { + this.IP = data.NetworkSettings.Networks[Object.keys(data.NetworkSettings.Networks)[0]].IPAddress; + } this.Image = data.Image; this.Command = data.Command; this.Checked = false; diff --git a/bower.json b/bower.json index 5526ed7db..f1e37a8f1 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "uifordocker", - "version": "1.3.0", + "version": "1.4.0", "homepage": "https://github.com/kevana/ui-for-docker", "authors": [ "Michael Crosby ", diff --git a/dockerui.go b/dockerui.go index b01a2f91a..5f995f205 100644 --- a/dockerui.go +++ b/dockerui.go @@ -15,15 +15,21 @@ import ( "fmt" "github.com/gorilla/securecookie" "gopkg.in/alecthomas/kingpin.v2" + "crypto/tls" + "crypto/x509" ) var ( - endpoint = kingpin.Flag("endpoint", "Dockerd endpoint").Default("/var/run/docker.sock").Short('e').String() - addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String() - assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String() - data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String() - swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool() - labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')) + endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String() + addr = kingpin.Flag("bind", "Address and port to serve UI For Docker").Default(":9000").Short('p').String() + assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String() + data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String() + swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool() + tlsverify = kingpin.Flag("tlsverify", "TLS support").Default("false").Bool() + tlscacert = kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String() + tlscert = kingpin.Flag("tlscert", "Path to the TLS certificate file").Default("/certs/cert.pem").String() + tlskey = kingpin.Flag("tlskey", "Path to the TLS key").Default("/certs/key.pem").String() + labels = LabelParser(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')) authKey []byte authKeyFile = "authKey.dat" ) @@ -32,6 +38,13 @@ type UnixHandler struct { path string } +type TlsFlags struct { + tls bool + caPath string + certPath string + keyPath string +} + type Config struct { Swarm bool `json:"swarm"` HiddenLabels Labels `json:"hiddenLabels"` @@ -106,35 +119,70 @@ func configurationHandler(w http.ResponseWriter, r *http.Request, c Config) { json.NewEncoder(w).Encode(c) } -func createTcpHandler(e string) http.Handler { - u, err := url.Parse(e) +func createTcpHandler(u *url.URL) http.Handler { + u.Scheme = "http"; + return httputil.NewSingleHostReverseProxy(u) +} + +func createTlsConfig(tlsFlags TlsFlags) *tls.Config { + cert, err := tls.LoadX509KeyPair(tlsFlags.certPath, tlsFlags.keyPath) if err != nil { log.Fatal(err) } - return httputil.NewSingleHostReverseProxy(u) + caCert, err := ioutil.ReadFile(tlsFlags.caPath) + if err != nil { + log.Fatal(err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + return tlsConfig; +} + +func createTcpHandlerWithTLS(u *url.URL, tlsFlags TlsFlags) http.Handler { + u.Scheme = "https"; + var tlsConfig = createTlsConfig(tlsFlags) + proxy := httputil.NewSingleHostReverseProxy(u) + proxy.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + return proxy; } func createUnixHandler(e string) http.Handler { return &UnixHandler{e} } -func createHandler(dir string, d string, e string, c Config) http.Handler { +func createHandler(dir string, d string, e string, c Config, tlsFlags TlsFlags) http.Handler { var ( mux = http.NewServeMux() fileHandler = http.FileServer(http.Dir(dir)) h http.Handler ) - - if strings.Contains(e, "http") { - h = createTcpHandler(e) - } else { - if _, err := os.Stat(e); err != nil { + u, perr := url.Parse(e) + if perr != nil { + log.Fatal(perr) + } + if u.Scheme == "tcp" { + if tlsFlags.tls { + h = createTcpHandlerWithTLS(u, tlsFlags) + } else { + h = createTcpHandler(u) + } + } else if u.Scheme == "unix" { + var socketPath = u.Path + if _, err := os.Stat(socketPath); err != nil { if os.IsNotExist(err) { - log.Fatalf("unix socket %s does not exist", e) + log.Fatalf("unix socket %s does not exist", socketPath) } log.Fatal(err) } - h = createUnixHandler(e) + h = createUnixHandler(socketPath) + } else { + log.Fatalf("Bad Docker enpoint: %s. Only unix:// and tcp:// are supported.", e) } // Use existing csrf authKey if present or generate a new one. @@ -173,7 +221,7 @@ func csrfWrapper(h http.Handler) http.Handler { } func main() { - kingpin.Version("1.3.0") + kingpin.Version("1.4.0") kingpin.Parse() configuration := Config{ @@ -181,7 +229,14 @@ func main() { HiddenLabels: *labels, } - handler := createHandler(*assets, *data, *endpoint, configuration) + tlsFlags := TlsFlags{ + tls: *tlsverify, + caPath: *tlscacert, + certPath: *tlscert, + keyPath: *tlskey, + } + + handler := createHandler(*assets, *data, *endpoint, configuration, tlsFlags) if err := http.ListenAndServe(*addr, handler); err != nil { log.Fatal(err) } diff --git a/gruntFile.js b/gruntFile.js index b5e1a6c98..3ff9bbc31 100644 --- a/gruntFile.js +++ b/gruntFile.js @@ -40,6 +40,7 @@ module.exports = function (grunt) { grunt.registerTask('run', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:run']); grunt.registerTask('run-swarm', ['if:binaryNotExist', 'build', 'shell:buildImage', 'shell:runSwarm', 'watch:buildSwarm']); grunt.registerTask('run-dev', ['if:binaryNotExist', 'shell:buildImage', 'shell:run', 'watch:build']); + grunt.registerTask('run-ssl', ['if:binaryNotExist', 'shell:buildImage', 'shell:runSsl', 'watch:buildSsl']); grunt.registerTask('clear', ['clean:app']); // Print a timestamp (useful for when watching) @@ -224,6 +225,10 @@ module.exports = function (grunt) { buildSwarm: { files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'], tasks: ['build', 'shell:buildImage', 'shell:runSwarm', 'shell:cleanImages'] + }, + buildSsl: { + files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl %>', '<%= src.html %>'], + tasks: ['build', 'shell:buildImage', 'shell:runSsl', 'shell:cleanImages'] } }, jshint: { @@ -267,7 +272,14 @@ module.exports = function (grunt) { command: [ 'docker stop ui-for-docker', 'docker rm ui-for-docker', - 'docker run --privileged -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -e http://10.0.7.10:4000 --swarm -d /data' + 'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:4000 --swarm -d /data' + ].join(';') + }, + runSsl: { + command: [ + 'docker stop ui-for-docker', + 'docker rm ui-for-docker', + 'docker run -d -p 9000:9000 -v /tmp/docker-ui:/data -v /tmp/docker-ssl:/certs --name ui-for-docker ui-for-docker -H tcp://10.0.7.10:2376 -d /data --tlsverify' ].join(';') }, cleanImages: { diff --git a/package.json b/package.json index 86990bd32..2fa2f25f0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Michael Crosby & Kevan Ahlquist", "name": "uifordocker", "homepage": "https://github.com/kevana/ui-for-docker", - "version": "1.3.0", + "version": "1.4.0", "repository": { "type": "git", "url": "git@github.com:kevana/ui-for-docker.git"