diff --git a/.gitignore b/.gitignore index 6760c7934..61fe02d69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,4 @@ -logs/* -!.gitkeep -*.esproj/* node_modules bower_components -.idea -*.iml dist -dist/* portainer-checksum.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..250f46d86 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# Contributing Guidelines + +Some basic conventions for contributing to this project. + +### General + +Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. + +* Non-trivial changes should be discussed in an issue first +* Develop in a topic branch, not master + +### Linting + +Please check your code using `grunt lint` before submitting your pull requests. + +### Commit Message Format + +Each commit message should include a **type**, a **scope** and a **subject**: + +``` + (): +``` + +Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie: + +``` + #271 feat(standard): add style config and refactor to match + #270 fix(config): only override publicPath when served by webpack + #269 feat(eslint-config-defaults): replace eslint-config-airbnb + #268 feat(config): allow user to configure webpack stats output +``` + +#### Type + +Must be one of the following: + +* **feat**: A new feature +* **fix**: A bug fix +* **docs**: Documentation only changes +* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing + semi-colons, etc) +* **refactor**: A code change that neither fixes a bug or adds a feature +* **test**: Adding missing tests +* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation + generation + +#### Scope + +The scope could be anything specifying place of the commit change. For example `networks`, +`containers`, `images` etc... + +#### Subject + +The subject contains succinct description of the change: + +* use the imperative, present tense: "change" not "changed" nor "changes" +* don't capitalize first letter +* no dot (.) at the end diff --git a/LICENSE b/LICENSE index 52f0f4b98..d03999c57 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Portainer: Copyright (c) 2016 CloudInovasi +Portainer: Copyright (c) 2016 Portainer.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,7 +18,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id) +UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Procfile b/Procfile deleted file mode 100644 index 1390d66ed..000000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: portainer -p ":$PORT" -e "$DOCKER_ENDPOINT" diff --git a/README.md b/README.md index 13ef3f3ea..8f5258c51 100644 --- a/README.md +++ b/README.md @@ -1,165 +1,81 @@ # Portainer -[![Microbadger](https://images.microbadger.com/badges/image/cloudinovasi/portainer.svg)](http://microbadger.com/images/cloudinovasi/portainer "Image size") +The easiest way to manage Docker. + +[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size") [![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -Portainer is a web interface for the Docker remote API. +Portainer is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster. -![Dashboard](/dashboard.png) +# Usage -## Supported Docker versions +It's really simple to deploy it using Docker: -The following Docker versions are supported: - -* full support for Docker 1.10, 1.11 and 1.12 -* partial support for Docker 1.9 (some features won't be available) - -## Run - -### Quickstart - -1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer` - -2. Open your browser to `http://:9000` - -Bind mounting the Unix socket into the Portainer container is much more secure than exposing your docker daemon over TCP. - -The `--privileged` flag is required for hosts using SELinux. - -### Specify socket to connect to Docker daemon - -By default Portainer 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 `--host`, `-H` flags to change this socket: - -``` -# Connect to a tcp socket: -$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://127.0.0.1:2375 +```shell +$ docker run -d -p 9000:9000 portainer/portainer -H tcp://: ``` -``` -# Connect to another unix socket: -$ docker run -d -p 9000:9000 cloudinovasi/portainer -H unix:///path/to/docker.sock +Just point it at your targeted Docker host and then access Portainer by hitting [http://localhost:9000](http://localhost:9000) with a web browser. + +If your target is a Docker Swarm cluster or a Docker cluster using *swarm mode*, just add the flag `--swarm`: + +```shell +$ docker run -d -p 9000:9000 portainer/portainer -H tcp://: --swarm ``` -### Swarm support +If you don't specify any target, its default behaviour is to use a bind mount on the Docker socket so you can easily deploy it to manage your local Docker host: -**Supported Swarm version: 1.2.3** - -You can access a specific view for you Swarm cluster by defining the `--swarm` flag: - -``` -# Connect to a tcp socket and enable Swarm: -$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://: --swarm +```shell +$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer ``` -*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. +Have a look at our [wiki](https://github.com/portainer/portainer/wiki/Deployment) for more deployment options. -### Change address/port Portainer is served on -Portainer listens on port 9000 by default. If you run Portainer inside a container then you can bind the container's internal port to any external address and port: +# Configuration -``` -# Expose Portainer on 10.20.30.1:80 -$ docker run -d -p 10.20.30.1:80:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer -``` +Portainer is easy to tune using CLI flags. -### Access a Docker engine protected via TLS +## Hiding specific containers -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/portainer -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/portainer -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. - -### Use your own logo - -You can use the `--logo` flag to specify an URL to your own logo. - -For example, using the Docker logo: - -``` -$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer --logo "https://www.docker.com/sites/all/themes/docker/assets/images/brand-full.svg" -``` - -The custom logo will replace the Portainer logo in the UI. - -### 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. +Portainer allows you to hide container with a specific label by using the `-l` flag. For example, take a container started with the label `owner=acme`: - -``` +```shell $ docker run -d --label owner=acme nginx ``` -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/portainer -l owner=acme +Simply add the `-l owner=acme` option on the CLI when starting Portainer: +```shell +$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer -l owner=acme ``` -### Reverse proxy configuration +## Use your own templates -Has been tested with Nginx 1.11. +Portainer allows you to rapidly deploy containers using `App Templates`. -Use the following configuration to host the UI at `myhost.mydomain.com/portainer`: +By default [Portainer templates](https://raw.githubusercontent.com/portainer/templates/master/templates.json) will be used but you can also define your own templates. -```nginx -upstream portainer { - server ADDRESS:PORT; -} +Add the `--templates` flag and specify the external location of your templates when starting Portainer: -server { - listen 80; - - location /portainer/ { - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_pass http://portainer/; - } - location /portainer/ws/ { - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_http_version 1.1; - proxy_pass http://portainer/ws/; - } -} +```shell +$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer --templates http://my-host.my-domain/templates.json ``` -Replace `ADDRESS:PORT` with the Portainer container details. +For more information about hosting your own template definitions and the format, see: https://github.com/portainer/templates -### Host your own apps +Have a look at our [wiki](https://github.com/portainer/portainer/wiki/Configuration) for more configuration options. -You can specify an URL to your own templates (**Apps**) definitions using the `--templates` or `-t` flags. +# FAQ -By default, CloudInovasi templates will be used (https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json). +Be sure to check our [FAQ](https://github.com/portainer/portainer/wiki/FAQ) if you are missing some information. -For more information about hosting your own template definition and the format, see: https://github.com/cloud-inovasi/ui-templates +# Limitations -### Available options +Portainer has full support for the following Docker versions: -The following options are available for the `portainer` binary: +* Docker 1.10 to Docker 1.12 (including `swarm-mode`) +* Docker Swarm >= 1.2.3 -* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`) -* `--bind`, `-p`: Address and port to serve Portainer (default: `":9000"`) -* `--data`, `-d`: Path to the data folder (default: `"."`) -* `--assets`, `-a`: Path to the assets (default: `"."`) -* `--swarm`, `-s`: Swarm cluster support (default: `false`) -* `--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`) -* `--hide-label`, `-l`: Hide containers with a specific label in the UI -* `--logo`: URL to a picture to be displayed as a logo in the UI -* `--templates`, `-t`: URL to templates (apps) definitions +Partial support for the following Docker versions (some features may not be available): + +* Docker 1.9 diff --git a/api/main.go b/api/main.go index 3cc5b5ba9..4e81b2c56 100644 --- a/api/main.go +++ b/api/main.go @@ -1,4 +1,4 @@ -package main // import "github.com/cloudinovasi/portainer" +package main // import "github.com/portainer/portainer" import ( "gopkg.in/alecthomas/kingpin.v2" @@ -6,7 +6,7 @@ import ( // main is the entry point of the program func main() { - kingpin.Version("1.8.1") + kingpin.Version("1.9.0") var ( endpoint = kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String() addr = kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String() @@ -19,7 +19,7 @@ func main() { swarm = kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool() labels = pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')) logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String() - templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json").Short('t').String() + templates = kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String() ) kingpin.Parse() diff --git a/app/app.js b/app/app.js index c7e9a3acb..0daaa86d7 100644 --- a/app/app.js +++ b/app/app.js @@ -18,11 +18,15 @@ angular.module('portainer', [ 'events', 'images', 'image', + 'service', + 'services', + 'createService', 'stats', 'swarm', 'network', 'networks', 'createNetwork', + 'task', 'templates', 'volumes', 'createVolume']) @@ -80,16 +84,21 @@ angular.module('portainer', [ templateUrl: 'app/components/createContainer/createcontainer.html', controller: 'CreateContainerController' }) - .state('actions.create.volume', { - url: "/volume", - templateUrl: 'app/components/createVolume/createvolume.html', - controller: 'CreateVolumeController' - }) .state('actions.create.network', { url: "/network", templateUrl: 'app/components/createNetwork/createnetwork.html', controller: 'CreateNetworkController' }) + .state('actions.create.service', { + url: "/service", + templateUrl: 'app/components/createService/createservice.html', + controller: 'CreateServiceController' + }) + .state('actions.create.volume', { + url: "/volume", + templateUrl: 'app/components/createVolume/createvolume.html', + controller: 'CreateVolumeController' + }) .state('docker', { url: '/docker/', templateUrl: 'app/components/docker/docker.html', @@ -120,6 +129,21 @@ angular.module('portainer', [ templateUrl: 'app/components/network/network.html', controller: 'NetworkController' }) + .state('services', { + url: '/services/', + templateUrl: 'app/components/services/services.html', + controller: 'ServicesController' + }) + .state('service', { + url: '^/service/:id/', + templateUrl: 'app/components/service/service.html', + controller: 'ServiceController' + }) + .state('task', { + url: '^/task/:id', + templateUrl: 'app/components/task/task.html', + controller: 'TaskController' + }) .state('templates', { url: '/templates/', templateUrl: 'app/components/templates/templates.html', @@ -164,4 +188,4 @@ angular.module('portainer', [ .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', 'settings') .constant('TEMPLATES_ENDPOINT', 'templates') - .constant('UI_VERSION', 'v1.8.1'); + .constant('UI_VERSION', 'v1.9.0'); diff --git a/app/components/container/container.html b/app/components/container/container.html index 90a8a6a10..a454ed04f 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -29,7 +29,7 @@
- + @@ -130,7 +130,7 @@
- +
diff --git a/app/components/containerLogs/containerlogs.html b/app/components/containerLogs/containerlogs.html index fc4765891..4e685432f 100644 --- a/app/components/containerLogs/containerlogs.html +++ b/app/components/containerLogs/containerlogs.html @@ -12,7 +12,7 @@
- +
{{ container.Name|trimcontainername }}
Name
diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index 30989f821..378233f34 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -9,7 +9,7 @@
- +
@@ -17,15 +17,15 @@
- - - - - - - + + + + + + +
- Add container + Add container
@@ -66,7 +66,7 @@ -
- - + + - + diff --git a/app/components/images/images.html b/app/components/images/images.html index f4bf6fd4e..21fbe57db 100644 --- a/app/components/images/images.html +++ b/app/components/images/images.html @@ -55,7 +55,7 @@
- +
diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index 2d692c707..1f7e0394c 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -72,7 +72,7 @@ function ($scope, $state, Config, Image, Messages) { counter = counter + 1; Image.remove({id: i.Id}, function (d) { if (d[0].message) { - $('#loadingViewSpinner').hide(); + $('#loadImagesSpinner').hide(); Messages.error("Unable to remove image", {}, d[0].message); } else { Messages.send("Image deleted", i.Id); diff --git a/app/components/network/network.html b/app/components/network/network.html index 36bec67ec..678113b2f 100644 --- a/app/components/network/network.html +++ b/app/components/network/network.html @@ -8,118 +8,56 @@
-
- - -
- -
-
{{ network.Name }}
-
Name
-
-
-
-
- - -
- -
-
-
- - -
-
-
- Actions -
-
-
-
-
-
-
+
+ Host IP @@ -86,11 +86,11 @@
{{ container.Status|containerstatus }}{{ container|swarmcontainername}}{{ container|containername}}{{ container|swarmcontainername}}{{ container|containername}} {{ container.Image }} {{ container.IP ? container.IP : '-' }}{{ container.hostIP }}{{ container.hostIP }} {{ p.private }} diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index a0000c421..d807cc226 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -8,6 +8,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config) $scope.sortType = 'State'; $scope.sortReverse = false; $scope.state.selectedItemCount = 0; + $scope.swarm_mode = false; $scope.order = function (sortType) { $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; @@ -27,7 +28,7 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config) if (model.IP) { $scope.state.displayIP = true; } - if ($scope.swarm) { + if ($scope.swarm && !$scope.swarm_mode) { model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]]; } return model; @@ -151,7 +152,11 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config) $scope.swarm = c.swarm; if (c.swarm) { Info.get({}, function (d) { - $scope.swarm_hosts = retrieveSwarmHostsInfo(d); + if (!_.startsWith(d.ServerVersion, 'swarm')) { + $scope.swarm_mode = true; + } else { + $scope.swarm_hosts = retrieveSwarmHostsInfo(d); + } update({all: Settings.displayAll ? 1 : 0}); }); } else { diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index 4724c4111..939534f58 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -1,6 +1,6 @@ angular.module('createContainer', []) -.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', -function ($scope, $state, Config, Container, Image, Volume, Network, Messages) { +.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Info', 'Container', 'Image', 'Volume', 'Network', 'Messages', +function ($scope, $state, Config, Info, Container, Image, Volume, Network, Messages) { $scope.state = { alwaysPull: true @@ -55,6 +55,11 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) { Config.$promise.then(function (c) { var swarm = c.swarm; + Info.get({}, function(info) { + if (swarm && !_.startsWith(info.ServerVersion, 'swarm')) { + $scope.swarm_mode = true; + } + }); $scope.formValues.AvailableRegistries = c.registries; diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index 3eb25b6f0..508e2a03f 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -254,7 +254,7 @@
-
+
diff --git a/app/components/createNetwork/createNetworkController.js b/app/components/createNetwork/createNetworkController.js index cb99db101..3d03b56bd 100644 --- a/app/components/createNetwork/createNetworkController.js +++ b/app/components/createNetwork/createNetworkController.js @@ -11,7 +11,10 @@ function ($scope, $state, Messages, Network) { Driver: 'bridge', CheckDuplicate: true, Internal: false, + // Force IPAM Driver to 'default', should not be required. + // See: https://github.com/docker/docker/issues/25735 IPAM: { + Driver: 'default', Config: [] } }; diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js new file mode 100644 index 000000000..675aeb338 --- /dev/null +++ b/app/components/createService/createServiceController.js @@ -0,0 +1,178 @@ +angular.module('createService', []) +.controller('CreateServiceController', ['$scope', '$state', 'Service', 'Volume', 'Network', 'ImageHelper', 'Messages', +function ($scope, $state, Service, Volume, Network, ImageHelper, Messages) { + + $scope.formValues = { + Name: '', + Image: '', + Registry: '', + Mode: 'replicated', + Replicas: 1, + Command: '', + WorkingDir: '', + User: '', + Env: [], + Volumes: [], + Network: '', + ExtraNetworks: [], + Ports: [] + }; + + $scope.addPortBinding = function() { + $scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp' }); + }; + + $scope.removePortBinding = function(index) { + $scope.formValues.Ports.splice(index, 1); + }; + + $scope.addExtraNetwork = function() { + $scope.formValues.ExtraNetworks.push({ Name: '' }); + }; + + $scope.removeExtraNetwork = function(index) { + $scope.formValues.ExtraNetworks.splice(index, 1); + }; + + $scope.addVolume = function() { + $scope.formValues.Volumes.push({ name: '', containerPath: '' }); + }; + + $scope.removeVolume = function(index) { + $scope.formValues.Volumes.splice(index, 1); + }; + + $scope.addEnvironmentVariable = function() { + $scope.formValues.Env.push({ name: '', value: ''}); + }; + + $scope.removeEnvironmentVariable = function(index) { + $scope.formValues.Env.splice(index, 1); + }; + + function prepareImageConfig(config, input) { + var imageConfig = ImageHelper.createImageConfig(input.Image, input.Registry); + config.TaskTemplate.ContainerSpec.Image = imageConfig.repo + ':' + imageConfig.tag; + } + + function preparePortsConfig(config, input) { + var ports = []; + input.Ports.forEach(function (binding) { + if (binding.PublishedPort && binding.TargetPort) { + ports.push({ PublishedPort: +binding.PublishedPort, TargetPort: +binding.TargetPort, Protocol: binding.Protocol }); + } + }); + config.EndpointSpec.Ports = ports; + } + + function prepareSchedulingConfig(config, input) { + if (input.Mode === 'replicated') { + config.Mode.Replicated = { + Replicas: input.Replicas + }; + } else { + config.Mode.Global = {}; + } + } + + function prepareCommandConfig(config, input) { + if (input.Command) { + config.TaskTemplate.ContainerSpec.Command = _.split(input.Command, ' '); + } + if (input.User) { + config.TaskTemplate.ContainerSpec.User = input.User; + } + if (input.WorkingDir) { + config.TaskTemplate.ContainerSpec.Dir = input.WorkingDir; + } + } + + function prepareEnvConfig(config, input) { + var env = []; + input.Env.forEach(function (v) { + if (v.name && v.value) { + env.push(v.name + "=" + v.value); + } + }); + config.TaskTemplate.ContainerSpec.Env = env; + } + + function prepareVolumes(config, input) { + input.Volumes.forEach(function (volume) { + if (volume.Source && volume.Target) { + var mount = {}; + mount.Type = volume.Bind ? 'bind' : 'volume'; + mount.ReadOnly = volume.ReadOnly ? true : false; + mount.Source = volume.Source; + mount.Target = volume.Target; + config.TaskTemplate.ContainerSpec.Mounts.push(mount); + } + }); + } + + function prepareNetworks(config, input) { + var networks = []; + if (input.Network) { + networks.push({ Target: input.Network }); + } + input.ExtraNetworks.forEach(function (network) { + networks.push({ Target: network.Name }); + }); + config.Networks = _.uniqWith(networks, _.isEqual); + } + + function prepareConfiguration() { + var input = $scope.formValues; + var config = { + Name: input.Name, + TaskTemplate: { + ContainerSpec: { + Mounts: [] + } + }, + Mode: {}, + EndpointSpec: {} + }; + prepareSchedulingConfig(config, input); + prepareImageConfig(config, input); + preparePortsConfig(config, input); + prepareCommandConfig(config, input); + prepareEnvConfig(config, input); + prepareVolumes(config, input); + prepareNetworks(config, input); + return config; + } + + function createNewService(config) { + Service.create(config, function (d) { + $('#createServiceSpinner').hide(); + Messages.send('Service created', d.ID); + $state.go('services', {}, {reload: true}); + }, function (e) { + $('#createServiceSpinner').hide(); + Messages.error("Failure", e, 'Unable to create service'); + }); + } + + $scope.create = function createService() { + $('#createServiceSpinner').show(); + var config = prepareConfiguration(); + createNewService(config); + }; + + Volume.query({}, function (d) { + $scope.availableVolumes = d.Volumes; + }, function (e) { + Messages.error("Failure", e, "Unable to retrieve volumes"); + }); + + Network.query({}, function (d) { + $scope.availableNetworks = d.filter(function (network) { + if (network.Scope === 'swarm') { + return network; + } + }); + }, function (e) { + Messages.error("Failure", e, "Unable to retrieve networks"); + }); +}]); diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html new file mode 100644 index 000000000..6357d667a --- /dev/null +++ b/app/components/createService/createservice.html @@ -0,0 +1,272 @@ + + + + Services > Add service + + + +
+
+ + + + +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+
+
+ +
+ +
+
+
+ + +
+ +
+ + map port + +
+ +
+
+
+ host + +
+
+ container + +
+
+ + + + +
+
+
+ +
+ + +
+
+
+
+ +
+
+ + + + +
+ +
+
+ +
+ +
+ +
+
+ + +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+ + environment variable + +
+ +
+
+
+ name + +
+
+ value + + + + +
+
+
+ +
+ +
+
+ + +
+
+ +
+ +
+ + volume + +
+ +
+
+
+
+ +
+
+
+ bind + + +
+
+ container + + + + +
+
+
+ +
+ +
+
+ + +
+
+ +
+ +
+ +
+
+
+ + +
+ +
+ + network + +
+ +
+
+
+ + + + +
+
+
+
+ +
+ +
+
+ + +
+
+ +
+
+
+
+
+ +
+
+
+ +
+ + Cancel +
+
diff --git a/app/components/dashboard/dashboard.html b/app/components/dashboard/dashboard.html index 4980a5c8b..ca289aafc 100644 --- a/app/components/dashboard/dashboard.html +++ b/app/components/dashboard/dashboard.html @@ -6,7 +6,7 @@
-
+
@@ -33,7 +33,7 @@
-
+
@@ -60,6 +60,28 @@
+
+ + + + + + + + + + + + + + + + + +
This node is part of a Swarm cluster
Node role{{ infoData.Swarm.ControlAvailable ? 'Manager' : 'Worker' }}
Nodes in the cluster{{ infoData.Swarm.Nodes }}
+
+
+
@@ -68,7 +90,7 @@
- +
{{ containerData.running }} running
diff --git a/app/components/dashboard/dashboardController.js b/app/components/dashboard/dashboardController.js index 13b2c8617..884633943 100644 --- a/app/components/dashboard/dashboardController.js +++ b/app/components/dashboard/dashboardController.js @@ -14,6 +14,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume $scope.volumeData = { total: 0 }; + $scope.swarm_mode = false; function prepareContainerData(d, containersToHideLabels) { var running = 0; @@ -63,6 +64,9 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume function prepareInfoData(d) { var info = d; $scope.infoData = info; + if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) { + $scope.swarm_mode = true; + } } function fetchDashboardData(containersToHideLabels) { diff --git a/app/components/dashboard/master-ctrl.js b/app/components/dashboard/master-ctrl.js index 9d5080005..abbb4295d 100644 --- a/app/components/dashboard/master-ctrl.js +++ b/app/components/dashboard/master-ctrl.js @@ -1,5 +1,6 @@ angular.module('dashboard') -.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', function ($scope, $cookieStore, Settings, Config) { +.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', 'Info', +function ($scope, $cookieStore, Settings, Config, Info) { /** * Sidebar Toggle & Cookie Control */ @@ -9,7 +10,20 @@ angular.module('dashboard') return window.innerWidth; }; - $scope.config = Config; + $scope.swarm_mode = false; + + Config.$promise.then(function (c) { + $scope.swarm = c.swarm; + Info.get({}, function(d) { + if ($scope.swarm && !_.startsWith(d.ServerVersion, 'swarm')) { + $scope.swarm_mode = true; + $scope.swarm_manager = false; + if (d.Swarm.ControlAvailable) { + $scope.swarm_manager = true; + } + } + }); + }); $scope.$watch($scope.getWidth, function(newValue, oldValue) { if (newValue >= mobileView) { diff --git a/app/components/image/image.html b/app/components/image/image.html index b4298903b..b94f40a16 100644 --- a/app/components/image/image.html +++ b/app/components/image/image.html @@ -82,7 +82,7 @@
ID {{ image.Id }} - +
- - + + - - + + - - + + - - - + + + - - - + + + + + +
Id{{ network.Id }}Name{{ network.Name }}
Scope{{ network.Scope }}ID + {{ network.Id }} + +
Driver {{ network.Driver }}
IPAM - - - - - - - - - - - - - -
Driver{{ network.IPAM.Driver }}
Subnet{{ network.IPAM.Config[0].Subnet }}
Gateway{{ network.IPAM.Config[0].Gateway }}
-
Scope{{ network.Scope }}
Containers - - - - - - - - - - - - - - - - - - - - - - - - -
Id{{ Id }}
EndpointID{{ container.EndpointID}}
MacAddress{{ container.MacAddress}}
IPv4Address{{ container.IPv4Address}}
IPv6Address{{ container.IPv6Address}}
- -
-
Subnet{{ network.IPAM.Config[0].Subnet }}
Options - - - - - -
{{ k }}{{ v }}
-
Gateway{{ network.IPAM.Config[0].Gateway }}
+
+
+
+
+ +
+
+ + + + + + + +
{{ key }}{{ value }}
diff --git a/app/components/network/networkController.js b/app/components/network/networkController.js index 8d55ce70c..d5e93b7ff 100644 --- a/app/components/network/networkController.js +++ b/app/components/network/networkController.js @@ -1,20 +1,8 @@ angular.module('network', []) -.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', -function ($scope, Network, Messages, $state, $stateParams) { +.controller('NetworkController', ['$scope', '$state', '$stateParams', 'Network', 'Messages', +function ($scope, $state, $stateParams, Network, Messages) { - $scope.disconnect = function disconnect(networkId, containerId) { - $('#loadingViewSpinner').show(); - Network.disconnect({id: $stateParams.id}, {Container: containerId}, function (d) { - $('#loadingViewSpinner').hide(); - Messages.send("Container disconnected", containerId); - $state.go('network', {id: $stateParams.id}, {reload: true}); - }, function (e) { - $('#loadingViewSpinner').hide(); - Messages.error("Failure", e, "Unable to disconnect container"); - }); - }; - - $scope.remove = function remove(networkId) { + $scope.removeNetwork = function removeNetwork(networkId) { $('#loadingViewSpinner').show(); Network.remove({id: $stateParams.id}, function (d) { if (d.message) { diff --git a/app/components/networks/networks.html b/app/components/networks/networks.html index aba75e57f..d0b5c9fb0 100644 --- a/app/components/networks/networks.html +++ b/app/components/networks/networks.html @@ -7,93 +7,134 @@ Networks -
- - -
- -
-
- -
- - Add network -
-
- -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - Name - - - - - - Id - - - - - - Scope - - - - - - Driver - - - - - - IPAM Driver - - - - - - IPAM Subnet - - - - - - IPAM Gateway - - - -
{{ network.Name|truncate:40}}{{ network.Id }}{{ network.Scope }}{{ network.Driver }}{{ network.IPAM.Driver }}{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}
-
-
- +
+
+ + + + +
+ +
+ +
+ +
+
+ + +
+
+ Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster. +
+
+
+
+ Note: The network will be created using the bridge driver. +
+
+ +
+
+ + + +
+
+
+
+
+
+
+ +
+
+ + +
+ +
+
+ +
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Id + + + + + + Scope + + + + + + Driver + + + + + + IPAM Driver + + + + + + IPAM Subnet + + + + + + IPAM Gateway + + + +
{{ network.Name|truncate:40}}{{ network.Id }}{{ network.Scope }}{{ network.Driver }}{{ network.IPAM.Driver }}{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}
+
+
+ +
diff --git a/app/components/networks/networksController.js b/app/components/networks/networksController.js index 8a5287286..84e755c07 100644 --- a/app/components/networks/networksController.js +++ b/app/components/networks/networksController.js @@ -7,16 +7,39 @@ function ($scope, $state, Network, Config, Messages) { $scope.sortType = 'Name'; $scope.sortReverse = false; - $scope.formValues = { - Subnet: '', - Gateway: '' + $scope.config = { + Name: '' }; - $scope.config = { - Name: '', - IPAM: { - Config: [] + function prepareNetworkConfiguration() { + var config = angular.copy($scope.config); + if ($scope.swarm) { + config.Driver = 'overlay'; + // Force IPAM Driver to 'default', should not be required. + // See: https://github.com/docker/docker/issues/25735 + config.IPAM = { + Driver: 'default' + }; } + return config; + } + + $scope.createNetwork = function() { + $('#createNetworkSpinner').show(); + var config = prepareNetworkConfiguration(); + Network.create(config, function (d) { + if (d.message) { + $('#createNetworkSpinner').hide(); + Messages.error('Unable to create network', {}, d.message); + } else { + Messages.send("Network created", d.Id); + $('#createNetworkSpinner').hide(); + $state.go('networks', {}, {reload: true}); + } + }, function (e) { + $('#createNetworkSpinner').hide(); + Messages.error("Failure", e, 'Unable to create network'); + }); }; $scope.order = function(sortType) { @@ -72,5 +95,8 @@ function ($scope, $state, Network, Config, Messages) { }); } - fetchNetworks(); + Config.$promise.then(function (c) { + $scope.swarm = c.swarm; + fetchNetworks(); + }); }]); diff --git a/app/components/service/service.html b/app/components/service/service.html new file mode 100644 index 000000000..ad674e4e5 --- /dev/null +++ b/app/components/service/service.html @@ -0,0 +1,150 @@ + + + + + + + + + Services > {{ service.Name }} + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ service.Name }} + + + + + +
ID + {{ service.Id }} + +
Scheduling mode{{ service.Mode }}
Replicas + + {{ service.Replicas }} + Scale + + + + + + +
Image{{ service.Image }}
Published ports +
+ {{ mapping.TargetPort }} {{ mapping.PublishedPort }} +
+
Env + + + + + +
{{ var|key: '=' }}{{ var|value: '=' }}
+
Labels + + + + + +
{{ k }}{{ v }}
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + +
Id + + Status + + + + + + Slot + + + + + + Node + + + + + + Last update + + + +
{{ task.Id }}{{ task.Status }}{{ task.Slot }}{{ task.Node }}{{ task.Updated|getisodate }}
+
+
+
+
diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js new file mode 100644 index 000000000..07fecf38a --- /dev/null +++ b/app/components/service/serviceController.js @@ -0,0 +1,97 @@ +angular.module('service', []) +.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', +function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages) { + + $scope.service = {}; + $scope.tasks = []; + $scope.displayNode = false; + $scope.sortType = 'Status'; + $scope.sortReverse = false; + + $scope.order = function (sortType) { + $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; + + $scope.renameService = function renameService(service) { + $('#loadServicesSpinner').show(); + var serviceName = service.Name; + var config = ServiceHelper.serviceToConfig(service.Model); + config.Name = service.newServiceName; + Service.update({ id: service.Id, version: service.Version }, config, function (data) { + $('#loadServicesSpinner').hide(); + Messages.send("Service successfully renamed", "New name: " + service.newServiceName); + $state.go('service', {id: service.Id}, {reload: true}); + }, function (e) { + $('#loadServicesSpinner').hide(); + service.EditName = false; + service.Name = serviceName; + Messages.error("Failure", e, "Unable to rename service"); + }); + }; + + $scope.scaleService = function scaleService(service) { + $('#loadServicesSpinner').show(); + var config = ServiceHelper.serviceToConfig(service.Model); + config.Mode.Replicated.Replicas = service.Replicas; + Service.update({ id: service.Id, version: service.Version }, config, function (data) { + $('#loadServicesSpinner').hide(); + Messages.send("Service successfully scaled", "New replica count: " + service.Replicas); + $state.go('service', {id: service.Id}, {reload: true}); + }, function (e) { + $('#loadServicesSpinner').hide(); + service.Scale = false; + service.Replicas = service.ReplicaCount; + Messages.error("Failure", e, "Unable to scale service"); + }); + }; + + $scope.removeService = function removeService() { + $('#loadingViewSpinner').show(); + Service.remove({id: $stateParams.id}, function (d) { + if (d.message) { + $('#loadingViewSpinner').hide(); + Messages.send("Error", {}, d.message); + } else { + $('#loadingViewSpinner').hide(); + Messages.send("Service removed", $stateParams.id); + $state.go('services', {}); + } + }, function (e) { + $('#loadingViewSpinner').hide(); + Messages.error("Failure", e, "Unable to remove service"); + }); + }; + + function fetchServiceDetails() { + $('#loadingViewSpinner').show(); + Service.get({id: $stateParams.id}, function (d) { + var service = new ServiceViewModel(d); + service.newServiceName = service.Name; + $scope.service = service; + Task.query({filters: {service: [service.Name]}}, function (tasks) { + Node.query({}, function (nodes) { + $scope.displayNode = true; + $scope.tasks = tasks.map(function (task) { + return new TaskViewModel(task, nodes); + }); + $('#loadingViewSpinner').hide(); + }, function (e) { + $('#loadingViewSpinner').hide(); + $scope.tasks = tasks.map(function (task) { + return new TaskViewModel(task, null); + }); + Messages.error("Failure", e, "Unable to retrieve node information"); + }); + }, function (e) { + $('#loadingViewSpinner').hide(); + Messages.error("Failure", e, "Unable to retrieve tasks associated to the service"); + }); + }, function (e) { + $('#loadingViewSpinner').hide(); + Messages.error("Failure", e, "Unable to retrieve service details"); + }); + } + + fetchServiceDetails(); +}]); diff --git a/app/components/services/services.html b/app/components/services/services.html new file mode 100644 index 000000000..8cc618a28 --- /dev/null +++ b/app/components/services/services.html @@ -0,0 +1,78 @@ + + + + + + + Services + + +
+
+ + +
+ +
+
+ +
+ + Add service +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + +
+ + Name + + + + + + Image + + + + + + Scheduling mode + + + +
{{ service.Name }}{{ service.Image }} + {{ service.Mode }} + + {{ service.Replicas }} + Scale + + + + + + +
+
+
+ +
+
diff --git a/app/components/services/servicesController.js b/app/components/services/servicesController.js new file mode 100644 index 000000000..ac7054e01 --- /dev/null +++ b/app/components/services/servicesController.js @@ -0,0 +1,84 @@ +angular.module('services', []) +.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', +function ($scope, $stateParams, $state, Service, ServiceHelper, Messages) { + + $scope.services = []; + $scope.state = {}; + $scope.state.selectedItemCount = 0; + $scope.sortType = 'Name'; + $scope.sortReverse = false; + + $scope.scaleService = function scaleService(service) { + $('#loadServicesSpinner').show(); + var config = ServiceHelper.serviceToConfig(service.Model); + config.Mode.Replicated.Replicas = service.Replicas; + Service.update({ id: service.Id, version: service.Version }, config, function (data) { + $('#loadServicesSpinner').hide(); + Messages.send("Service successfully scaled", "New replica count: " + service.Replicas); + $state.go('services', {}, {reload: true}); + }, function (e) { + $('#loadServicesSpinner').hide(); + service.Scale = false; + service.Replicas = service.ReplicaCount; + Messages.error("Failure", e, "Unable to scale service"); + }); + }; + + $scope.order = function (sortType) { + $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; + + $scope.selectItem = function (item) { + if (item.Checked) { + $scope.state.selectedItemCount++; + } else { + $scope.state.selectedItemCount--; + } + }; + + $scope.removeAction = function () { + $('#loadServicesSpinner').show(); + var counter = 0; + var complete = function () { + counter = counter - 1; + if (counter === 0) { + $('#loadServicesSpinner').hide(); + } + }; + angular.forEach($scope.services, function (service) { + if (service.Checked) { + counter = counter + 1; + Service.remove({id: service.Id}, function (d) { + if (d.message) { + $('#loadServicesSpinner').hide(); + Messages.error("Unable to remove service", {}, d[0].message); + } else { + Messages.send("Service deleted", service.Id); + var index = $scope.services.indexOf(service); + $scope.services.splice(index, 1); + } + complete(); + }, function (e) { + Messages.error("Failure", e, 'Unable to remove service'); + complete(); + }); + } + }); + }; + + function fetchServices() { + $('#loadServicesSpinner').show(); + Service.query({}, function (d) { + $scope.services = d.map(function (service) { + return new ServiceViewModel(service); + }); + $('#loadServicesSpinner').hide(); + }, function(e) { + $('#loadServicesSpinner').hide(); + Messages.error("Failure", e, "Unable to retrieve services"); + }); + } + + fetchServices(); +}]); diff --git a/app/components/stats/stats.html b/app/components/stats/stats.html index 66c77e2bc..d4c85d64b 100644 --- a/app/components/stats/stats.html +++ b/app/components/stats/stats.html @@ -10,7 +10,7 @@
- +
{{ container.Name|trimcontainername }}
diff --git a/app/components/swarm/swarm.html b/app/components/swarm/swarm.html index 2e09be30a..a9b5316e8 100644 --- a/app/components/swarm/swarm.html +++ b/app/components/swarm/swarm.html @@ -16,13 +16,14 @@ Nodes - {{ swarm.Nodes }} + {{ swarm.Nodes }} + {{ info.Swarm.Nodes }} - + Images {{ info.Images }} - + Swarm version {{ docker.Version|swarmversion }} @@ -30,27 +31,29 @@ Docker API version {{ docker.ApiVersion }} - + Strategy {{ swarm.Strategy }} Total CPU - {{ info.NCPU }} + {{ info.NCPU }} + {{ totalCPU }} Total memory - {{ info.MemTotal|humansize }} + {{ info.MemTotal|humansize }} + {{ totalMemory|humansize }} - + Operating system {{ info.OperatingSystem }} - + Kernel version {{ info.KernelVersion }} - + Go version {{ docker.GoVersion }} @@ -60,8 +63,9 @@
+
-
+
@@ -126,4 +130,69 @@
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Role + + + + + + CPU + + + + + + Memory + + + + + + Engine + + + + + + Status + + + +
{{ node.Description.Hostname }}{{ node.Spec.Role }}{{ node.Description.Resources.NanoCPUs / 1000000000 }}{{ node.Description.Resources.MemoryBytes|humansize }}{{ node.Description.Engine.EngineVersion }}{{ node.Status.State }}
+
+
+
diff --git a/app/components/swarm/swarmController.js b/app/components/swarm/swarmController.js index eca575753..b0a1435fc 100644 --- a/app/components/swarm/swarmController.js +++ b/app/components/swarm/swarmController.js @@ -1,62 +1,80 @@ angular.module('swarm', []) - .controller('SwarmController', ['$scope', 'Info', 'Version', 'Settings', - function ($scope, Info, Version, Settings) { +.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', +function ($scope, Info, Version, Node) { - $scope.sortType = 'Name'; - $scope.sortReverse = true; - $scope.info = {}; - $scope.docker = {}; - $scope.swarm = {}; + $scope.sortType = 'Name'; + $scope.sortReverse = true; + $scope.info = {}; + $scope.docker = {}; + $scope.swarm = {}; + $scope.swarm_mode = false; + $scope.totalCPU = 0; + $scope.totalMemory = 0; - $scope.order = function(sortType) { - $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; - $scope.sortType = sortType; - }; + $scope.order = function(sortType) { + $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; - Version.get({}, function (d) { - $scope.docker = d; - }); - Info.get({}, function (d) { - $scope.info = d; - extractSwarmInfo(d); + Version.get({}, function (d) { + $scope.docker = d; + }); + + Info.get({}, function (d) { + $scope.info = d; + if (!_.startsWith(d.ServerVersion, 'swarm')) { + $scope.swarm_mode = true; + Node.query({}, function(d) { + $scope.nodes = d; + var CPU = 0, memory = 0; + angular.forEach(d, function(node) { + CPU += node.Description.Resources.NanoCPUs; + memory += node.Description.Resources.MemoryBytes; + }); + $scope.totalCPU = CPU / 1000000000; + $scope.totalMemory = memory; }); + } else { + extractSwarmInfo(d); + } + }); - function extractSwarmInfo(info) { - // Swarm info is available in SystemStatus object - var systemStatus = info.SystemStatus; - // Swarm strategy - $scope.swarm[systemStatus[1][0]] = systemStatus[1][1]; - // Swarm filters - $scope.swarm[systemStatus[2][0]] = systemStatus[2][1]; - // Swarm node count - var node_count = parseInt(systemStatus[3][1], 10); - $scope.swarm[systemStatus[3][0]] = node_count; + function extractSwarmInfo(info) { + // Swarm info is available in SystemStatus object + var systemStatus = info.SystemStatus; + // Swarm strategy + $scope.swarm[systemStatus[1][0]] = systemStatus[1][1]; + // Swarm filters + $scope.swarm[systemStatus[2][0]] = systemStatus[2][1]; + // Swarm node count + var node_count = parseInt(systemStatus[3][1], 10); + $scope.swarm[systemStatus[3][0]] = node_count; - $scope.swarm.Status = []; - extractNodesInfo(systemStatus, node_count); - } + $scope.swarm.Status = []; + extractNodesInfo(systemStatus, node_count); + } - function extractNodesInfo(info, node_count) { - // First information for node1 available at element #4 of SystemStatus - // The next 10 elements are information related to the node - var node_offset = 4; - for (i = 0; i < node_count; i++) { - extractNodeInfo(info, node_offset); - node_offset += 9; - } - } + function extractNodesInfo(info, node_count) { + // First information for node1 available at element #4 of SystemStatus + // The next 10 elements are information related to the node + var node_offset = 4; + for (i = 0; i < node_count; i++) { + extractNodeInfo(info, node_offset); + node_offset += 9; + } + } - function extractNodeInfo(info, offset) { - var node = {}; - node.name = info[offset][0]; - node.ip = info[offset][1]; - node.id = info[offset + 1][1]; - node.status = info[offset + 2][1]; - node.containers = info[offset + 3][1]; - node.cpu = info[offset + 4][1].split('/')[1]; - node.memory = info[offset + 5][1].split('/')[1]; - node.labels = info[offset + 6][1]; - node.version = info[offset + 8][1]; - $scope.swarm.Status.push(node); - } - }]); + function extractNodeInfo(info, offset) { + var node = {}; + node.name = info[offset][0]; + node.ip = info[offset][1]; + node.id = info[offset + 1][1]; + node.status = info[offset + 2][1]; + node.containers = info[offset + 3][1]; + node.cpu = info[offset + 4][1].split('/')[1]; + node.memory = info[offset + 5][1].split('/')[1]; + node.labels = info[offset + 6][1]; + node.version = info[offset + 8][1]; + $scope.swarm.Status.push(node); + } +}]); diff --git a/app/components/task/task.html b/app/components/task/task.html new file mode 100644 index 000000000..1769c8c1b --- /dev/null +++ b/app/components/task/task.html @@ -0,0 +1,50 @@ + + + + + + Services > {{ serviceName }} > {{ task.ID }} + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID{{ task.ID }}
State{{ task.Status.State }}
Error message{{ task.Status.Err }}
Image{{ task.Spec.ContainerSpec.Image }}
Slot{{ task.Slot }}
Created{{ task.CreatedAt|getisodate }}
Container ID{{ task.Status.ContainerStatus.ContainerID }}
+
+
+
+
diff --git a/app/components/task/taskController.js b/app/components/task/taskController.js new file mode 100644 index 000000000..e6a1dc33e --- /dev/null +++ b/app/components/task/taskController.js @@ -0,0 +1,29 @@ +angular.module('task', []) +.controller('TaskController', ['$scope', '$stateParams', '$state', 'Task', 'Service', 'Messages', +function ($scope, $stateParams, $state, Task, Service, Messages) { + + $scope.task = {}; + $scope.serviceName = 'service'; + $scope.isTaskRunning = false; + + function fetchTaskDetails() { + $('#loadingViewSpinner').show(); + Task.get({id: $stateParams.id}, function (d) { + $scope.task = d; + fetchAssociatedServiceDetails(d.ServiceID); + $('#loadingViewSpinner').hide(); + }, function (e) { + Messages.error("Failure", e, "Unable to retrieve task details"); + }); + } + + function fetchAssociatedServiceDetails(serviceId) { + Service.get({id: serviceId}, function (d) { + $scope.serviceName = d.Spec.Name; + }, function (e) { + Messages.error("Failure", e, "Unable to retrieve associated service details"); + }); + } + + fetchTaskDetails(); +}]); diff --git a/app/components/templates/templates.html b/app/components/templates/templates.html index fe4b06be9..40a42bedc 100644 --- a/app/components/templates/templates.html +++ b/app/components/templates/templates.html @@ -34,11 +34,17 @@
-
+
When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the networks view to create one.
+
+
+ + App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host. +
+
diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 030d62050..3c373f1c1 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -1,6 +1,6 @@ angular.module('templates', []) -.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'Messages', -function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image, Volume, Network, Templates, Messages) { +.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'Volume', 'Network', 'Templates', 'Messages', +function ($scope, $q, $state, $filter, Config, Info, Container, ContainerHelper, Image, Volume, Network, Templates, Messages) { $scope.templates = []; $scope.selectedTemplate = null; $scope.formValues = { @@ -165,6 +165,11 @@ function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image Config.$promise.then(function (c) { $scope.swarm = c.swarm; + Info.get({}, function(info) { + if ($scope.swarm && !_.startsWith(info.ServerVersion, 'swarm')) { + $scope.swarm_mode = true; + } + }); var containersToHideLabels = c.hiddenLabels; Network.query({}, function (d) { var networks = d; diff --git a/app/components/volumes/volumes.html b/app/components/volumes/volumes.html index 869ec72b4..701f4c761 100644 --- a/app/components/volumes/volumes.html +++ b/app/components/volumes/volumes.html @@ -16,7 +16,7 @@
- + Add volume
diff --git a/app/shared/filters.js b/app/shared/filters.js index 21b4ea019..339b7f85a 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -18,6 +18,24 @@ angular.module('portainer.filters', []) } }; }) +.filter('taskstatusbadge', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + if (status.indexOf('new') !== -1 || status.indexOf('allocated') !== -1 || + status.indexOf('assigned') !== -1 || status.indexOf('accepted') !== -1) { + return 'info'; + } else if (status.indexOf('pending') !== -1) { + return 'warning'; + } else if (status.indexOf('shutdown') !== -1 || status.indexOf('failed') !== -1 || + status.indexOf('rejected') !== -1) { + return 'danger'; + } else if (status.indexOf('complete') !== -1) { + return 'primary'; + } + return 'success'; + }; +}) .filter('containerstatusbadge', function () { 'use strict'; return function (text) { @@ -191,4 +209,10 @@ angular.module('portainer.filters', []) return function (obj) { return _.isEmpty(obj); }; +}) +.filter('ipaddress', function () { + 'use strict'; + return function (ip) { + return ip.slice(0, ip.indexOf('/')); + }; }); diff --git a/app/shared/helpers.js b/app/shared/helpers.js index 7c51e4e3d..ad200560d 100644 --- a/app/shared/helpers.js +++ b/app/shared/helpers.js @@ -34,4 +34,18 @@ angular.module('portainer.helpers', []) }); } }; +}]) +.factory('ServiceHelper', [function ServiceHelperFactory() { + 'use strict'; + return { + serviceToConfig: function(service) { + return { + Name: service.Spec.Name, + TaskTemplate: service.Spec.TaskTemplate, + Mode: service.Spec.Mode, + Networks: service.Spec.Networks, + EndpointSpec: service.Spec.EndpointSpec + }; + } + }; }]); diff --git a/app/shared/services.js b/app/shared/services.js index c6cc56501..62ba0f8c3 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -37,6 +37,25 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize']) } }); }]) + .factory('Service', ['$resource', 'Settings', function ServiceFactory($resource, Settings) { + 'use strict'; + // https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-9-services + return $resource(Settings.url + '/services/:id/:action', {}, { + get: { method: 'GET', params: {id: '@id'} }, + query: { method: 'GET', isArray: true }, + create: { method: 'POST', params: {action: 'create'} }, + update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} }, + remove: { method: 'DELETE', params: {id: '@id'} } + }); + }]) + .factory('Task', ['$resource', 'Settings', function TaskFactory($resource, Settings) { + 'use strict'; + // https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-9-services + return $resource(Settings.url + '/tasks/:id', {}, { + get: { method: 'GET', params: {id: '@id'} }, + query: { method: 'GET', isArray: true, params: {filters: '@filters'} } + }); + }]) .factory('Exec', ['$resource', 'Settings', function ExecFactory($resource, Settings) { 'use strict'; // https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/exec-resize @@ -131,6 +150,22 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize']) get: {method: 'GET'} }); }]) + .factory('Node', ['$resource', 'Settings', function NodeFactory($resource, Settings) { + 'use strict'; + // https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-7-nodes + return $resource(Settings.url + '/nodes', {}, { + query: { + method: 'GET', isArray: true + } + }); + }]) + .factory('Swarm', ['$resource', 'Settings', function SwarmFactory($resource, Settings) { + 'use strict'; + // https://docs.docker.com/engine/reference/api/docker_remote_api_<%= remoteApiVersion %>/#/3-8-swarm + return $resource(Settings.url + '/swarm', {}, { + get: {method: 'GET'} + }); + }]) .factory('Auth', ['$resource', 'Settings', function AuthFactory($resource, Settings) { 'use strict'; // http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#check-auth-configuration diff --git a/app/shared/viewmodel.js b/app/shared/viewmodel.js index ec02f1755..b505ac162 100644 --- a/app/shared/viewmodel.js +++ b/app/shared/viewmodel.js @@ -8,6 +8,47 @@ function ImageViewModel(data) { this.VirtualSize = data.VirtualSize; } +function TaskViewModel(data, node_data) { + this.Id = data.ID; + this.Created = data.CreatedAt; + this.Updated = data.UpdatedAt; + this.Slot = data.Slot; + this.Status = data.Status.State; + if (node_data) { + for (var i = 0; i < node_data.length; ++i) { + if (data.NodeID === node_data[i].ID) { + this.Node = node_data[i].Description.Hostname; + } + } + } +} + +function ServiceViewModel(data) { + this.Model = data; + this.Id = data.ID; + this.Name = data.Spec.Name; + this.Image = data.Spec.TaskTemplate.ContainerSpec.Image; + this.Version = data.Version.Index; + if (data.Spec.Mode.Replicated) { + this.Mode = 'replicated' ; + this.Replicas = data.Spec.Mode.Replicated.Replicas; + } else { + this.Mode = 'global'; + } + if (data.Spec.Labels) { + this.Labels = data.Spec.Labels; + } + if (data.Spec.TaskTemplate.ContainerSpec.Env) { + this.Env = data.Spec.TaskTemplate.ContainerSpec.Env; + } + if (data.Endpoint.Ports) { + this.Ports = data.Endpoint.Ports; + } + this.Checked = false; + this.Scale = false; + this.EditName = false; +} + function ContainerViewModel(data) { this.Id = data.Id; this.Status = data.Status; diff --git a/assets/css/app.css b/assets/css/app.css index 4bbb71015..38711bdb9 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -255,3 +255,19 @@ input[type="radio"] { text-align: center; font-size: 0.8em; } + +.btn-responsive { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +@media screen and (min-width: 1107px) { + .btn-responsive { + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + border-radius: 4px; + } +} diff --git a/bower.json b/bower.json index ddcd38464..f04b4f56f 100644 --- a/bower.json +++ b/bower.json @@ -1,9 +1,9 @@ { "name": "portainer", - "version": "1.8.1", - "homepage": "https://github.com/cloud-inovasi/portainer", + "version": "1.9.0", + "homepage": "https://github.com/portainer/portainer", "authors": [ - "Anthony Lapenna " + "Anthony Lapenna " ], "description": "A web interface for the Docker Remote API.", "keywords": [ diff --git a/dashboard.png b/dashboard.png deleted file mode 100644 index 7bdb73e86..000000000 Binary files a/dashboard.png and /dev/null differ diff --git a/examples/nginx-basic-auth/Dockerfile b/examples/nginx-basic-auth/Dockerfile deleted file mode 100644 index 505a3951c..000000000 --- a/examples/nginx-basic-auth/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM nginx:latest - -COPY default.conf /etc/nginx/conf.d/default.conf -COPY users.htpasswd /etc/nginx/users.htpasswd diff --git a/examples/nginx-basic-auth/default.conf b/examples/nginx-basic-auth/default.conf deleted file mode 100644 index c39a2db16..000000000 --- a/examples/nginx-basic-auth/default.conf +++ /dev/null @@ -1,17 +0,0 @@ -upstream portainer { - server portainer:9000; -} - -server { - listen 80; - server_name localhost; - - location / { - auth_basic "Docker UI"; - auth_basic_user_file /etc/nginx/users.htpasswd; - - proxy_http_version 1.1; - proxy_set_header Connection ""; - proxy_pass http://portainer; - } -} diff --git a/examples/nginx-basic-auth/docker-compose.yml b/examples/nginx-basic-auth/docker-compose.yml deleted file mode 100644 index f644ffd5b..000000000 --- a/examples/nginx-basic-auth/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -portainer: - image: cloudinovasi/portainer - command: -e http://: - -nginx: - build: . - links: - - portainer - ports: - - 80:80 diff --git a/examples/nginx-basic-auth/users.htpasswd b/examples/nginx-basic-auth/users.htpasswd deleted file mode 100644 index 37d4fa4e5..000000000 --- a/examples/nginx-basic-auth/users.htpasswd +++ /dev/null @@ -1 +0,0 @@ -user:{PLAIN}password diff --git a/index.html b/index.html index 3b60136b7..e3db77937 100644 --- a/index.html +++ b/index.html @@ -44,6 +44,9 @@ + @@ -56,19 +59,22 @@ - - -
diff --git a/package.json b/package.json index d21b684b3..e434261d9 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { - "author": "Cloud Inovasi", + "author": "Portainer.io", "name": "portainer", - "homepage": "https://github.com/cloud-inovasi/portainer", - "version": "1.8.1", + "homepage": "http://portainer.io", + "version": "1.9.0", "repository": { "type": "git", - "url": "git@github.com:cloud-inovasi/portainer.git" + "url": "git@github.com:portainer/portainer.git" }, "bugs": { - "url": "https://github.com/cloud-inovasi/portainer/issues" + "url": "https://github.com/portainer/portainer/issues" }, "licenses": [ { "type": "MIT", - "url": "https://raw.githubusercontent.com/cloud-inovasi/portainer/develop/LICENSE" + "url": "https://raw.githubusercontent.com/portainer/portainer/develop/LICENSE" } ], "engines": {