Compare commits
187 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7afeb8a80d | ||
|
|
f8ced03792 | ||
|
|
1fdf56372b | ||
|
|
835b273700 | ||
|
|
fcc9203416 | ||
|
|
e25c5a014c | ||
|
|
fa9ba303aa | ||
|
|
e6dee37af0 | ||
|
|
d03e992b4f | ||
|
|
1a868be6ea | ||
|
|
e2fc8af87a | ||
|
|
70933d1056 | ||
|
|
7e0b0a05de | ||
|
|
980f65a08a | ||
|
|
8cf6d34362 | ||
|
|
70f139514f | ||
|
|
fa4ec04c47 | ||
|
|
7ebe4af77d | ||
|
|
579241db92 | ||
|
|
7d78871eee | ||
|
|
3a6e9d2fbe | ||
|
|
e4d98082dc | ||
|
|
cd26051144 | ||
|
|
27e584fc14 | ||
|
|
2bdc9322de | ||
|
|
35d5d75966 | ||
|
|
2610e3d02a | ||
|
|
d579f62fa7 | ||
|
|
d1b9820a29 | ||
|
|
13943c3d8b | ||
|
|
d8b800ddbc | ||
|
|
59f1a2f673 | ||
|
|
9ee652c818 | ||
|
|
816c1ea448 | ||
|
|
0bacaef71a | ||
|
|
2ef821f118 | ||
|
|
487cb4e755 | ||
|
|
06d3debf38 | ||
|
|
907f83aaff | ||
|
|
4b747a78cd | ||
|
|
d6f3dd8cda | ||
|
|
51632e367c | ||
|
|
6e98237419 | ||
|
|
ecc8857a32 | ||
|
|
7d05e81c37 | ||
|
|
6ce3fe7a9e | ||
|
|
9443284f52 | ||
|
|
4d6dadd17c | ||
|
|
d54d30a7be | ||
|
|
a08ea134fc | ||
|
|
c9ba16ef10 | ||
|
|
986171ecfe | ||
|
|
712b4528c0 | ||
|
|
03456ddcf8 | ||
|
|
ce32ed5b98 | ||
|
|
edeed41797 | ||
|
|
419727e1eb | ||
|
|
9165b5b215 | ||
|
|
0a38bba874 | ||
|
|
d9f6124609 | ||
|
|
5b16deb73e | ||
|
|
4e77c72fa2 | ||
|
|
1e5207517d | ||
|
|
2a28921984 | ||
|
|
b5bf7cdead | ||
|
|
8869a2c79c | ||
|
|
99d49a1f87 | ||
|
|
a53c0f08a3 | ||
|
|
0e40bb13fc | ||
|
|
db46087799 | ||
|
|
367a275672 | ||
|
|
b3a641e15a | ||
|
|
868b400af3 | ||
|
|
8fcae6810e | ||
|
|
913c580340 | ||
|
|
13a8b11d3d | ||
|
|
5af99c6fe3 | ||
|
|
2d35ac8f82 | ||
|
|
3db487f386 | ||
|
|
643769d4a6 | ||
|
|
2c49d3b5d9 | ||
|
|
714f515f0b | ||
|
|
672479bf4f | ||
|
|
8c3f7b3ec2 | ||
|
|
3aa0f4d263 | ||
|
|
2f35f04207 | ||
|
|
3b3b23142c | ||
|
|
9bd88fd10d | ||
|
|
3092d0b7eb | ||
|
|
d924d340d7 | ||
|
|
c1ffd02491 | ||
|
|
8e9dd8c2df | ||
|
|
1bfd6bbe95 | ||
|
|
715638e368 | ||
|
|
08c868bc1c | ||
|
|
9f46b12625 | ||
|
|
6fc25691bd | ||
|
|
c1713e0d01 | ||
|
|
8187f17d33 | ||
|
|
f0e194f63b | ||
|
|
eabf1f10e4 | ||
|
|
c913d858ee | ||
|
|
17f35ef705 | ||
|
|
0bdbb4a75d | ||
|
|
f9327b3337 | ||
|
|
bf6c9c8b3b | ||
|
|
45015a573b | ||
|
|
d4f0145161 | ||
|
|
fa53339fea | ||
|
|
e5396091a7 | ||
|
|
1ae18e1577 | ||
|
|
b953850a1f | ||
|
|
d0954abe29 | ||
|
|
c3cf5b5f9d | ||
|
|
6589730acc | ||
|
|
442dcff0f1 | ||
|
|
8bac1955a8 | ||
|
|
09a5534499 | ||
|
|
65c126f6a1 | ||
|
|
6adec680a4 | ||
|
|
b81d4fa7f2 | ||
|
|
d8f2e3da86 | ||
|
|
b0c0512515 | ||
|
|
bb9e044e89 | ||
|
|
520532cb9a | ||
|
|
44e09ecadf | ||
|
|
35ced4901a | ||
|
|
134416c9a3 | ||
|
|
8f7f4acc0d | ||
|
|
fde0d3ea9f | ||
|
|
477799af7e | ||
|
|
72570153a5 | ||
|
|
9f335b692f | ||
|
|
e88b22bd45 | ||
|
|
833053a2e1 | ||
|
|
64c52348f3 | ||
|
|
c3b79e6cc2 | ||
|
|
422a982d60 | ||
|
|
6e9fe26fde | ||
|
|
6bfa3096dc | ||
|
|
7cd2da4c6e | ||
|
|
739a5ec299 | ||
|
|
59e65222eb | ||
|
|
01d5d11c01 | ||
|
|
29a59cab44 | ||
|
|
be184c11a6 | ||
|
|
d6ab97ad25 | ||
|
|
6a0f76890e | ||
|
|
1946868248 | ||
|
|
84b02c711a | ||
|
|
679a681749 | ||
|
|
c35d1b14ec | ||
|
|
87df297a56 | ||
|
|
b8e420e0e8 | ||
|
|
f8c8668863 | ||
|
|
ced0746a81 | ||
|
|
39909d774f | ||
|
|
12e6e0557d | ||
|
|
e27282de3c | ||
|
|
fe63f9939a | ||
|
|
b623a5d452 | ||
|
|
d8113df979 | ||
|
|
b3ba36c02a | ||
|
|
37863e3f74 | ||
|
|
da6f39b137 | ||
|
|
4fe63d7102 | ||
|
|
7c8881f37d | ||
|
|
c20069fce0 | ||
|
|
2eb1c9e857 | ||
|
|
48e1fe769e | ||
|
|
2b8bc82d4e | ||
|
|
8f33151647 | ||
|
|
8e743a8d32 | ||
|
|
9f22e01d3b | ||
|
|
502c8718c5 | ||
|
|
220faa52e7 | ||
|
|
857c93bff9 | ||
|
|
ca5cf33c8f | ||
|
|
1cd620a45e | ||
|
|
4eb9a9a0af | ||
|
|
c82abae8e5 | ||
|
|
f56256f897 | ||
|
|
e31749e64d | ||
|
|
89d666f365 | ||
|
|
b502852966 | ||
|
|
e101397a2c | ||
|
|
ddcecc06d4 |
44
.github/ISSUE_TEMPLATE.md
vendored
Normal file
44
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
<!--
|
||||
|
||||
Thanks for opening an issue on Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on gitter: https://gitter.im/portainer/Lobby.
|
||||
|
||||
If you are reporting a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
||||
|
||||
If you suspect your issue is a bug, please edit your issue description to
|
||||
include the BUG REPORT INFORMATION shown below.
|
||||
|
||||
---------------------------------------------------
|
||||
BUG REPORT INFORMATION
|
||||
---------------------------------------------------
|
||||
You do NOT have to include this information if this is a FEATURE REQUEST
|
||||
-->
|
||||
|
||||
**Description**
|
||||
|
||||
<!--
|
||||
Briefly describe the problem you are having in a few paragraphs.
|
||||
-->
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
|
||||
|
||||
**Technical details:**
|
||||
|
||||
* Portainer version:
|
||||
* Portainer Docker image tag (latest/arm/windows...):
|
||||
* Target Docker version (the host/cluster you manage):
|
||||
* Target Swarm version (if applicable):
|
||||
* Platform (windows/linux):
|
||||
* Browser:
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,10 +1,6 @@
|
||||
logs/*
|
||||
!.gitkeep
|
||||
*.esproj/*
|
||||
node_modules
|
||||
bower_components
|
||||
.idea
|
||||
*.iml
|
||||
dist
|
||||
dist/*
|
||||
portainer-checksum.txt
|
||||
api/cmd/portainer/portainer*
|
||||
.tmp
|
||||
|
||||
77
CONTRIBUTING.md
Normal file
77
CONTRIBUTING.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 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.
|
||||
|
||||
* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
|
||||
* Develop in a topic branch, not master/develop
|
||||
|
||||
When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
|
||||
|
||||
For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`.
|
||||
|
||||
### Issues open to contribution
|
||||
|
||||
Want to contribute but don't know where to start?
|
||||
|
||||
Some of the open issues are labeled with prefix `exp/`, this is used to mark them as available for contributors to work on. All of these have an attributed difficulty level:
|
||||
|
||||
* **beginner**: a task that should be accessible with users not familiar with the codebase
|
||||
* **intermediate**: a task that require some understanding of the project codebase or some experience in
|
||||
either AngularJS or Golang
|
||||
|
||||
You can have a use Github filters to list these issues:
|
||||
|
||||
* beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner
|
||||
* intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
|
||||
|
||||
### 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**:
|
||||
|
||||
```
|
||||
<type>(<scope>): <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(containers): add exposed ports in the containers view
|
||||
#270 fix(templates): fix a display issue in the templates view
|
||||
#269 style(dashboard): update dashboard with new layout
|
||||
```
|
||||
|
||||
#### 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...
|
||||
You can use the **area** label tag associated on the issue here (for `area/containers` use `containers` as a scope...)
|
||||
|
||||
#### 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
|
||||
34
LICENSE
34
LICENSE
@@ -1,24 +1,24 @@
|
||||
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
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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.
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)
|
||||
Portainer contains code which was originally under this license:
|
||||
|
||||
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
|
||||
|
||||
174
README.md
174
README.md
@@ -1,162 +1,52 @@
|
||||
# Portainer
|
||||
|
||||
Portainer is a web interface for the Docker remote API.
|
||||
<p align="center">
|
||||
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
|
||||
</p>
|
||||
|
||||

|
||||
[](https://microbadger.com/images/portainer/portainer "Latest version on Docker Hub")
|
||||
[](http://microbadger.com/images/portainer/portainer "Image size")
|
||||
[](http://portainer.readthedocs.io/en/latest/?badge=stable)
|
||||
[](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
|
||||
|
||||
## Supported Docker versions
|
||||
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
|
||||
|
||||
The following Docker versions are supported:
|
||||
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (Docker for Linux and Docker for Windows 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)
|
||||
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm*.
|
||||
|
||||
## Run
|
||||
## Demo
|
||||
|
||||
### Quickstart
|
||||
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
|
||||
|
||||
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/portainer`
|
||||
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **demo** and the password **tryportainer**).
|
||||
|
||||
2. Open your browser to `http://<dockerd host ip>:9000`
|
||||
Please note that the public demo cluster is **reset every 15min**.
|
||||
|
||||
Bind mounting the Unix socket into the Portainer container is much more secure than exposing your docker daemon over TCP.
|
||||
## Getting started
|
||||
|
||||
The `--privileged` flag is required for hosts using SELinux.
|
||||
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
|
||||
* [Documentation](https://portainer.readthedocs.io)
|
||||
|
||||
### Specify socket to connect to Docker daemon
|
||||
## Getting help
|
||||
|
||||
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`.
|
||||
* Issues: https://github.com/portainer/portainer/issues
|
||||
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
|
||||
* Gitter (chat): https://gitter.im/portainer/Lobby
|
||||
* Slack: http://portainer.io/slack/
|
||||
|
||||
You can use the `--host`, `-H` flags to change this socket:
|
||||
## Reporting bugs and contributing
|
||||
|
||||
```
|
||||
# Connect to a tcp socket:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://127.0.0.1:2375
|
||||
```
|
||||
* Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
|
||||
* Want to help us build **_portainer_**? Follow our [contribution guidelines](https://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get!
|
||||
|
||||
```
|
||||
# Connect to another unix socket:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H unix:///path/to/docker.sock
|
||||
```
|
||||
## Limitations
|
||||
|
||||
### Swarm support
|
||||
**_Portainer_** has full support for the following Docker versions:
|
||||
|
||||
**Supported Swarm version: 1.2.3**
|
||||
* Docker 1.10 to Docker 1.12 (including `swarm-mode`)
|
||||
* Docker Swarm >= 1.2.3
|
||||
|
||||
You can access a specific view for you Swarm cluster by defining the `--swarm` flag:
|
||||
Partial support for the following Docker versions (some features may not be available):
|
||||
|
||||
```
|
||||
# Connect to a tcp socket and enable Swarm:
|
||||
$ docker run -d -p 9000:9000 cloudinovasi/portainer -H tcp://<SWARM_HOST>:<SWARM_PORT> --swarm
|
||||
```
|
||||
|
||||
*NOTE*: Due to Swarm not exposing information in a machine readable way, the app is bound to a specific version of Swarm at the moment.
|
||||
|
||||
### 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:
|
||||
|
||||
```
|
||||
# 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
|
||||
```
|
||||
|
||||
### 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/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.
|
||||
|
||||
For example, take a container started with the label `owner=acme`:
|
||||
|
||||
```
|
||||
$ 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
|
||||
```
|
||||
|
||||
### Reverse proxy configuration
|
||||
|
||||
Has been tested with Nginx 1.11.
|
||||
|
||||
Use the following configuration to host the UI at `myhost.mydomain.com/portainer`:
|
||||
|
||||
```nginx
|
||||
upstream portainer {
|
||||
server ADDRESS:PORT;
|
||||
}
|
||||
|
||||
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/;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `ADDRESS:PORT` with the Portainer container details.
|
||||
|
||||
### Host your own apps
|
||||
|
||||
You can specify an URL to your own templates (**Apps**) definitions using the `--templates` or `-t` flags.
|
||||
|
||||
By default, CloudInovasi templates will be used (https://raw.githubusercontent.com/cloud-inovasi/ui-templates/master/templates.json).
|
||||
|
||||
For more information about hosting your own template definition and the format, see: https://github.com/cloud-inovasi/ui-templates
|
||||
|
||||
### Available options
|
||||
|
||||
The following options are available for the `portainer` binary:
|
||||
|
||||
* `--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
|
||||
* Docker 1.9
|
||||
|
||||
60
api/api.go
60
api/api.go
@@ -1,60 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type (
|
||||
api struct {
|
||||
endpoint *url.URL
|
||||
bindAddress string
|
||||
assetPath string
|
||||
dataPath string
|
||||
tlsConfig *tls.Config
|
||||
templatesURL string
|
||||
}
|
||||
|
||||
apiConfig struct {
|
||||
Endpoint string
|
||||
BindAddress string
|
||||
AssetPath string
|
||||
DataPath string
|
||||
SwarmSupport bool
|
||||
TLSEnabled bool
|
||||
TLSCACertPath string
|
||||
TLSCertPath string
|
||||
TLSKeyPath string
|
||||
TemplatesURL string
|
||||
}
|
||||
)
|
||||
|
||||
func (a *api) run(settings *Settings) {
|
||||
handler := a.newHandler(settings)
|
||||
if err := http.ListenAndServe(a.bindAddress, handler); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newAPI(apiConfig apiConfig) *api {
|
||||
endpointURL, err := url.Parse(apiConfig.Endpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if apiConfig.TLSEnabled {
|
||||
tlsConfig = newTLSConfig(apiConfig.TLSCACertPath, apiConfig.TLSCertPath, apiConfig.TLSKeyPath)
|
||||
}
|
||||
|
||||
return &api{
|
||||
endpoint: endpointURL,
|
||||
bindAddress: apiConfig.BindAddress,
|
||||
assetPath: apiConfig.AssetPath,
|
||||
dataPath: apiConfig.DataPath,
|
||||
tlsConfig: tlsConfig,
|
||||
templatesURL: apiConfig.TemplatesURL,
|
||||
}
|
||||
}
|
||||
72
api/bolt/datastore.go
Normal file
72
api/bolt/datastore.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
// BoltDB as the storage system.
|
||||
type Store struct {
|
||||
// Path where is stored the BoltDB database.
|
||||
Path string
|
||||
|
||||
// Services
|
||||
UserService *UserService
|
||||
EndpointService *EndpointService
|
||||
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
const (
|
||||
databaseFileName = "portainer.db"
|
||||
userBucketName = "users"
|
||||
endpointBucketName = "endpoints"
|
||||
activeEndpointBucketName = "activeEndpoint"
|
||||
)
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
func NewStore(storePath string) *Store {
|
||||
store := &Store{
|
||||
Path: storePath,
|
||||
UserService: &UserService{},
|
||||
EndpointService: &EndpointService{},
|
||||
}
|
||||
store.UserService.store = store
|
||||
store.EndpointService.store = store
|
||||
return store
|
||||
}
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
func (store *Store) Open() error {
|
||||
path := store.Path + "/" + databaseFileName
|
||||
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.db = db
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(userBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(activeEndpointBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Close closes the BoltDB database.
|
||||
func (store *Store) Close() error {
|
||||
if store.db != nil {
|
||||
return store.db.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
174
api/bolt/endpoint_service.go
Normal file
174
api/bolt/endpoint_service.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// EndpointService represents a service for managing users.
|
||||
type EndpointService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
const (
|
||||
activeEndpointID = 0
|
||||
)
|
||||
|
||||
// Endpoint returns an endpoint by ID.
|
||||
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||
value := bucket.Get(internal.Itob(int(ID)))
|
||||
if value == nil {
|
||||
return portainer.ErrEndpointNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var endpoint portainer.Endpoint
|
||||
err = internal.UnmarshalEndpoint(data, &endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
// Endpoints return an array containing all the endpoints.
|
||||
func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var endpoint portainer.Endpoint
|
||||
err := internal.UnmarshalEndpoint(v, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// CreateEndpoint assign an ID to a new endpoint and saves it.
|
||||
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
endpoint.ID = portainer.EndpointID(id)
|
||||
|
||||
data, err := internal.MarshalEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateEndpoint updates an endpoint.
|
||||
func (service *EndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||
data, err := internal.MarshalEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteEndpoint deletes an endpoint.
|
||||
func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||
err := bucket.Delete(internal.Itob(int(ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetActive returns the active endpoint.
|
||||
func (service *EndpointService) GetActive() (*portainer.Endpoint, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||
value := bucket.Get(internal.Itob(activeEndpointID))
|
||||
if value == nil {
|
||||
return portainer.ErrEndpointNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var endpoint portainer.Endpoint
|
||||
err = internal.UnmarshalEndpoint(data, &endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
// SetActive saves an endpoint as active.
|
||||
func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||
|
||||
data, err := internal.MarshalEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(activeEndpointID), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteActive deletes the active endpoint.
|
||||
func (service *EndpointService) DeleteActive() error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||
err := bucket.Delete(internal.Itob(activeEndpointID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
37
api/bolt/internal/internal.go
Normal file
37
api/bolt/internal/internal.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// MarshalUser encodes a user to binary format.
|
||||
func MarshalUser(user *portainer.User) ([]byte, error) {
|
||||
return json.Marshal(user)
|
||||
}
|
||||
|
||||
// UnmarshalUser decodes a user from a binary data.
|
||||
func UnmarshalUser(data []byte, user *portainer.User) error {
|
||||
return json.Unmarshal(data, user)
|
||||
}
|
||||
|
||||
// MarshalEndpoint encodes an endpoint to binary format.
|
||||
func MarshalEndpoint(endpoint *portainer.Endpoint) ([]byte, error) {
|
||||
return json.Marshal(endpoint)
|
||||
}
|
||||
|
||||
// UnmarshalEndpoint decodes an endpoint from a binary data.
|
||||
func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
|
||||
return json.Unmarshal(data, endpoint)
|
||||
}
|
||||
|
||||
// Itob returns an 8-byte big endian representation of v.
|
||||
// This function is typically used for encoding integer IDs to byte slices
|
||||
// so that they can be used as BoltDB keys.
|
||||
func Itob(v int) []byte {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(v))
|
||||
return b
|
||||
}
|
||||
56
api/bolt/user_service.go
Normal file
56
api/bolt/user_service.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// UserService represents a service for managing users.
|
||||
type UserService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// User returns a user by username.
|
||||
func (service *UserService) User(username string) (*portainer.User, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
value := bucket.Get([]byte(username))
|
||||
if value == nil {
|
||||
return portainer.ErrUserNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user portainer.User
|
||||
err = internal.UnmarshalUser(data, &user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateUser saves a user.
|
||||
func (service *UserService) UpdateUser(user *portainer.User) error {
|
||||
data, err := internal.MarshalUser(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
err = bucket.Put([]byte(user.Username), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
61
api/cli/cli.go
Normal file
61
api/cli/cli.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// Service implements the CLIService interface
|
||||
type Service struct{}
|
||||
|
||||
const (
|
||||
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
|
||||
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
|
||||
)
|
||||
|
||||
// ParseFlags parse the CLI flags and return a portainer.Flags struct
|
||||
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
kingpin.Version(version)
|
||||
|
||||
flags := &portainer.CLIFlags{
|
||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
// ValidateFlags validates the values of the flags.
|
||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
if *flags.Endpoint != "" {
|
||||
if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") {
|
||||
return errInvalidEnpointProtocol
|
||||
}
|
||||
|
||||
if strings.HasPrefix(*flags.Endpoint, "unix://") {
|
||||
socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
14
api/cli/defaults.go
Normal file
14
api/cli/defaults.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// +build !windows
|
||||
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultTLSVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
)
|
||||
12
api/cli/defaults_windows.go
Normal file
12
api/cli/defaults_windows.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\ProgramData\\Portainer"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultTLSVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\ProgramData\\Portainer\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\ProgramData\\Portainer\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\ProgramData\\Portainer\\certs\\key.pem"
|
||||
)
|
||||
@@ -1,46 +1,40 @@
|
||||
package main
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"fmt"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// pair defines a key/value pair
|
||||
type pair struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
type pairList []portainer.Pair
|
||||
|
||||
// pairList defines an array of Label
|
||||
type pairList []pair
|
||||
|
||||
// Set implementation for Labels
|
||||
// Set implementation for a list of portainer.Pair
|
||||
func (l *pairList) Set(value string) error {
|
||||
parts := strings.SplitN(value, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("expected NAME=VALUE got '%s'", value)
|
||||
}
|
||||
p := new(pair)
|
||||
p := new(portainer.Pair)
|
||||
p.Name = parts[0]
|
||||
p.Value = parts[1]
|
||||
*l = append(*l, *p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implementation for Labels
|
||||
// String implementation for a list of pair
|
||||
func (l *pairList) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsCumulative implementation for Labels
|
||||
// IsCumulative implementation for a list of pair
|
||||
func (l *pairList) IsCumulative() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// LabelParser defines a custom parser for Labels flags
|
||||
func pairs(s kingpin.Settings) (target *[]pair) {
|
||||
target = new([]pair)
|
||||
func pairs(s kingpin.Settings) (target *[]portainer.Pair) {
|
||||
target = new([]portainer.Pair)
|
||||
s.SetValue((*pairList)(target))
|
||||
return
|
||||
}
|
||||
92
api/cmd/portainer/main.go
Normal file
92
api/cmd/portainer/main.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main // import "github.com/portainer/portainer"
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt"
|
||||
"github.com/portainer/portainer/cli"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"github.com/portainer/portainer/file"
|
||||
"github.com/portainer/portainer/http"
|
||||
"github.com/portainer/portainer/jwt"
|
||||
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var cli portainer.CLIService = &cli.Service{}
|
||||
flags, err := cli.ParseFlags(portainer.APIVersion)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = cli.ValidateFlags(flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
settings := &portainer.Settings{
|
||||
HiddenLabels: *flags.Labels,
|
||||
Logo: *flags.Logo,
|
||||
}
|
||||
|
||||
fileService, err := file.NewService(*flags.Data, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var store = bolt.NewStore(*flags.Data)
|
||||
err = store.Open()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
jwtService, err := jwt.NewService()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var cryptoService portainer.CryptoService = &crypto.Service{}
|
||||
|
||||
// Initialize the active endpoint from the CLI only if there is no
|
||||
// active endpoint defined yet.
|
||||
var activeEndpoint *portainer.Endpoint
|
||||
if *flags.Endpoint != "" {
|
||||
activeEndpoint, err = store.EndpointService.GetActive()
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
activeEndpoint = &portainer.Endpoint{
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
}
|
||||
err = store.EndpointService.CreateEndpoint(activeEndpoint)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
Settings: settings,
|
||||
TemplatesURL: *flags.Templates,
|
||||
UserService: store.UserService,
|
||||
EndpointService: store.EndpointService,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
ActiveEndpoint: activeEndpoint,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
22
api/crypto/crypto.go
Normal file
22
api/crypto/crypto.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Service represents a service for encrypting/hashing data.
|
||||
type Service struct{}
|
||||
|
||||
// Hash hashes a string using the bcrypt algorithm
|
||||
func (*Service) Hash(data string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.
|
||||
func (*Service) CompareHashAndData(hash string, data string) error {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(data))
|
||||
}
|
||||
48
api/csrf.go
48
api/csrf.go
@@ -1,48 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/securecookie"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const keyFile = "authKey.dat"
|
||||
|
||||
// newAuthKey reuses an existing CSRF authkey if present or generates a new one
|
||||
func newAuthKey(path string) []byte {
|
||||
var authKey []byte
|
||||
authKeyPath := path + "/" + keyFile
|
||||
data, err := ioutil.ReadFile(authKeyPath)
|
||||
if err != nil {
|
||||
log.Print("Unable to find an existing CSRF auth key. Generating a new key.")
|
||||
authKey = securecookie.GenerateRandomKey(32)
|
||||
err := ioutil.WriteFile(authKeyPath, authKey, 0644)
|
||||
if err != nil {
|
||||
log.Fatal("Unable to persist CSRF auth key.")
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
authKey = data
|
||||
}
|
||||
return authKey
|
||||
}
|
||||
|
||||
// newCSRF initializes a new CSRF handler
|
||||
func newCSRFHandler(keyPath string) func(h http.Handler) http.Handler {
|
||||
authKey := newAuthKey(keyPath)
|
||||
return csrf.Protect(
|
||||
authKey,
|
||||
csrf.HttpOnly(false),
|
||||
csrf.Secure(false),
|
||||
)
|
||||
}
|
||||
|
||||
// newCSRFWrapper wraps a http.Handler to add the CSRF token
|
||||
func newCSRFWrapper(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
40
api/errors.go
Normal file
40
api/errors.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package portainer
|
||||
|
||||
// General errors.
|
||||
const (
|
||||
ErrUnauthorized = Error("Unauthorized")
|
||||
)
|
||||
|
||||
// User errors.
|
||||
const (
|
||||
ErrUserNotFound = Error("User not found")
|
||||
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
|
||||
)
|
||||
|
||||
// Endpoint errors.
|
||||
const (
|
||||
ErrEndpointNotFound = Error("Endpoint not found")
|
||||
ErrNoActiveEndpoint = Error("Undefined Docker endpoint")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
const (
|
||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||
)
|
||||
|
||||
// JWT errors.
|
||||
const (
|
||||
ErrSecretGeneration = Error("Unable to generate secret key")
|
||||
ErrInvalidJWTToken = Error("Invalid JWT token")
|
||||
)
|
||||
|
||||
// File errors.
|
||||
const (
|
||||
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
|
||||
)
|
||||
|
||||
// Error represents an application error.
|
||||
type Error string
|
||||
|
||||
// Error returns the error message.
|
||||
func (e Error) Error() string { return string(e) }
|
||||
24
api/exec.go
24
api/exec.go
@@ -1,24 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"log"
|
||||
)
|
||||
|
||||
// execContainer is used to create a websocket communication with an exec instance
|
||||
func (a *api) execContainer(ws *websocket.Conn) {
|
||||
qry := ws.Request().URL.Query()
|
||||
execID := qry.Get("id")
|
||||
|
||||
var host string
|
||||
if a.endpoint.Scheme == "tcp" {
|
||||
host = a.endpoint.Host
|
||||
} else if a.endpoint.Scheme == "unix" {
|
||||
host = a.endpoint.Path
|
||||
}
|
||||
|
||||
if err := hijack(host, a.endpoint.Scheme, "POST", "/exec/"+execID+"/start", a.tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
log.Fatalf("error during hijack: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
142
api/file/file.go
Normal file
142
api/file/file.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
|
||||
TLSStorePath = "tls"
|
||||
// TLSCACertFile represents the name on disk for a TLS CA file.
|
||||
TLSCACertFile = "ca.pem"
|
||||
// TLSCertFile represents the name on disk for a TLS certificate file.
|
||||
TLSCertFile = "cert.pem"
|
||||
// TLSKeyFile represents the name on disk for a TLS key file.
|
||||
TLSKeyFile = "key.pem"
|
||||
)
|
||||
|
||||
// Service represents a service for managing files and directories.
|
||||
type Service struct {
|
||||
dataStorePath string
|
||||
fileStorePath string
|
||||
}
|
||||
|
||||
// NewService initializes a new service. It creates a data directory and a directory to store files
|
||||
// inside this directory if they don't exist.
|
||||
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
service := &Service{
|
||||
dataStorePath: dataStorePath,
|
||||
fileStorePath: path.Join(dataStorePath, fileStorePath),
|
||||
}
|
||||
|
||||
// Checking if a mount directory exists is broken with Go on Windows.
|
||||
// This will need to be reviewed after the issue has been fixed in Go.
|
||||
// err := createDirectoryIfNotExist(dataStorePath, 0755)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
err := service.createDirectoryInStoreIfNotExist(TLSStorePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r.
|
||||
func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error {
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
endpointStorePath := path.Join(TLSStorePath, ID)
|
||||
err := service.createDirectoryInStoreIfNotExist(endpointStorePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileName string
|
||||
switch fileType {
|
||||
case portainer.TLSFileCA:
|
||||
fileName = TLSCACertFile
|
||||
case portainer.TLSFileCert:
|
||||
fileName = TLSCertFile
|
||||
case portainer.TLSFileKey:
|
||||
fileName = TLSKeyFile
|
||||
default:
|
||||
return portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
tlsFilePath := path.Join(endpointStorePath, fileName)
|
||||
err = service.createFileInStore(tlsFilePath, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
|
||||
func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType) (string, error) {
|
||||
var fileName string
|
||||
switch fileType {
|
||||
case portainer.TLSFileCA:
|
||||
fileName = TLSCACertFile
|
||||
case portainer.TLSFileCert:
|
||||
fileName = TLSCertFile
|
||||
case portainer.TLSFileKey:
|
||||
fileName = TLSKeyFile
|
||||
default:
|
||||
return "", portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil
|
||||
}
|
||||
|
||||
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
|
||||
func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID)
|
||||
err := os.RemoveAll(endpointPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
|
||||
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
|
||||
path := path.Join(service.fileStorePath, name)
|
||||
return createDirectoryIfNotExist(path, 0700)
|
||||
}
|
||||
|
||||
// createDirectoryIfNotExist creates a directory if it doesn't exists on the file system.
|
||||
func createDirectoryIfNotExist(path string, mode uint32) error {
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.Mkdir(path, os.FileMode(mode))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createFile creates a new file in the file store with the content from r.
|
||||
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/net/websocket"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// newHandler creates a new http.Handler with CSRF protection
|
||||
func (a *api) newHandler(settings *Settings) http.Handler {
|
||||
var (
|
||||
mux = http.NewServeMux()
|
||||
fileHandler = http.FileServer(http.Dir(a.assetPath))
|
||||
)
|
||||
|
||||
handler := a.newAPIHandler()
|
||||
CSRFHandler := newCSRFHandler(a.dataPath)
|
||||
|
||||
mux.Handle("/", fileHandler)
|
||||
mux.Handle("/dockerapi/", http.StripPrefix("/dockerapi", handler))
|
||||
mux.Handle("/ws/exec", websocket.Handler(a.execContainer))
|
||||
mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
|
||||
settingsHandler(w, r, settings)
|
||||
})
|
||||
mux.HandleFunc("/templates", func(w http.ResponseWriter, r *http.Request) {
|
||||
templatesHandler(w, r, a.templatesURL)
|
||||
})
|
||||
return CSRFHandler(newCSRFWrapper(mux))
|
||||
}
|
||||
|
||||
// newAPIHandler initializes a new http.Handler based on the URL scheme
|
||||
func (a *api) newAPIHandler() http.Handler {
|
||||
var handler http.Handler
|
||||
var endpoint = *a.endpoint
|
||||
if endpoint.Scheme == "tcp" {
|
||||
if a.tlsConfig != nil {
|
||||
handler = a.newTCPHandlerWithTLS(&endpoint)
|
||||
} else {
|
||||
handler = a.newTCPHandler(&endpoint)
|
||||
}
|
||||
} else if endpoint.Scheme == "unix" {
|
||||
socketPath := endpoint.Path
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Fatalf("Unix socket %s does not exist", socketPath)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
handler = a.newUnixHandler(socketPath)
|
||||
} else {
|
||||
log.Fatalf("Bad Docker enpoint: %v. Only unix:// and tcp:// are supported.", &endpoint)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
// newUnixHandler initializes a new UnixHandler
|
||||
func (a *api) newUnixHandler(e string) http.Handler {
|
||||
return &unixHandler{e}
|
||||
}
|
||||
|
||||
// newTCPHandler initializes a HTTP reverse proxy
|
||||
func (a *api) newTCPHandler(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return httputil.NewSingleHostReverseProxy(u)
|
||||
}
|
||||
|
||||
// newTCPHandlerWithL initializes a HTTPS reverse proxy with a TLS configuration
|
||||
func (a *api) newTCPHandlerWithTLS(u *url.URL) http.Handler {
|
||||
u.Scheme = "https"
|
||||
proxy := httputil.NewSingleHostReverseProxy(u)
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: a.tlsConfig,
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
96
api/http/auth_handler.go
Normal file
96
api/http/auth_handler.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// AuthHandler represents an HTTP API handler for managing authentication.
|
||||
type AuthHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
|
||||
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
|
||||
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
|
||||
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
||||
)
|
||||
|
||||
// NewAuthHandler returns a new instance of AuthHandler.
|
||||
func NewAuthHandler() *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.HandleFunc("/auth", h.handlePostAuth)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
var req postAuthRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var username = req.Username
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
username,
|
||||
}
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
|
||||
}
|
||||
|
||||
type postAuthRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
type postAuthResponse struct {
|
||||
JWT string `json:"jwt"`
|
||||
}
|
||||
159
api/http/docker_handler.go
Normal file
159
api/http/docker_handler.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||
type DockerHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
proxy http.Handler
|
||||
}
|
||||
|
||||
// NewDockerHandler returns a new instance of DockerHandler.
|
||||
func NewDockerHandler(middleWareService *middleWareService) *DockerHandler {
|
||||
h := &DockerHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
}
|
||||
h.PathPrefix("/").Handler(middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.proxyRequestsToDockerAPI(w, r)
|
||||
})))
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||
if handler.proxy != nil {
|
||||
handler.proxy.ServeHTTP(w, r)
|
||||
} else {
|
||||
Error(w, portainer.ErrNoActiveEndpoint, http.StatusNotFound, handler.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) setupProxy(endpoint *portainer.Endpoint) error {
|
||||
var proxy http.Handler
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLS {
|
||||
proxy, err = newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
proxy = newHTTPProxy(endpointURL)
|
||||
}
|
||||
} else {
|
||||
// Assume unix:// scheme
|
||||
proxy = newSocketProxy(endpointURL.Path)
|
||||
}
|
||||
handler.proxy = proxy
|
||||
return nil
|
||||
}
|
||||
|
||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves
|
||||
func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
req.Host = req.URL.Host
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
// explicitly disable User-Agent so it's not set to default value
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
|
||||
func newHTTPProxy(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return NewSingleHostReverseProxyWithHostHeader(u)
|
||||
}
|
||||
|
||||
func newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
u.Scheme = "https"
|
||||
proxy := NewSingleHostReverseProxyWithHostHeader(u)
|
||||
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxy.Transport = &http.Transport{
|
||||
TLSClientConfig: config,
|
||||
}
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func newSocketProxy(path string) http.Handler {
|
||||
return &unixSocketHandler{path}
|
||||
}
|
||||
|
||||
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
|
||||
type unixSocketHandler struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
}
|
||||
309
api/http/endpoint_handler.go
Normal file
309
api/http/endpoint_handler.go
Normal file
@@ -0,0 +1,309 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
|
||||
type EndpointHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
FileService portainer.FileService
|
||||
server *Server
|
||||
middleWareService *middleWareService
|
||||
}
|
||||
|
||||
// NewEndpointHandler returns a new instance of EndpointHandler.
|
||||
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
|
||||
h := &EndpointHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
}
|
||||
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostEndpoints(w, r)
|
||||
}))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetEndpoints(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetEndpoint(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePutEndpoint(w, r)
|
||||
}))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleDeleteEndpoint(w, r)
|
||||
}))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/active", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostEndpoint(w, r)
|
||||
}))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetEndpoints handles GET requests on /endpoints
|
||||
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
encodeJSON(w, endpoints, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpoints handles POST requests on /endpoints
|
||||
// if the active URL parameter is specified, will also define the new endpoint as the active endpoint.
|
||||
// /endpoints(?active=true|false)
|
||||
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
var req postEndpointsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.TLS {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||
endpoint.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
activeEndpointParameter := r.FormValue("active")
|
||||
if activeEndpointParameter != "" {
|
||||
active, err := strconv.ParseBool(activeEndpointParameter)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
if active == true {
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
type postEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
TLS bool
|
||||
}
|
||||
|
||||
type postEndpointsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||
// GET /endpoints/0 returns active endpoint
|
||||
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var endpoint *portainer.Endpoint
|
||||
if id == "0" {
|
||||
endpoint, err = handler.EndpointService.GetActive()
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if handler.server.ActiveEndpoint == nil {
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpoint handles POST requests on /endpoints/:id/active
|
||||
func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.server.updateActiveEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointsRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
}
|
||||
|
||||
if req.TLS {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||
endpoint.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
} else {
|
||||
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type putEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
TLS bool
|
||||
}
|
||||
|
||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||
// DELETE /endpoints/0 deletes the active endpoint
|
||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var endpoint *portainer.Endpoint
|
||||
if id == "0" {
|
||||
endpoint, err = handler.EndpointService.GetActive()
|
||||
endpointID = int(endpoint.ID)
|
||||
} else {
|
||||
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
}
|
||||
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if id == "0" {
|
||||
err = handler.EndpointService.DeleteActive()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
36
api/http/file_handler.go
Normal file
36
api/http/file_handler.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileHandler represents an HTTP API handler for managing static files.
|
||||
type FileHandler struct {
|
||||
http.Handler
|
||||
}
|
||||
|
||||
func newFileHandler(assetPath string) *FileHandler {
|
||||
h := &FileHandler{
|
||||
Handler: http.FileServer(http.Dir(assetPath)),
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func isHTML(acceptContent []string) bool {
|
||||
for _, accept := range acceptContent {
|
||||
if strings.Contains(accept, "text/html") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (fileHandler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !isHTML(r.Header["Accept"]) {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
fileHandler.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
84
api/http/handler.go
Normal file
84
api/http/handler.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Handler is a collection of all the service handlers.
|
||||
type Handler struct {
|
||||
AuthHandler *AuthHandler
|
||||
UserHandler *UserHandler
|
||||
EndpointHandler *EndpointHandler
|
||||
SettingsHandler *SettingsHandler
|
||||
TemplatesHandler *TemplatesHandler
|
||||
DockerHandler *DockerHandler
|
||||
WebSocketHandler *WebSocketHandler
|
||||
UploadHandler *UploadHandler
|
||||
FileHandler *FileHandler
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrInvalidJSON defines an error raised the app is unable to parse request data
|
||||
ErrInvalidJSON = portainer.Error("Invalid JSON")
|
||||
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
|
||||
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
||||
)
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/auth") {
|
||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
|
||||
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/endpoints") {
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
|
||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/upload") {
|
||||
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/websocket") {
|
||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
|
||||
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/") {
|
||||
h.FileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Error writes an API error message to the response and logger.
|
||||
func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) {
|
||||
// Log error.
|
||||
if logger != nil {
|
||||
logger.Printf("http error: %s (code=%d)", err, code)
|
||||
}
|
||||
|
||||
// Write generic error response.
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
|
||||
}
|
||||
|
||||
// errorResponse is a generic response for sending a error.
|
||||
type errorResponse struct {
|
||||
Err string `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
// handleNotAllowed writes an API error message to the response and sets the Allow header.
|
||||
func handleNotAllowed(w http.ResponseWriter, allowedMethods []string) {
|
||||
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
|
||||
}
|
||||
|
||||
// encodeJSON encodes v to w in JSON format. Error() is called if encoding fails.
|
||||
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, logger)
|
||||
}
|
||||
}
|
||||
63
api/http/middleware.go
Normal file
63
api/http/middleware.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Service represents a service to manage HTTP middlewares
|
||||
type middleWareService struct {
|
||||
jwtService portainer.JWTService
|
||||
}
|
||||
|
||||
func addMiddleware(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
|
||||
for _, mw := range middleware {
|
||||
h = mw(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (service *middleWareService) addMiddleWares(h http.Handler) http.Handler {
|
||||
h = service.middleWareSecureHeaders(h)
|
||||
h = service.middleWareAuthenticate(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// middleWareAuthenticate provides secure headers middleware for handlers
|
||||
func (*middleWareService) middleWareSecureHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// middleWareAuthenticate provides Authentication middleware for handlers
|
||||
func (service *middleWareService) middleWareAuthenticate(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var token string
|
||||
|
||||
// Get token from the Authorization header
|
||||
tokens, ok := r.Header["Authorization"]
|
||||
if ok && len(tokens) >= 1 {
|
||||
token = tokens[0]
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err := service.jwtService.VerifyToken(token)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
})
|
||||
}
|
||||
85
api/http/server.go
Normal file
85
api/http/server.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Server implements the portainer.Server interface
|
||||
type Server struct {
|
||||
BindAddress string
|
||||
AssetsPath string
|
||||
UserService portainer.UserService
|
||||
EndpointService portainer.EndpointService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
ActiveEndpoint *portainer.Endpoint
|
||||
Handler *Handler
|
||||
}
|
||||
|
||||
func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
|
||||
if endpoint != nil {
|
||||
server.ActiveEndpoint = endpoint
|
||||
server.Handler.WebSocketHandler.endpoint = endpoint
|
||||
err := server.Handler.DockerHandler.setupProxy(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = server.EndpointService.SetActive(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (server *Server) Start() error {
|
||||
middleWareService := &middleWareService{
|
||||
jwtService: server.JWTService,
|
||||
}
|
||||
|
||||
var authHandler = NewAuthHandler()
|
||||
authHandler.UserService = server.UserService
|
||||
authHandler.CryptoService = server.CryptoService
|
||||
authHandler.JWTService = server.JWTService
|
||||
var userHandler = NewUserHandler(middleWareService)
|
||||
userHandler.UserService = server.UserService
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
var settingsHandler = NewSettingsHandler(middleWareService)
|
||||
settingsHandler.settings = server.Settings
|
||||
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||
templatesHandler.templatesURL = server.TemplatesURL
|
||||
var dockerHandler = NewDockerHandler(middleWareService)
|
||||
var websocketHandler = NewWebSocketHandler()
|
||||
// EndpointHandler requires a reference to the server to be able to update the active endpoint.
|
||||
var endpointHandler = NewEndpointHandler(middleWareService)
|
||||
endpointHandler.EndpointService = server.EndpointService
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.server = server
|
||||
var uploadHandler = NewUploadHandler(middleWareService)
|
||||
uploadHandler.FileService = server.FileService
|
||||
var fileHandler = newFileHandler(server.AssetsPath)
|
||||
|
||||
server.Handler = &Handler{
|
||||
AuthHandler: authHandler,
|
||||
UserHandler: userHandler,
|
||||
EndpointHandler: endpointHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
TemplatesHandler: templatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
FileHandler: fileHandler,
|
||||
UploadHandler: uploadHandler,
|
||||
}
|
||||
err := server.updateActiveEndpoint(server.ActiveEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return http.ListenAndServe(server.BindAddress, server.Handler)
|
||||
}
|
||||
40
api/http/settings_handler.go
Normal file
40
api/http/settings_handler.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||
type SettingsHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
settings *portainer.Settings
|
||||
}
|
||||
|
||||
// NewSettingsHandler returns a new instance of SettingsHandler.
|
||||
func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
|
||||
h := &SettingsHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
}
|
||||
h.HandleFunc("/settings", h.handleGetSettings)
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetSettings handles GET requests on /settings
|
||||
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, handler.settings, handler.Logger)
|
||||
}
|
||||
53
api/http/templates_handler.go
Normal file
53
api/http/templates_handler.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||
type TemplatesHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
templatesURL string
|
||||
}
|
||||
|
||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||
func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler {
|
||||
h := &TemplatesHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
}
|
||||
h.Handle("/templates", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetTemplates(w, r)
|
||||
})))
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetTemplates handles GET requests on /templates
|
||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get(handler.templatesURL)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(body)
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
package main
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
)
|
||||
|
||||
// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config {
|
||||
// createTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func createTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
caCert, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
tlsConfig := &tls.Config{
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
return tlsConfig
|
||||
return config, nil
|
||||
}
|
||||
74
api/http/upload_handler.go
Normal file
74
api/http/upload_handler.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// UploadHandler represents an HTTP API handler for managing file uploads.
|
||||
type UploadHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
FileService portainer.FileService
|
||||
middleWareService *middleWareService
|
||||
}
|
||||
|
||||
// NewUploadHandler returns a new instance of UploadHandler.
|
||||
func NewUploadHandler(middleWareService *middleWareService) *UploadHandler {
|
||||
h := &UploadHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
}
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUploadTLS(w, r)
|
||||
})))
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
endpointID := vars["endpointID"]
|
||||
certificate := vars["certificate"]
|
||||
ID, err := strconv.Atoi(endpointID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
file, _, err := r.FormFile("file")
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var fileType portainer.TLSFileType
|
||||
switch certificate {
|
||||
case "ca":
|
||||
fileType = portainer.TLSFileCA
|
||||
case "cert":
|
||||
fileType = portainer.TLSFileCert
|
||||
case "key":
|
||||
fileType = portainer.TLSFileKey
|
||||
default:
|
||||
Error(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
258
api/http/user_handler.go
Normal file
258
api/http/user_handler.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// UserHandler represents an HTTP API handler for managing users.
|
||||
type UserHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
middleWareService *middleWareService
|
||||
}
|
||||
|
||||
// NewUserHandler returns a new instance of UserHandler.
|
||||
func NewUserHandler(middleWareService *middleWareService) *UserHandler {
|
||||
h := &UserHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
middleWareService: middleWareService,
|
||||
}
|
||||
h.Handle("/users", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUsers(w, r)
|
||||
})))
|
||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handleGetUser(w, r)
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePutUser(w, r)
|
||||
}))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h.handlePostUserPasswd(w, r)
|
||||
})))
|
||||
h.HandleFunc("/users/admin/check", h.handleGetAdminCheck)
|
||||
h.HandleFunc("/users/admin/init", h.handlePostAdminInit)
|
||||
return h
|
||||
}
|
||||
|
||||
// handlePostUsers handles POST requests on /users
|
||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
var req postUsersRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: req.Username,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type postUsersRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:username/passwd
|
||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
|
||||
var req postUserPasswdRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
valid := true
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
if err != nil {
|
||||
valid = false
|
||||
}
|
||||
|
||||
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
|
||||
}
|
||||
|
||||
type postUserPasswdRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
type postUserPasswdResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
// handleGetUser handles GET requests on /users/:username
|
||||
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
username := vars["username"]
|
||||
|
||||
user, err := handler.UserService.User(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
encodeJSON(w, &user, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutUser handles PUT requests on /users/:username
|
||||
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
|
||||
var req putUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: req.Username,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type putUserRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles GET requests on /users/admin/check
|
||||
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
encodeJSON(w, &user, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
var req postAdminInitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
Error(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type postAdminInitRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
@@ -1,17 +1,79 @@
|
||||
package main
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
||||
type WebSocketHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
endpoint *portainer.Endpoint
|
||||
}
|
||||
|
||||
// NewWebSocketHandler returns a new instance of WebSocketHandler.
|
||||
func NewWebSocketHandler() *WebSocketHandler {
|
||||
h := &WebSocketHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/websocket/exec", websocket.Handler(h.webSocketDockerExec))
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||
qry := ws.Request().URL.Query()
|
||||
execID := qry.Get("id")
|
||||
|
||||
// Should not be managed here
|
||||
endpoint, err := url.Parse(handler.endpoint.URL)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse endpoint URL: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
var host string
|
||||
if endpoint.Scheme == "tcp" {
|
||||
host = endpoint.Host
|
||||
} else if endpoint.Scheme == "unix" {
|
||||
host = endpoint.Path
|
||||
}
|
||||
|
||||
// Should not be managed here
|
||||
var tlsConfig *tls.Config
|
||||
if handler.endpoint.TLS {
|
||||
tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath,
|
||||
handler.endpoint.TLSCertPath,
|
||||
handler.endpoint.TLSKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := hijack(host, endpoint.Scheme, "POST", "/exec/"+execID+"/start", tlsConfig, true, ws, ws, ws, nil, nil); err != nil {
|
||||
log.Fatalf("error during hijack: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type execConfig struct {
|
||||
Tty bool
|
||||
Detach bool
|
||||
66
api/jwt/jwt.go
Normal file
66
api/jwt/jwt.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gorilla/securecookie"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Service represents a service for managing JWT tokens.
|
||||
type Service struct {
|
||||
secret []byte
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
Username string `json:"username"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens.
|
||||
func NewService() (*Service, error) {
|
||||
secret := securecookie.GenerateRandomKey(32)
|
||||
if secret == nil {
|
||||
return nil, portainer.ErrSecretGeneration
|
||||
}
|
||||
service := &Service{
|
||||
secret,
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// GenerateToken generates a new JWT token.
|
||||
func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) {
|
||||
expireToken := time.Now().Add(time.Hour * 8).Unix()
|
||||
cl := claims{
|
||||
data.Username,
|
||||
jwt.StandardClaims{
|
||||
ExpiresAt: expireToken,
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
|
||||
|
||||
signedToken, err := token.SignedString(service.secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
// VerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
|
||||
func (service *Service) VerifyToken(token string) error {
|
||||
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
msg := fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||
return nil, msg
|
||||
}
|
||||
return service.secret, nil
|
||||
})
|
||||
if err != nil || parsedToken == nil || !parsedToken.Valid {
|
||||
return portainer.ErrInvalidJWTToken
|
||||
}
|
||||
return nil
|
||||
}
|
||||
47
api/main.go
47
api/main.go
@@ -1,47 +0,0 @@
|
||||
package main // import "github.com/cloudinovasi/portainer"
|
||||
|
||||
import (
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// main is the entry point of the program
|
||||
func main() {
|
||||
kingpin.Version("1.8.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()
|
||||
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
|
||||
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
|
||||
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()
|
||||
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()
|
||||
)
|
||||
kingpin.Parse()
|
||||
|
||||
apiConfig := apiConfig{
|
||||
Endpoint: *endpoint,
|
||||
BindAddress: *addr,
|
||||
AssetPath: *assets,
|
||||
DataPath: *data,
|
||||
SwarmSupport: *swarm,
|
||||
TLSEnabled: *tlsverify,
|
||||
TLSCACertPath: *tlscacert,
|
||||
TLSCertPath: *tlscert,
|
||||
TLSKeyPath: *tlskey,
|
||||
TemplatesURL: *templates,
|
||||
}
|
||||
|
||||
settings := &Settings{
|
||||
Swarm: *swarm,
|
||||
HiddenLabels: *labels,
|
||||
Logo: *logo,
|
||||
}
|
||||
|
||||
api := newAPI(apiConfig)
|
||||
api.run(settings)
|
||||
}
|
||||
132
api/portainer.go
Normal file
132
api/portainer.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package portainer
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type (
|
||||
// Pair defines a key/value string pair
|
||||
Pair struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// CLIFlags represents the available flags on the CLI.
|
||||
CLIFlags struct {
|
||||
Addr *string
|
||||
Assets *string
|
||||
Data *string
|
||||
Endpoint *string
|
||||
Labels *[]Pair
|
||||
Logo *string
|
||||
Templates *string
|
||||
TLSVerify *bool
|
||||
TLSCacert *string
|
||||
TLSCert *string
|
||||
TLSKey *string
|
||||
}
|
||||
|
||||
// Settings represents Portainer settings.
|
||||
Settings struct {
|
||||
HiddenLabels []Pair `json:"hiddenLabels"`
|
||||
Logo string `json:"logo"`
|
||||
}
|
||||
|
||||
// User represent a user account.
|
||||
User struct {
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
}
|
||||
|
||||
// TokenData represents the data embedded in a JWT token.
|
||||
TokenData struct {
|
||||
Username string
|
||||
}
|
||||
|
||||
// EndpointID represents an endpoint identifier.
|
||||
EndpointID int
|
||||
|
||||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it.
|
||||
Endpoint struct {
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
TLS bool `json:"TLS"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
|
||||
// TLSFileType represents a type of TLS file required to connect to a Docker endpoint.
|
||||
// It can be either a TLS CA file, a TLS certificate file or a TLS key file.
|
||||
TLSFileType int
|
||||
|
||||
// CLIService represents a service for managing CLI.
|
||||
CLIService interface {
|
||||
ParseFlags(version string) (*CLIFlags, error)
|
||||
ValidateFlags(flags *CLIFlags) error
|
||||
}
|
||||
|
||||
// DataStore defines the interface to manage the data.
|
||||
DataStore interface {
|
||||
Open() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Server defines the interface to serve the data.
|
||||
Server interface {
|
||||
Start() error
|
||||
}
|
||||
|
||||
// UserService represents a service for managing users.
|
||||
UserService interface {
|
||||
User(username string) (*User, error)
|
||||
UpdateUser(user *User) error
|
||||
}
|
||||
|
||||
// EndpointService represents a service for managing endpoints.
|
||||
EndpointService interface {
|
||||
Endpoint(ID EndpointID) (*Endpoint, error)
|
||||
Endpoints() ([]Endpoint, error)
|
||||
CreateEndpoint(endpoint *Endpoint) error
|
||||
UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error
|
||||
DeleteEndpoint(ID EndpointID) error
|
||||
GetActive() (*Endpoint, error)
|
||||
SetActive(endpoint *Endpoint) error
|
||||
DeleteActive() error
|
||||
}
|
||||
|
||||
// CryptoService represents a service for encrypting/hashing data.
|
||||
CryptoService interface {
|
||||
Hash(data string) (string, error)
|
||||
CompareHashAndData(hash string, data string) error
|
||||
}
|
||||
|
||||
// JWTService represents a service for managing JWT tokens.
|
||||
JWTService interface {
|
||||
GenerateToken(data *TokenData) (string, error)
|
||||
VerifyToken(token string) error
|
||||
}
|
||||
|
||||
// FileService represents a service for managing files.
|
||||
FileService interface {
|
||||
StoreTLSFile(endpointID EndpointID, fileType TLSFileType, r io.Reader) error
|
||||
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
|
||||
DeleteTLSFiles(endpointID EndpointID) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of portainer API.
|
||||
APIVersion = "1.11.2"
|
||||
)
|
||||
|
||||
const (
|
||||
// TLSFileCA represents a TLS CA certificate file.
|
||||
TLSFileCA TLSFileType = iota
|
||||
// TLSFileCert represents a TLS certificate file.
|
||||
TLSFileCert
|
||||
// TLSFileKey represents a TLS key file.
|
||||
TLSFileKey
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Settings defines the settings available under the /settings endpoint
|
||||
type Settings struct {
|
||||
Swarm bool `json:"swarm"`
|
||||
HiddenLabels pairList `json:"hiddenLabels"`
|
||||
Logo string `json:"logo"`
|
||||
}
|
||||
|
||||
// settingsHandler defines a handler function used to encode the configuration in JSON
|
||||
func settingsHandler(w http.ResponseWriter, r *http.Request, s *Settings) {
|
||||
json.NewEncoder(w).Encode(*s)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// templatesHandler defines a handler function used to retrieve the templates from a URL and put them in the response
|
||||
func templatesHandler(w http.ResponseWriter, r *http.Request, templatesURL string) {
|
||||
resp, err := http.Get(templatesURL)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Error making request to %s: %s", templatesURL, err.Error()), http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Error reading body from templates URL", http.StatusInternalServerError)
|
||||
log.Print(err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(body)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
)
|
||||
|
||||
// unixHandler defines a handler holding the path to a socket under UNIX
|
||||
type unixHandler struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// ServeHTTP implementation for unixHandler
|
||||
func (h *unixHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
copyHeader(w.Header(), res.Header)
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
518
app/app.js
518
app/app.js
@@ -5,9 +5,14 @@ angular.module('portainer', [
|
||||
'ui.select',
|
||||
'ngCookies',
|
||||
'ngSanitize',
|
||||
'ngFileUpload',
|
||||
'angularUtils.directives.dirPagination',
|
||||
'LocalStorageModule',
|
||||
'angular-jwt',
|
||||
'portainer.services',
|
||||
'portainer.helpers',
|
||||
'portainer.filters',
|
||||
'auth',
|
||||
'dashboard',
|
||||
'container',
|
||||
'containerConsole',
|
||||
@@ -15,125 +20,512 @@ angular.module('portainer', [
|
||||
'containers',
|
||||
'createContainer',
|
||||
'docker',
|
||||
'endpoint',
|
||||
'endpointInit',
|
||||
'endpoints',
|
||||
'events',
|
||||
'images',
|
||||
'image',
|
||||
'main',
|
||||
'service',
|
||||
'services',
|
||||
'settings',
|
||||
'sidebar',
|
||||
'createService',
|
||||
'stats',
|
||||
'swarm',
|
||||
'network',
|
||||
'networks',
|
||||
'node',
|
||||
'createNetwork',
|
||||
'task',
|
||||
'templates',
|
||||
'volumes',
|
||||
'createVolume'])
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider) {
|
||||
'use strict';
|
||||
|
||||
$httpProvider.defaults.xsrfCookieName = 'csrfToken';
|
||||
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
|
||||
localStorageServiceProvider
|
||||
.setStorageType('sessionStorage')
|
||||
.setPrefix('portainer');
|
||||
|
||||
$urlRouterProvider.otherwise('/');
|
||||
jwtOptionsProvider.config({
|
||||
tokenGetter: ['LocalStorage', function(LocalStorage) {
|
||||
return LocalStorage.getJWT();
|
||||
}],
|
||||
unauthenticatedRedirector: ['$state', function($state) {
|
||||
$state.go('auth', {error: 'Your session has expired'});
|
||||
}]
|
||||
});
|
||||
$httpProvider.interceptors.push('jwtInterceptor');
|
||||
|
||||
$urlRouterProvider.otherwise('/auth');
|
||||
|
||||
$stateProvider
|
||||
.state('index', {
|
||||
url: '/',
|
||||
templateUrl: 'app/components/dashboard/dashboard.html',
|
||||
controller: 'DashboardController'
|
||||
.state('auth', {
|
||||
url: '/auth',
|
||||
params: {
|
||||
logout: false,
|
||||
error: ''
|
||||
},
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/auth/auth.html',
|
||||
controller: 'AuthenticationController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('containers', {
|
||||
url: '/containers/',
|
||||
templateUrl: 'app/components/containers/containers.html',
|
||||
controller: 'ContainersController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/containers/containers.html',
|
||||
controller: 'ContainersController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('container', {
|
||||
url: "^/containers/:id",
|
||||
templateUrl: 'app/components/container/container.html',
|
||||
controller: 'ContainerController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/container/container.html',
|
||||
controller: 'ContainerController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('stats', {
|
||||
url: "^/containers/:id/stats",
|
||||
templateUrl: 'app/components/stats/stats.html',
|
||||
controller: 'StatsController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/stats/stats.html',
|
||||
controller: 'StatsController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('logs', {
|
||||
url: "^/containers/:id/logs",
|
||||
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
||||
controller: 'ContainerLogsController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
||||
controller: 'ContainerLogsController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('console', {
|
||||
url: "^/containers/:id/console",
|
||||
templateUrl: 'app/components/containerConsole/containerConsole.html',
|
||||
controller: 'ContainerConsoleController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/containerConsole/containerConsole.html',
|
||||
controller: 'ContainerConsoleController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('dashboard', {
|
||||
url: '/dashboard',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/dashboard/dashboard.html',
|
||||
controller: 'DashboardController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('actions', {
|
||||
abstract: true,
|
||||
url: "/actions",
|
||||
template: '<ui-view/>'
|
||||
views: {
|
||||
"content": {
|
||||
template: '<div ui-view="content"></div>'
|
||||
},
|
||||
"sidebar": {
|
||||
template: '<div ui-view="sidebar"></div>'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create', {
|
||||
abstract: true,
|
||||
url: "/create",
|
||||
template: '<ui-view/>'
|
||||
views: {
|
||||
"content": {
|
||||
template: '<div ui-view="content"></div>'
|
||||
},
|
||||
"sidebar": {
|
||||
template: '<div ui-view="sidebar"></div>'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.container', {
|
||||
url: "/container",
|
||||
templateUrl: 'app/components/createContainer/createcontainer.html',
|
||||
controller: 'CreateContainerController'
|
||||
})
|
||||
.state('actions.create.volume', {
|
||||
url: "/volume",
|
||||
templateUrl: 'app/components/createVolume/createvolume.html',
|
||||
controller: 'CreateVolumeController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/createContainer/createcontainer.html',
|
||||
controller: 'CreateContainerController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('actions.create.network', {
|
||||
url: "/network",
|
||||
templateUrl: 'app/components/createNetwork/createnetwork.html',
|
||||
controller: 'CreateNetworkController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/createNetwork/createnetwork.html',
|
||||
controller: 'CreateNetworkController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('actions.create.service', {
|
||||
url: "/service",
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/createService/createservice.html',
|
||||
controller: 'CreateServiceController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('actions.create.volume', {
|
||||
url: "/volume",
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/createVolume/createvolume.html',
|
||||
controller: 'CreateVolumeController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('docker', {
|
||||
url: '/docker/',
|
||||
templateUrl: 'app/components/docker/docker.html',
|
||||
controller: 'DockerController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/docker/docker.html',
|
||||
controller: 'DockerController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('endpoints', {
|
||||
url: '/endpoints/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/endpoints/endpoints.html',
|
||||
controller: 'EndpointsController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('endpoint', {
|
||||
url: '^/endpoints/:id',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/endpoint/endpoint.html',
|
||||
controller: 'EndpointController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('endpointInit', {
|
||||
url: '/init/endpoint',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/endpointInit/endpointInit.html',
|
||||
controller: 'EndpointInitController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('events', {
|
||||
url: '/events/',
|
||||
templateUrl: 'app/components/events/events.html',
|
||||
controller: 'EventsController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/events/events.html',
|
||||
controller: 'EventsController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('images', {
|
||||
url: '/images/',
|
||||
templateUrl: 'app/components/images/images.html',
|
||||
controller: 'ImagesController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/images/images.html',
|
||||
controller: 'ImagesController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('image', {
|
||||
url: '^/images/:id/',
|
||||
templateUrl: 'app/components/image/image.html',
|
||||
controller: 'ImageController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/image/image.html',
|
||||
controller: 'ImageController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('networks', {
|
||||
url: '/networks/',
|
||||
templateUrl: 'app/components/networks/networks.html',
|
||||
controller: 'NetworksController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/networks/networks.html',
|
||||
controller: 'NetworksController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('network', {
|
||||
url: '^/networks/:id/',
|
||||
templateUrl: 'app/components/network/network.html',
|
||||
controller: 'NetworkController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/network/network.html',
|
||||
controller: 'NetworkController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('node', {
|
||||
url: '^/nodes/:id/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/node/node.html',
|
||||
controller: 'NodeController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('services', {
|
||||
url: '/services/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/services/services.html',
|
||||
controller: 'ServicesController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('service', {
|
||||
url: '^/service/:id/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/service/service.html',
|
||||
controller: 'ServiceController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('settings', {
|
||||
url: '/settings/',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/settings/settings.html',
|
||||
controller: 'SettingsController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('task', {
|
||||
url: '^/task/:id',
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/task/task.html',
|
||||
controller: 'TaskController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('templates', {
|
||||
url: '/templates/',
|
||||
templateUrl: 'app/components/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('volumes', {
|
||||
url: '/volumes/',
|
||||
templateUrl: 'app/components/volumes/volumes.html',
|
||||
controller: 'VolumesController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/volumes/volumes.html',
|
||||
controller: 'VolumesController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
})
|
||||
.state('swarm', {
|
||||
url: '/swarm/',
|
||||
templateUrl: 'app/components/swarm/swarm.html',
|
||||
controller: 'SwarmController'
|
||||
views: {
|
||||
"content": {
|
||||
templateUrl: 'app/components/swarm/swarm.html',
|
||||
controller: 'SwarmController'
|
||||
},
|
||||
"sidebar": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: true
|
||||
}
|
||||
});
|
||||
|
||||
// The Docker API likes to return plaintext errors, this catches them and disp
|
||||
@@ -141,27 +533,39 @@ angular.module('portainer', [
|
||||
return {
|
||||
'response': function(response) {
|
||||
if (typeof(response.data) === 'string' &&
|
||||
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
|
||||
(response.data.startsWith('Conflict.') || response.data.startsWith('conflict:'))) {
|
||||
$.gritter.add({
|
||||
title: 'Error',
|
||||
text: $('<div>').text(response.data).html(),
|
||||
time: 10000
|
||||
});
|
||||
}
|
||||
var csrfToken = response.headers('X-Csrf-Token');
|
||||
if (csrfToken) {
|
||||
document.cookie = 'csrfToken=' + csrfToken;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
});
|
||||
}])
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', function ($rootScope, $state, Authentication, authManager, StateManager) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
authManager.redirectWhenUnauthenticated();
|
||||
|
||||
Authentication.init();
|
||||
StateManager.init();
|
||||
|
||||
$rootScope.$state = $state;
|
||||
|
||||
$rootScope.$on('tokenHasExpired', function($state) {
|
||||
$state.go('auth', {error: 'Your session has expired'});
|
||||
});
|
||||
}])
|
||||
// This is your docker url that the api will use to make requests
|
||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||
.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', 'settings')
|
||||
.constant('TEMPLATES_ENDPOINT', 'templates')
|
||||
.constant('UI_VERSION', 'v1.8.0');
|
||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('DOCKER_ENDPOINT', 'api/docker')
|
||||
.constant('CONFIG_ENDPOINT', 'api/settings')
|
||||
.constant('AUTH_ENDPOINT', 'api/auth')
|
||||
.constant('USERS_ENDPOINT', 'api/users')
|
||||
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
|
||||
.constant('TEMPLATES_ENDPOINT', 'api/templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.11.2');
|
||||
|
||||
101
app/components/auth/auth.html
Normal file
101
app/components/auth/auth.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<div class="page-wrapper">
|
||||
<!-- login box -->
|
||||
<div class="container simple-box">
|
||||
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
||||
<!-- login box logo -->
|
||||
<div class="row">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||
</div>
|
||||
<!-- !login box logo -->
|
||||
<!-- init password panel -->
|
||||
<div class="panel panel-default" ng-if="initPassword">
|
||||
<div class="panel-body">
|
||||
<!-- init password form -->
|
||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
Please specify a password for the <b>admin</b> user account.
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password.length >= 8]" aria-hidden="true"></i>
|
||||
Your password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- password input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="admin_password" type="password" class="form-control" name="password" ng-model="initPasswordData.password" autofocus>
|
||||
</div>
|
||||
<!-- !password input -->
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password !== '' && initPasswordData.password === initPasswordData.password_confirmation]" aria-hidden="true"></i>
|
||||
Confirm your password
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- password confirmation input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="password_confirmation" type="password" class="form-control" name="password" ng-model="initPasswordData.password_confirmation">
|
||||
</div>
|
||||
<!-- !password confirmation input -->
|
||||
<!-- validate button -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="initPasswordData.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Unable to create default user
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-disabled="initPasswordData.password.length < 8 || initPasswordData.password !== initPasswordData.password_confirmation" ng-click="createAdminUser()"><i class="fa fa-key" aria-hidden="true"></i> Validate</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !validate button -->
|
||||
</form>
|
||||
<!-- !init password form -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !init password panel -->
|
||||
<!-- login panel -->
|
||||
<div class="panel panel-default" ng-if="!initPassword">
|
||||
<div class="panel-body">
|
||||
<!-- login form -->
|
||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
||||
<!-- username input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
|
||||
<input id="username" type="text" class="form-control" name="username" ng-model="authData.username" placeholder="Username">
|
||||
</div>
|
||||
<!-- !username input -->
|
||||
<!-- password input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="password" type="password" class="form-control" name="password" ng-model="authData.password" autofocus>
|
||||
</div>
|
||||
<!-- !password input -->
|
||||
<!-- login button -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }}
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !login button -->
|
||||
</form>
|
||||
<!-- !login form -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !login panel -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !login box -->
|
||||
</div>
|
||||
80
app/components/auth/authController.js
Normal file
80
app/components/auth/authController.js
Normal file
@@ -0,0 +1,80 @@
|
||||
angular.module('auth', [])
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'Messages',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, Messages) {
|
||||
|
||||
$scope.authData = {
|
||||
username: 'admin',
|
||||
password: '',
|
||||
error: ''
|
||||
};
|
||||
$scope.initPasswordData = {
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
error: false
|
||||
};
|
||||
|
||||
if ($stateParams.logout) {
|
||||
Authentication.logout();
|
||||
}
|
||||
|
||||
if ($stateParams.error) {
|
||||
$scope.authData.error = $stateParams.error;
|
||||
Authentication.logout();
|
||||
}
|
||||
|
||||
if (Authentication.isAuthenticated()) {
|
||||
$state.go('dashboard');
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
});
|
||||
|
||||
Users.checkAdminUser({}, function (d) {},
|
||||
function (e) {
|
||||
if (e.status === 404) {
|
||||
$scope.initPassword = true;
|
||||
} else {
|
||||
Messages.error("Failure", e, 'Unable to verify administrator account existence');
|
||||
}
|
||||
});
|
||||
|
||||
$scope.createAdminUser = function() {
|
||||
var password = $sanitize($scope.initPasswordData.password);
|
||||
Users.initAdminUser({password: password}, function (d) {
|
||||
$scope.initPassword = false;
|
||||
$timeout(function() {
|
||||
var element = $window.document.getElementById('password');
|
||||
if(element) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
}, function (e) {
|
||||
$scope.initPassword.error = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.authenticateUser = function() {
|
||||
$scope.authenticationError = false;
|
||||
var username = $sanitize($scope.authData.username);
|
||||
var password = $sanitize($scope.authData.password);
|
||||
Authentication.login(username, password).then(function success() {
|
||||
EndpointService.getActive().then(function success(data) {
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}, function error(err) {
|
||||
if (err.status === 404) {
|
||||
$state.go('endpointInit');
|
||||
} else {
|
||||
Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
|
||||
}
|
||||
});
|
||||
}, function error() {
|
||||
$scope.authData.error = 'Invalid credentials';
|
||||
});
|
||||
};
|
||||
}]);
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
<rd-widget-header icon="fa-cogs" title="Actions"></rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button class="btn btn-primary" ng-click="start()" ng-if="!container.State.Running"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Start</button>
|
||||
<button class="btn btn-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop btn-ico" aria-hidden="true"></i>Stop</button>
|
||||
<button class="btn btn-danger" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb btn-ico" aria-hidden="true"></i>Kill</button>
|
||||
<button class="btn btn-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh btn-ico" aria-hidden="true"></i>Restart</button>
|
||||
<button class="btn btn-primary" ng-click="pause()" ng-if="container.State.Running && !container.State.Paused"><i class="fa fa-pause btn-ico" aria-hidden="true"></i>Pause</button>
|
||||
<button class="btn btn-primary" ng-click="unpause()" ng-if="container.State.Paused"><i class="fa fa-play btn-ico" aria-hidden="true"></i>Resume</button>
|
||||
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash btn-ico" aria-hidden="true"></i>Remove</button>
|
||||
<button class="btn btn-primary" ng-click="start()" ng-if="!container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
||||
<button class="btn btn-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
||||
<button class="btn btn-danger" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
||||
<button class="btn btn-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
|
||||
<button class="btn btn-primary" ng-click="pause()" ng-if="container.State.Running && !container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||
<button class="btn btn-primary" ng-click="unpause()" ng-if="container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Container status"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-server" title="Container status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
@@ -40,9 +40,11 @@
|
||||
<a href="" data-toggle="tooltip" title="Edit container name" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
<td ng-if="container.edit">
|
||||
<input type="text" class="containerNameInput" ng-model="container.newContainerName">
|
||||
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
|
||||
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
|
||||
<form ng-submit="renameContainer()">
|
||||
<input type="text" class="containerNameInput" ng-model="container.newContainerName">
|
||||
<a href="" ng-click="container.edit = false;"><i class="fa fa-times"></i></a>
|
||||
<a href="" ng-click="renameContainer()"><i class="fa fa-check-square-o"></i></a>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="container.NetworkSettings.IPAddress">
|
||||
@@ -52,7 +54,7 @@
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<i ng-class="{true: 'fa fa-heartbeat text-icon green-icon', false: 'fa fa-heartbeat text-icon red-icon'}[container.State.Running]"></i>
|
||||
<i ng-class="{true: 'fa fa-heartbeat space-right green-icon', false: 'fa fa-heartbeat space-right red-icon'}[container.State.Running]"></i>
|
||||
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -67,9 +69,9 @@
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart btn-ico" aria-hidden="true"></i>Stats</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle btn-ico" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal btn-ico" aria-hidden="true"></i>Console</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -130,7 +132,7 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Container details"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-server" title="Container details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
@@ -172,6 +174,23 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="container.HostConfig.RestartPolicy.Name !== 'no'">
|
||||
<td>Restart policies</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr>
|
||||
<td class="col-md-3">Name</td>
|
||||
<td>{{ container.HostConfig.RestartPolicy.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="col-md-3">MaximumRetryCount</td>
|
||||
<td>
|
||||
{{ container.HostConfig.RestartPolicy.MaximumRetryCount }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
@@ -202,3 +221,47 @@
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="!(container.NetworkSettings.Networks | emptyobject)">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Connected networks">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Network Name</th>
|
||||
<th>IP Address</th>
|
||||
<th>Gateway</th>
|
||||
<th>MacAddress</th>
|
||||
<th>Actions</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="(key, value) in container.NetworkSettings.Networks | itemsPerPage: state.pagination_count">
|
||||
<td><a ui-sref="network({id: value.NetworkID})">{{ key }}</a></td>
|
||||
<td>{{ value.IPAddress || '-' }}</td>
|
||||
<td>{{ value.Gateway || '-' }}</td>
|
||||
<td>{{ value.MacAddress || '-' }}</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(container, value.NetworkID)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Leave Network</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
angular.module('container', [])
|
||||
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Messages',
|
||||
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Messages) {
|
||||
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Pagination',
|
||||
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Pagination) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Registry: ''
|
||||
};
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('container_networks');
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('container_networks', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
var update = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
@@ -74,9 +80,9 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
|
||||
|
||||
$scope.commit = function () {
|
||||
$('#createImageSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var imageConfig = ImageHelper.createImageConfig(image, registry);
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
|
||||
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
$('#createImageSpinner').hide();
|
||||
update();
|
||||
@@ -153,5 +159,22 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
|
||||
$scope.container.edit = false;
|
||||
};
|
||||
|
||||
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.disconnect({id: networkId}, { Container: $stateParams.id, Force: false }, function (d) {
|
||||
if (d.message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Error", {}, d.message);
|
||||
} else {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Container left network", $stateParams.id);
|
||||
$state.go('container', {id: $stateParams.id}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to disconnect container from network");
|
||||
});
|
||||
};
|
||||
|
||||
update();
|
||||
}]);
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<rd-header-title title="Container console">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
|
||||
<rd-header-content ng-if="state.loaded">
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" ng-if="state.loaded">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-terminal" title="Console">
|
||||
@@ -16,18 +16,27 @@
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- command-list -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-3">
|
||||
<select class="selectpicker form-control" ng-model="state.command">
|
||||
<option value="bash">/bin/bash</option>
|
||||
<option value="sh">/bin/sh</option>
|
||||
</select>
|
||||
<form>
|
||||
<div class="row">
|
||||
<!-- command-list -->
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
|
||||
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
|
||||
</span>
|
||||
<select class="form-control" ng-model="state.command" id="command">
|
||||
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
|
||||
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
|
||||
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
|
||||
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-9 pull-left">
|
||||
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="connected">Connect</button>
|
||||
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!connected">Disconnect</button>
|
||||
<!-- !command-list -->
|
||||
<div class="col-sm-8">
|
||||
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="state.connected">Connect</button>
|
||||
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!state.connected">Disconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
angular.module('containerConsole', [])
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages',
|
||||
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'Messages',
|
||||
function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Messages) {
|
||||
$scope.state = {};
|
||||
$scope.state.command = "bash";
|
||||
$scope.connected = false;
|
||||
$scope.state.loaded = false;
|
||||
$scope.state.connected = false;
|
||||
|
||||
var socket, term;
|
||||
|
||||
// Ensure the socket is closed before leaving the view
|
||||
$scope.$on('$stateChangeStart', function (event, next, current) {
|
||||
if (socket !== null) {
|
||||
if (socket && socket !== null) {
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
|
||||
Container.get({id: $stateParams.id}, function(d) {
|
||||
$scope.container = d;
|
||||
if (d.message) {
|
||||
Messages.error("Error", d, 'Unable to retrieve container details');
|
||||
$('#loadingViewSpinner').hide();
|
||||
} else {
|
||||
Image.get({id: d.Image}, function(imgData) {
|
||||
$scope.imageOS = imgData.Os;
|
||||
$scope.state.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
|
||||
$scope.state.loaded = true;
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to retrieve image details');
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to retrieve container details');
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
|
||||
@@ -39,7 +55,7 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
|
||||
} else {
|
||||
var execId = d.Id;
|
||||
resizeTTY(execId, termHeight, termWidth);
|
||||
var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId;
|
||||
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId;
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
@@ -54,7 +70,7 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
|
||||
};
|
||||
|
||||
$scope.disconnect = function() {
|
||||
$scope.connected = false;
|
||||
$scope.state.connected = false;
|
||||
if (socket !== null) {
|
||||
socket.close();
|
||||
}
|
||||
@@ -79,31 +95,26 @@ function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages) {
|
||||
function initTerm(url, height, width) {
|
||||
socket = new WebSocket(url);
|
||||
|
||||
$scope.connected = true;
|
||||
$scope.state.connected = true;
|
||||
socket.onopen = function(evt) {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
term = new Terminal({
|
||||
cols: width,
|
||||
rows: height,
|
||||
cursorBlink: true
|
||||
});
|
||||
term = new Terminal();
|
||||
|
||||
term.on('data', function (data) {
|
||||
socket.send(data);
|
||||
});
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
term.resize(width, height);
|
||||
term.setOption('cursorBlink', true);
|
||||
|
||||
socket.onmessage = function (e) {
|
||||
term.write(e.data);
|
||||
};
|
||||
socket.onerror = function (error) {
|
||||
$scope.connected = false;
|
||||
|
||||
$scope.state.connected = false;
|
||||
};
|
||||
socket.onclose = function(evt) {
|
||||
$scope.connected = false;
|
||||
// term.write("Session terminated");
|
||||
// term.destroy();
|
||||
$scope.state.connected = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
<i class="fa fa-server"></i>
|
||||
</div>
|
||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
||||
<div class="comment">Name</div>
|
||||
|
||||
@@ -3,29 +3,37 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="containers" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadContainersSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Containers</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Containers">
|
||||
<rd-widget-header icon="fa-server" title="Containers">
|
||||
<div class="pull-right">
|
||||
<i id="loadContainersSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button type="button" class="btn btn-primary" ng-click="startAction()" ng-disabled="!state.selectedItemCount">Start</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="stopAction()" ng-disabled="!state.selectedItemCount">Stop</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="killAction()" ng-disabled="!state.selectedItemCount">Kill</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="restartAction()" ng-disabled="!state.selectedItemCount">Restart</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount">Pause</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount">Unpause</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="startAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="stopAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="killAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="restartAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="pauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||
<button type="button" class="btn btn-primary btn-responsive" ng-click="unpauseAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||
<button type="button" class="btn btn-danger btn-responsive" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<a class="btn btn-default" type="button" ui-sref="actions.create.container">Add container</a>
|
||||
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.container">Add container</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="checkbox" ng-model="state.displayAll" id="displayAll" ng-change="toggleGetAll()" style="margin-top: -2px; margin-right: 5px;"/><label for="displayAll">Show all containers</label>
|
||||
@@ -37,12 +45,14 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Status')">
|
||||
State
|
||||
<span ng-show="sortType == 'State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
@@ -66,7 +76,7 @@
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="swarm">
|
||||
<th ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<a ui-sref="containers" ng-click="order('Host')">
|
||||
Host IP
|
||||
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
@@ -75,7 +85,7 @@
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="containers" ng-click="order('Ports')">
|
||||
Exposed Ports
|
||||
Published Ports
|
||||
<span ng-show="sortType == 'Ports' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Ports' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
@@ -83,23 +93,32 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
|
||||
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status|containerstatus }}</span></td>
|
||||
<td ng-if="swarm"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
|
||||
<td ng-if="!swarm"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
||||
<td><span class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span></td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername}}</a></td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername}}</a></td>
|
||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
|
||||
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
||||
<td ng-if="swarm">{{ container.hostIP }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
|
||||
<td>
|
||||
<a ng-if="container.Ports.length > 0" ng-repeat="p in container.Ports" class="image-tag" ng-href="http://{{p.host}}:{{p.public}}" target="_blank">
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i> {{ p.private }}
|
||||
<i class="fa fa-external-link" aria-hidden="true"></i> {{p.public}}:{{ p.private }}
|
||||
</a>
|
||||
<span ng-if="container.Ports.length == 0" >-</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!containers">
|
||||
<td colspan="8" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="containers.length == 0">
|
||||
<td colspan="8" class="text-center text-muted">No containers available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="containers" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
|
||||
@@ -1,38 +1,47 @@
|
||||
angular.module('containers', [])
|
||||
.controller('ContainersController', ['$scope', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config',
|
||||
function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config) {
|
||||
|
||||
.controller('ContainersController', ['$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination',
|
||||
function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
|
||||
$scope.state.displayAll = Settings.displayAll;
|
||||
$scope.state.displayIP = false;
|
||||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = false;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
var update = function (data, containersToHideLabels) {
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('containers', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
var update = function (data) {
|
||||
$('#loadContainersSpinner').show();
|
||||
$scope.state.selectedItemCount = 0;
|
||||
Container.query(data, function (d) {
|
||||
var containers = d;
|
||||
if (containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
|
||||
if ($scope.containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
|
||||
}
|
||||
$scope.containers = containers.map(function (container) {
|
||||
var model = new ContainerViewModel(container);
|
||||
model.Status = $filter('containerstatus')(model.Status);
|
||||
|
||||
if (model.IP) {
|
||||
$scope.state.displayIP = true;
|
||||
}
|
||||
if ($scope.swarm) {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
|
||||
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
|
||||
}
|
||||
return model;
|
||||
});
|
||||
$('#loadContainersSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadContainersSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve containers");
|
||||
$scope.containers = [];
|
||||
});
|
||||
};
|
||||
|
||||
@@ -72,6 +81,19 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
|
||||
complete();
|
||||
});
|
||||
}
|
||||
else if (action === Container.pause) {
|
||||
action({id: c.Id}, function (d) {
|
||||
if (d.message) {
|
||||
Messages.send("Container is already paused", c.Id);
|
||||
} else {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to pause container');
|
||||
complete();
|
||||
});
|
||||
}
|
||||
else {
|
||||
action({id: c.Id}, function (d) {
|
||||
Messages.send("Container " + msg, c.Id);
|
||||
@@ -89,6 +111,15 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
|
||||
}
|
||||
};
|
||||
|
||||
$scope.selectItems = function (allSelected) {
|
||||
angular.forEach($scope.state.filteredContainers, function (container) {
|
||||
if (container.Checked !== allSelected) {
|
||||
container.Checked = allSelected;
|
||||
$scope.selectItem(container);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
@@ -145,17 +176,15 @@ function ($scope, Container, ContainerHelper, Info, Settings, Messages, Config)
|
||||
return swarm_hosts;
|
||||
}
|
||||
|
||||
$scope.swarm = false;
|
||||
Config.$promise.then(function (c) {
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
$scope.swarm = c.swarm;
|
||||
if (c.swarm) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
|
||||
Info.get({}, function (d) {
|
||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
||||
update({all: Settings.displayAll ? 1 : 0}, containersToHideLabels);
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
});
|
||||
} else {
|
||||
update({all: Settings.displayAll ? 1 : 0}, containersToHideLabels);
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
angular.module('createContainer', [])
|
||||
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages',
|
||||
function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
|
||||
|
||||
$scope.state = {
|
||||
alwaysPull: true
|
||||
};
|
||||
.controller('CreateContainerController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Messages',
|
||||
function ($scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Messages) {
|
||||
|
||||
$scope.formValues = {
|
||||
alwaysPull: true,
|
||||
Console: 'none',
|
||||
Volumes: [],
|
||||
AvailableRegistries: [],
|
||||
Registry: ''
|
||||
Registry: '',
|
||||
NetworkContainer: '',
|
||||
Labels: []
|
||||
};
|
||||
|
||||
$scope.imageConfig = {};
|
||||
|
||||
$scope.config = {
|
||||
Image: '',
|
||||
Env: [],
|
||||
ExposedPorts: {},
|
||||
HostConfig: {
|
||||
@@ -26,7 +25,8 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
|
||||
Binds: [],
|
||||
NetworkMode: 'bridge',
|
||||
Privileged: false
|
||||
}
|
||||
},
|
||||
Labels: {}
|
||||
};
|
||||
|
||||
$scope.addVolume = function() {
|
||||
@@ -53,10 +53,16 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
|
||||
$scope.config.HostConfig.PortBindings.splice(index, 1);
|
||||
};
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
var swarm = c.swarm;
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.formValues.AvailableRegistries = c.registries;
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
@@ -66,7 +72,7 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
|
||||
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if (swarm) {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
@@ -77,10 +83,24 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
|
||||
networks.push({Name: "host"});
|
||||
networks.push({Name: "none"});
|
||||
}
|
||||
networks.push({Name: "container"});
|
||||
$scope.availableNetworks = networks;
|
||||
if (!_.find(networks, {'Name': 'bridge'})) {
|
||||
$scope.config.HostConfig.NetworkMode = 'nat';
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve networks");
|
||||
});
|
||||
|
||||
Container.query({}, function (d) {
|
||||
var containers = d;
|
||||
if (containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
|
||||
}
|
||||
$scope.runningContainers = containers;
|
||||
}, function(e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve running containers");
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: centralize, already present in templatesController
|
||||
@@ -113,37 +133,17 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
|
||||
// TODO: centralize, already present in templatesController
|
||||
function pullImageAndCreateContainer(config) {
|
||||
Image.create($scope.imageConfig, function (data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
if (err) {
|
||||
var detail = data[data.length - 1];
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Error', {}, detail.error);
|
||||
} else {
|
||||
createContainer(config);
|
||||
}
|
||||
createContainer(config);
|
||||
}, function (e) {
|
||||
$('#createContainerSpinner').hide();
|
||||
Messages.error('Failure', e, 'Unable to pull image');
|
||||
});
|
||||
}
|
||||
|
||||
function createImageConfig(imageName, registry) {
|
||||
var imageNameAndTag = imageName.split(':');
|
||||
var image = imageNameAndTag[0];
|
||||
if (registry) {
|
||||
image = registry + '/' + imageNameAndTag[0];
|
||||
}
|
||||
var imageConfig = {
|
||||
fromImage: image,
|
||||
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
|
||||
};
|
||||
return imageConfig;
|
||||
}
|
||||
|
||||
function prepareImageConfig(config) {
|
||||
var image = _.toLower(config.Image);
|
||||
var image = config.Image;
|
||||
var registry = $scope.formValues.Registry;
|
||||
var imageConfig = createImageConfig(image, registry);
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
|
||||
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
$scope.imageConfig = imageConfig;
|
||||
}
|
||||
@@ -151,10 +151,10 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
|
||||
function preparePortBindings(config) {
|
||||
var bindings = {};
|
||||
config.HostConfig.PortBindings.forEach(function (portBinding) {
|
||||
if (portBinding.hostPort && portBinding.containerPort) {
|
||||
if (portBinding.containerPort) {
|
||||
var key = portBinding.containerPort + "/" + portBinding.protocol;
|
||||
var binding = {};
|
||||
if (portBinding.hostPort.indexOf(':') > -1) {
|
||||
if (portBinding.hostPort && portBinding.hostPort.indexOf(':') > -1) {
|
||||
var hostAndPort = portBinding.hostPort.split(':');
|
||||
binding.HostIp = hostAndPort[0];
|
||||
binding.HostPort = hostAndPort[1];
|
||||
@@ -214,24 +214,52 @@ function ($scope, $state, Config, Container, Image, Volume, Network, Messages) {
|
||||
config.Volumes = volumes;
|
||||
}
|
||||
|
||||
function prepareNetworkConfig(config) {
|
||||
var mode = config.HostConfig.NetworkMode;
|
||||
var container = $scope.formValues.NetworkContainer;
|
||||
var containerName = container;
|
||||
if (container && typeof container === 'object') {
|
||||
containerName = $filter('trimcontainername')(container.Names[0]);
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
|
||||
containerName = $filter('swarmcontainername')(container);
|
||||
}
|
||||
}
|
||||
var networkMode = mode;
|
||||
if (containerName) {
|
||||
networkMode += ':' + containerName;
|
||||
}
|
||||
config.HostConfig.NetworkMode = networkMode;
|
||||
}
|
||||
|
||||
function prepareLabels(config) {
|
||||
var labels = {};
|
||||
$scope.formValues.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
config.Labels = labels;
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
prepareNetworkConfig(config);
|
||||
prepareImageConfig(config);
|
||||
preparePortBindings(config);
|
||||
prepareConsole(config);
|
||||
prepareEnvironmentVariables(config);
|
||||
prepareVolumes(config);
|
||||
prepareLabels(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
var config = prepareConfiguration();
|
||||
$('#createContainerSpinner').show();
|
||||
if ($scope.state.alwaysPull) {
|
||||
if ($scope.formValues.alwaysPull) {
|
||||
pullImageAndCreateContainer(config);
|
||||
} else {
|
||||
createContainer(config);
|
||||
}
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create container"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > Add container
|
||||
<a ui-sref="containers">Containers</a> > Add container
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<div class="col-sm-offset-1 col-sm-11">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="state.alwaysPull"> Always pull image before creating
|
||||
<input type="checkbox" ng-model="formValues.alwaysPull"> Always pull image before creating
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,6 +53,10 @@
|
||||
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure">
|
||||
<span class="radio-value">On failure</span>
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="unless-stopped">
|
||||
<span class="radio-value">Unless stopped</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !restart-policy -->
|
||||
@@ -60,7 +64,7 @@
|
||||
<div class="form-group">
|
||||
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addPortBinding()">
|
||||
<span class="label label-default interactive" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
|
||||
</span>
|
||||
</div>
|
||||
@@ -69,14 +73,14 @@
|
||||
<div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80">
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select class="selectpicker form-control" ng-model="portBinding.protocol">
|
||||
<select class="form-control" ng-model="portBinding.protocol">
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
@@ -102,10 +106,11 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active clickable"><a data-target="#command" data-toggle="tab">Command</a></li>
|
||||
<li class="clickable"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="clickable"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
|
||||
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
|
||||
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||
<li class="interactive"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
|
||||
</ul>
|
||||
<!-- tab-content -->
|
||||
<div class="tab-content">
|
||||
@@ -177,7 +182,7 @@
|
||||
<div class="form-group">
|
||||
<label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addEnvironmentVariable()">
|
||||
<span class="label label-default interactive" ng-click="addEnvironmentVariable()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
|
||||
</span>
|
||||
</div>
|
||||
@@ -212,7 +217,7 @@
|
||||
<div class="form-group">
|
||||
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addVolume()">
|
||||
<span class="label label-default interactive" ng-click="addVolume()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
|
||||
</span>
|
||||
</div>
|
||||
@@ -228,7 +233,7 @@
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon"><input type="checkbox" ng-model="volume.isPath" ng-click="resetVolumePath($index)">Path</span>
|
||||
<select class="selectpicker form-control" ng-model="volume.name" ng-if="!volume.isPath">
|
||||
<select class="form-control" ng-model="volume.name" ng-if="!volume.isPath">
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||
</select>
|
||||
@@ -254,7 +259,7 @@
|
||||
<!-- tab-network -->
|
||||
<div class="tab-pane" id="network">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">You don't have any shared network. Head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||
</div>
|
||||
@@ -263,13 +268,26 @@
|
||||
<div class="form-group">
|
||||
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="selectpicker form-control" ng-model="config.HostConfig.NetworkMode">
|
||||
<select class="form-control" ng-model="config.HostConfig.NetworkMode" id="container_network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !network-input -->
|
||||
<!-- container-name-input -->
|
||||
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
|
||||
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
|
||||
<div class="col-sm-9">
|
||||
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<select ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !container-name-input -->
|
||||
<!-- hostname-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_hostname" class="col-sm-1 control-label text-left">Hostname</label>
|
||||
@@ -289,6 +307,41 @@
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-network -->
|
||||
<!-- tab-labels -->
|
||||
<div class="tab-pane" id="labels">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<label for="container_labels" class="col-sm-1 control-label text-left">Labels</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels-->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-labels -->
|
||||
<!-- tab-security -->
|
||||
<div class="tab-pane" id="security">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
|
||||
@@ -4,16 +4,21 @@ function ($scope, $state, Messages, Network) {
|
||||
$scope.formValues = {
|
||||
DriverOptions: [],
|
||||
Subnet: '',
|
||||
Gateway: ''
|
||||
Gateway: '',
|
||||
Labels: []
|
||||
};
|
||||
|
||||
$scope.config = {
|
||||
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: []
|
||||
}
|
||||
},
|
||||
Labels: {}
|
||||
};
|
||||
|
||||
$scope.addDriverOption = function() {
|
||||
@@ -24,6 +29,14 @@ function ($scope, $state, Messages, Network) {
|
||||
$scope.formValues.DriverOptions.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
function createNetwork(config) {
|
||||
$('#createNetworkSpinner').show();
|
||||
Network.create(config, function (d) {
|
||||
@@ -60,10 +73,21 @@ function ($scope, $state, Messages, Network) {
|
||||
config.Options = options;
|
||||
}
|
||||
|
||||
function prepareLabelsConfig(config) {
|
||||
var labels = {};
|
||||
$scope.formValues.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
config.Labels = labels;
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
prepareIPAMConfiguration(config);
|
||||
prepareDriverOptions(config);
|
||||
prepareLabelsConfig(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create network"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Networks > Add network
|
||||
<a ui-sref="networks">Networks</a> > Add network
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div class="form-group">
|
||||
<label for="network_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addDriverOption()">
|
||||
<span class="label label-default interactive" ng-click="addDriverOption()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
|
||||
</span>
|
||||
</div>
|
||||
@@ -78,6 +78,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !internal -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<label for="service_env" class="col-sm-1 control-label text-left">Labels</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels-->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
@@ -89,7 +118,7 @@
|
||||
<div>
|
||||
<i id="createNetworkSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-disabled="!config.Name" ng-click="create()">Create</button>
|
||||
<a type="button" class="btn btn-default btn-lg" ui-sref="networks">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
238
app/components/createService/createServiceController.js
Normal file
238
app/components/createService/createServiceController.js
Normal file
@@ -0,0 +1,238 @@
|
||||
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: '',
|
||||
EntryPoint: '',
|
||||
WorkingDir: '',
|
||||
User: '',
|
||||
Env: [],
|
||||
Labels: [],
|
||||
ContainerLabels: [],
|
||||
Volumes: [],
|
||||
Network: '',
|
||||
ExtraNetworks: [],
|
||||
Ports: [],
|
||||
Parallelism: 1,
|
||||
UpdateDelay: 0,
|
||||
FailureAction: 'pause'
|
||||
};
|
||||
|
||||
$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);
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addContainerLabel = function() {
|
||||
$scope.formValues.ContainerLabels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeContainerLabel = function(index) {
|
||||
$scope.formValues.ContainerLabels.splice(index, 1);
|
||||
};
|
||||
|
||||
function prepareImageConfig(config, input) {
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry);
|
||||
config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + 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 commandToArray(cmd) {
|
||||
var tokens = [].concat.apply([], cmd.split('"').map(function(v,i) {
|
||||
return i%2 ? v : v.split(' ');
|
||||
})).filter(Boolean);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function prepareCommandConfig(config, input) {
|
||||
if (input.EntryPoint) {
|
||||
config.TaskTemplate.ContainerSpec.Command = commandToArray(input.EntryPoint);
|
||||
}
|
||||
if (input.Command) {
|
||||
config.TaskTemplate.ContainerSpec.Args = commandToArray(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 prepareLabelsConfig(config, input) {
|
||||
var labels = {};
|
||||
input.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
config.Labels = labels;
|
||||
|
||||
var containerLabels = {};
|
||||
input.ContainerLabels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
containerLabels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
config.TaskTemplate.ContainerSpec.Labels = containerLabels;
|
||||
}
|
||||
|
||||
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 prepareUpdateConfig(config, input) {
|
||||
config.UpdateConfig = {
|
||||
Parallelism: input.Parallelism || 0,
|
||||
Delay: input.UpdateDelay || 0,
|
||||
FailureAction: input.FailureAction
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
prepareLabelsConfig(config, input);
|
||||
prepareVolumes(config, input);
|
||||
prepareNetworks(config, input);
|
||||
prepareUpdateConfig(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");
|
||||
});
|
||||
}]);
|
||||
395
app/components/createService/createservice.html
Normal file
395
app/components/createService/createservice.html
Normal file
@@ -0,0 +1,395 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create service"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="services">Services</a> > Add service
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" id="service_name" placeholder="e.g. myService">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- image-and-registry-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="service_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" ng-model="formValues.Image" id="service_image" placeholder="e.g. nginx:latest">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="leave empty to use DockerHub">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !image-and-registry-inputs -->
|
||||
<!-- scheduling-mode -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-1 control-label text-left">Scheduling mode</label>
|
||||
<div class="col-sm-11">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="global">
|
||||
Global
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="service_scheduling" ng-model="formValues.Mode" value="replicated">
|
||||
Replicated
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="formValues.Mode === 'replicated'">
|
||||
<label for="replicas" class="col-sm-1 control-label text-left">Replicas</label>
|
||||
<div class="col-sm-1">
|
||||
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3">
|
||||
</div>
|
||||
<div class="col-sm-10"></div>
|
||||
</div>
|
||||
<!-- !scheduling-mode -->
|
||||
<!-- port-mapping -->
|
||||
<div class="form-group">
|
||||
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
|
||||
</span>
|
||||
</div>
|
||||
<!-- port-mapping-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="portBinding in formValues.Ports" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 8080">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select class="form-control" ng-model="portBinding.Protocol">
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active interactive"><a data-target="#command" data-toggle="tab">Command</a></li>
|
||||
<li class="interactive"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
|
||||
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li>
|
||||
</ul>
|
||||
<!-- tab-content -->
|
||||
<div class="tab-content">
|
||||
<!-- tab-command -->
|
||||
<div class="tab-pane active" id="command">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- command-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_command" class="col-sm-1 control-label text-left">Command</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="formValues.Command" id="service_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command-input -->
|
||||
<!-- entrypoint-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_entrypoint" class="col-sm-1 control-label text-left">Entrypoint</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" ng-model="formValues.EntryPoint" id="service_entrypoint" placeholder="e.g. /bin/sh -c">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !entrypoint-input -->
|
||||
<!-- workdir-user-input -->
|
||||
<div class="form-group">
|
||||
<label for="service_workingdir" class="col-sm-1 control-label text-left">Working Dir</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="formValues.WorkingDir" id="service_workingdir" placeholder="e.g. /myapp">
|
||||
</div>
|
||||
<label for="service_user" class="col-sm-1 control-label text-left">User</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" ng-model="formValues.User" id="service_user" placeholder="e.g. nginx">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !workdir-user-input -->
|
||||
<!-- environment-variables -->
|
||||
<div class="form-group">
|
||||
<label for="service_env" class="col-sm-1 control-label text-left">Environment variables</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addEnvironmentVariable()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
|
||||
</span>
|
||||
</div>
|
||||
<!-- environment-variable-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="variable in formValues.Env" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !environment-variable-input-list -->
|
||||
</div>
|
||||
<!-- !environment-variables -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-command -->
|
||||
<!-- tab-volume -->
|
||||
<div class="tab-pane" id="volumes">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- volumes -->
|
||||
<div class="form-group">
|
||||
<label for="service_volumes" class="col-sm-1 control-label text-left">Volumes</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addVolume()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
|
||||
</span>
|
||||
</div>
|
||||
<!-- volumes-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="volume in formValues.Volumes" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="volume.ReadOnly"> Read-only
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon"><input type="checkbox" ng-model="volume.Bind">bind</span>
|
||||
<select class="form-control" ng-model="volume.Source" ng-if="!volume.Bind">
|
||||
<option selected disabled hidden value="">Select a volume</option>
|
||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||
</select>
|
||||
<input ng-if="volume.Bind" type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/on/host">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/in/container">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeVolume($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !volumes-input-list -->
|
||||
</div>
|
||||
<!-- !volumes -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-volume -->
|
||||
<!-- tab-network -->
|
||||
<div class="tab-pane" id="network">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- network-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" ng-model="formValues.Network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
</div>
|
||||
<!-- !network-input -->
|
||||
<!-- extra-networks -->
|
||||
<div class="form-group">
|
||||
<label for="service_extra_networks" class="col-sm-1 control-label text-left">Extra networks</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addExtraNetwork()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> network
|
||||
</span>
|
||||
</div>
|
||||
<!-- network-input-list -->
|
||||
<div style="margin-top: 10px;">
|
||||
<div class="col-sm-12" ng-repeat="network in formValues.ExtraNetworks" style="margin-top: 5px;">
|
||||
<div class="input-group col-sm-9 input-group-sm col-sm-offset-1">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeExtraNetwork($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
<select class="form-control" ng-model="network.Name">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !network-input-list -->
|
||||
</div>
|
||||
<!-- !extra-networks -->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-network -->
|
||||
<!-- tab-labels -->
|
||||
<div class="tab-pane" id="labels">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<label for="service_env" class="col-sm-1 control-label text-left">Labels</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels-->
|
||||
<!-- container-labels -->
|
||||
<div class="form-group">
|
||||
<label for="service_env" class="col-sm-1 control-label text-left">Container labels</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default interactive" ng-click="addContainerLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
|
||||
</span>
|
||||
</div>
|
||||
<!-- container-labels-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.ContainerLabels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeContainerLabel($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !container-labels-input-list -->
|
||||
</div>
|
||||
<!-- !container-labels-->
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-labels -->
|
||||
<!-- tab-update-config -->
|
||||
<div class="tab-pane" id="update-config">
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- parallelism-input -->
|
||||
<div class="form-group">
|
||||
<label for="parallelism" class="col-sm-1 control-label text-left">Parallelism</label>
|
||||
<div class="col-sm-1">
|
||||
<input type="number" class="form-control" ng-model="formValues.Parallelism" id="parallelism" placeholder="e.g. 1">
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Maximum number of tasks to be updated simultaneously (0 to update all at once).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !parallelism-input -->
|
||||
<!-- delay-input -->
|
||||
<div class="form-group">
|
||||
<label for="update-delay" class="col-sm-1 control-label text-left">Delay</label>
|
||||
<div class="col-sm-2">
|
||||
<input type="number" class="form-control" ng-model="formValues.UpdateDelay" id="update-delay" placeholder="e.g. 10">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<p class="small text-muted" style="margin-top: 10px;">
|
||||
Amount of time between updates.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !delay-input -->
|
||||
<!-- failureAction-input -->
|
||||
<div class="form-group">
|
||||
<label for="failure_action" class="col-sm-1 control-label text-left">Failure Action</label>
|
||||
<div class="col-sm-3">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="formValues.FailureAction" value="continue">
|
||||
Continue
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="formValues.FailureAction" value="pause">
|
||||
Pause
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-sm-8"></div>
|
||||
</div>
|
||||
<!-- !failureAction-input -->
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- !tab-update-config -->
|
||||
<!-- tab-security -->
|
||||
<div class="tab-pane" id="security">
|
||||
</div>
|
||||
<!-- !tab-security -->
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||
<div>
|
||||
<i id="createServiceSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-click="create()">Create</button>
|
||||
<a type="button" class="btn btn-default btn-lg" ui-sref="services">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Create volume"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Volumes > Add volume
|
||||
<a ui-sref="volumes">Volumes</a> > Add volume
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="form-group">
|
||||
<label for="volume_driveropts" class="col-sm-1 control-label text-left">Driver options</label>
|
||||
<div class="col-sm-11">
|
||||
<span class="label label-default clickable" ng-click="addDriverOption()">
|
||||
<span class="label label-default interactive" ng-click="addDriverOption()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> driver option
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!swarm">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Node info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
@@ -33,7 +33,7 @@
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="swarm">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Cluster info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
@@ -41,7 +41,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td>{{ infoData.SystemStatus[3][1] }}</td>
|
||||
<td>{{ infoData.SystemStatus[0][1] == 'primary' ? infoData.SystemStatus[3][1] : infoData.SystemStatus[4][1] }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swarm version</td>
|
||||
@@ -53,7 +53,29 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td>{{ infoData.MemTotal|humansize }}</td>
|
||||
<td>{{ infoData.MemTotal|humansize: 2 }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tachometer" title="Swarm info"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="small text-muted">This node is part of a Swarm cluster</span></td>
|
||||
</tr>
|
||||
<tr >
|
||||
<td>Node role</td>
|
||||
<td>{{ infoData.Swarm.ControlAvailable ? 'Manager' : 'Worker' }}</td>
|
||||
</tr>
|
||||
<tr ng-if="infoData.Swarm.ControlAvailable">
|
||||
<td>Nodes in the cluster</td>
|
||||
<td>{{ infoData.Swarm.Nodes }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -68,11 +90,11 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
<i class="fa fa-server"></i>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div><i class="fa fa-heartbeat text-icon green-icon"></i>{{ containerData.running }} running</div>
|
||||
<div><i class="fa fa-heartbeat text-icon red-icon"></i>{{ containerData.stopped }} stopped</div>
|
||||
<div><i class="fa fa-heartbeat space-right green-icon"></i>{{ containerData.running }} running</div>
|
||||
<div><i class="fa fa-heartbeat space-right red-icon"></i>{{ containerData.stopped }} stopped</div>
|
||||
</div>
|
||||
<div class="title">{{ containerData.total }}</div>
|
||||
<div class="comment">Containers</div>
|
||||
@@ -88,7 +110,7 @@
|
||||
<i class="fa fa-clone"></i>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<div><i class="fa fa-pie-chart text-icon"></i>{{ imageData.size|humansize }}</div>
|
||||
<div><i class="fa fa-pie-chart space-right"></i>{{ imageData.size|humansize }}</div>
|
||||
</div>
|
||||
<div class="title">{{ imageData.total }}</div>
|
||||
<div class="comment">Images</div>
|
||||
@@ -104,7 +126,7 @@
|
||||
<i class="fa fa-cubes"></i>
|
||||
</div>
|
||||
<div class="pull-right" ng-if="infoData.Driver">
|
||||
<div><i class="fa fa-hdd-o text-icon"></i>{{ infoData.Driver }} driver</div>
|
||||
<div><i class="fa fa-hdd-o space-right"></i>{{ infoData.Driver }} driver</div>
|
||||
</div>
|
||||
<div class="title">{{ volumeData.total }}</div>
|
||||
<div class="comment">Volumes</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('dashboard', [])
|
||||
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info',
|
||||
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info) {
|
||||
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Messages',
|
||||
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info, Messages) {
|
||||
|
||||
$scope.containerData = {
|
||||
total: 0
|
||||
@@ -80,11 +80,13 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
||||
prepareNetworkData(d[3]);
|
||||
prepareInfoData(d[4]);
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function(e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to load dashboard data");
|
||||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
fetchDashboardData(c.hiddenLabels);
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -3,139 +3,113 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Docker</rd-header-content>
|
||||
</rd-header>
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon pull-left">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
<div class="title">{{ docker.Version }}</div>
|
||||
<div class="comment">Docker version</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon pull-left">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
<div class="title">{{ docker.ApiVersion }}</div>
|
||||
<div class="comment">API version</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon pull-left">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
<div class="title">{{ docker.GoVersion }}</div>
|
||||
<div class="comment">Go version</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="row" ng-if="state.loaded">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Engine status"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-code" title="Engine version"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Containers</td>
|
||||
<td>{{ info.Containers }}</td>
|
||||
<td>Version</td>
|
||||
<td>{{ version.Version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Images</td>
|
||||
<td>{{ info.Images }}</td>
|
||||
<td>API version</td>
|
||||
<td>{{ version.ApiVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Debug</td>
|
||||
<td>{{ info.Debug }}</td>
|
||||
<td>Go version</td>
|
||||
<td>{{ version.GoVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPUs</td>
|
||||
<td>{{ info.NCPU }}</td>
|
||||
<td>OS type</td>
|
||||
<td>{{ version.Os }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Memory</td>
|
||||
<td>{{ info.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Operating System</td>
|
||||
<td>OS</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kernel Version</td>
|
||||
<td>{{ info.KernelVersion }}</td>
|
||||
<td>Architecture</td>
|
||||
<td>{{ version.Arch }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{{ info.ID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Labels</td>
|
||||
<td>{{ info.Labels }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File Descriptors</td>
|
||||
<td>{{ info.NFd }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Goroutines</td>
|
||||
<td>{{ info.NGoroutines }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage Driver</td>
|
||||
<td>{{ info.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage Driver Status</td>
|
||||
<td>
|
||||
<p ng-repeat="val in info.DriverStatus">
|
||||
{{ val[0] }}: {{ val[1] }}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Execution Driver</td>
|
||||
<td>{{ info.ExecutionDriver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv4 Forwarding</td>
|
||||
<td>{{ info.IPv4Forwarding }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Index Server Address</td>
|
||||
<td>{{ info.IndexServerAddress }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Init Path</td>
|
||||
<td>{{ info.InitPath }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker Root Directory</td>
|
||||
<td>{{ info.DockerRootDir }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Init SHA1</td>
|
||||
<td>{{ info.InitSha1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory Limit</td>
|
||||
<td>{{ info.MemoryLimit }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swap Limit</td>
|
||||
<td>{{ info.SwapLimit }}</td>
|
||||
<td>Kernel version</td>
|
||||
<td>{{ version.KernelVersion }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="state.loaded">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-th" title="Engine status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total CPU</td>
|
||||
<td>{{ info.NCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td>{{ info.MemTotal|humansize }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker root directory</td>
|
||||
<td>{{ info.DockerRootDir }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Storage driver</td>
|
||||
<td>{{ info.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Logging driver</td>
|
||||
<td>{{ info.LoggingDriver }}</td>
|
||||
</tr>
|
||||
<tr ng-if="info.CgroupDriver">
|
||||
<td>Cgroup driver</td>
|
||||
<td>{{ info.CgroupDriver }}</td>
|
||||
</tr>
|
||||
<tr ng-if="info.ExecutionDriver">
|
||||
<td>Execution driver</td>
|
||||
<td>{{ info.ExecutionDriver }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="state.loaded && info.Plugins">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plug" title="Engine plugins"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr ng-if="info.Plugins.Volume">
|
||||
<td>Volume</td>
|
||||
<td>{{ info.Plugins.Volume|arraytostr: ', '}}</td>
|
||||
</tr>
|
||||
<tr ng-if="info.Plugins.Network">
|
||||
<td>Network</td>
|
||||
<td>{{ info.Plugins.Network|arraytostr: ', '}}</td>
|
||||
</tr>
|
||||
<tr ng-if="info.Plugins.Authorization">
|
||||
<td>Authorization</td>
|
||||
<td>{{ info.Plugins.Authorization|arraytostr: ', '}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
angular.module('docker', [])
|
||||
.controller('DockerController', ['$scope', 'Info', 'Version', 'Settings',
|
||||
function ($scope, Info, Version, Settings) {
|
||||
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
.controller('DockerController', ['$scope', 'Info', 'Version', 'Messages',
|
||||
function ($scope, Info, Version, Messages) {
|
||||
$scope.state = {
|
||||
loaded: false
|
||||
};
|
||||
$scope.info = {};
|
||||
$scope.version = {};
|
||||
|
||||
Version.get({}, function (d) {
|
||||
$scope.docker = d;
|
||||
});
|
||||
Info.get({}, function (d) {
|
||||
$scope.info = d;
|
||||
Info.get({}, function (infoData) {
|
||||
$scope.info = infoData;
|
||||
Version.get({}, function (versionData) {
|
||||
$scope.version = versionData;
|
||||
$scope.state.loaded = true;
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to retrieve engine details');
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to retrieve engine information');
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}]);
|
||||
|
||||
99
app/components/endpoint/endpoint.html
Normal file
99
app/components/endpoint/endpoint.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Endpoint details">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="container_name" ng-model="endpoint.Name" placeholder="e.g. docker-prod01">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- endpoint-url-input -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_url" class="col-sm-2 control-label text-left">Endpoint URL</label>
|
||||
<div class="col-sm-10">
|
||||
<input ng-disabled="endpointType === 'local'" type="text" class="form-control" id="endpoint_url" ng-model="endpoint.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-url-input -->
|
||||
<!-- tls-checkbox -->
|
||||
<div class="form-group" ng-if="endpointType === 'remote'">
|
||||
<label for="tls" class="col-sm-2 control-label text-left">TLS</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="checkbox" name="tls" ng-model="endpoint.TLS">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-certs -->
|
||||
<div ng-if="endpoint.TLS">
|
||||
<!-- ca-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSCACert !== endpoint.TLSCACert">{{ formValues.TLSCACert.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSCACert && formValues.TLSCACert === endpoint.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ca-input -->
|
||||
<!-- cert-input -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSCert !== endpoint.TLSCert">{{ formValues.TLSCert.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSCert && formValues.TLSCert === endpoint.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cert-input -->
|
||||
<!-- key-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSKey !== endpoint.TLSKey">{{ formValues.TLSKey.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSKey && formValues.TLSKey === endpoint.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !key-input -->
|
||||
</div>
|
||||
<!-- !tls-certs -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="updateEndpoint()">Update endpoint</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="endpoints">Cancel</a>
|
||||
<i id="updateEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
56
app/components/endpoint/endpointController.js
Normal file
56
app/components/endpoint/endpointController.js
Normal file
@@ -0,0 +1,56 @@
|
||||
angular.module('endpoint', [])
|
||||
.controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Messages',
|
||||
function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false
|
||||
};
|
||||
$scope.formValues = {
|
||||
TLSCACert: null,
|
||||
TLSCert: null,
|
||||
TLSKey: null
|
||||
};
|
||||
|
||||
$scope.updateEndpoint = function() {
|
||||
var ID = $scope.endpoint.Id;
|
||||
var name = $scope.endpoint.Name;
|
||||
var URL = $scope.endpoint.URL;
|
||||
var TLS = $scope.endpoint.TLS;
|
||||
var TLSCACert = $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null;
|
||||
var TLSCert = $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null;
|
||||
var TLSKey = $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null;
|
||||
var type = $scope.endpointType;
|
||||
EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey, type).then(function success(data) {
|
||||
Messages.send("Endpoint updated", $scope.endpoint.Name);
|
||||
$state.go('endpoints');
|
||||
}, function error(err) {
|
||||
$scope.state.error = err.msg;
|
||||
}, function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function getEndpoint(endpointID) {
|
||||
$('#loadingViewSpinner').show();
|
||||
EndpointService.endpoint($stateParams.id).then(function success(data) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
$scope.endpoint = data;
|
||||
if (data.URL.indexOf("unix://") === 0) {
|
||||
$scope.endpointType = 'local';
|
||||
} else {
|
||||
$scope.endpointType = 'remote';
|
||||
}
|
||||
$scope.endpoint.URL = $filter('stripprotocol')(data.URL);
|
||||
$scope.formValues.TLSCACert = data.TLSCACert;
|
||||
$scope.formValues.TLSCert = data.TLSCert;
|
||||
$scope.formValues.TLSKey = data.TLSKey;
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", err, "Unable to retrieve endpoint details");
|
||||
});
|
||||
}
|
||||
|
||||
getEndpoint($stateParams.id);
|
||||
}]);
|
||||
145
app/components/endpointInit/endpointInit.html
Normal file
145
app/components/endpointInit/endpointInit.html
Normal file
@@ -0,0 +1,145 @@
|
||||
<div class="page-wrapper">
|
||||
<!-- simple box -->
|
||||
<div class="container simple-box">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-8 col-sm-offset-2">
|
||||
<!-- simple box logo -->
|
||||
<div class="row">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||
</div>
|
||||
<!-- !simple box logo -->
|
||||
<!-- init-endpoint panel -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- init-endpoint form -->
|
||||
<form class="form-horizontal" style="margin: 20px;" enctype="multipart/form-data" method="POST">
|
||||
<!-- comment -->
|
||||
<div class="form-group">
|
||||
<p>Connect Portainer to a Docker engine or Swarm cluster endpoint.</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- endpoin-type radio -->
|
||||
<div class="form-group">
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType" ng-click="cleanError()">Manage the Docker instance where Portainer is running</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType" ng-click="cleanError()">Manage a remote Docker instance</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- endpoint-type radio -->
|
||||
<!-- local-endpoint -->
|
||||
<div ng-if="formValues.endpointType === 'local'" style="margin-top: 25px;">
|
||||
<div class="form-group">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 5px;"></i>
|
||||
<span class="small text-primary">This feature is not available with Docker <b>on</b> Windows yet.</span>
|
||||
<div class="small text-primary">On Linux / Mac, ensure that you have started Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code></div>
|
||||
</div>
|
||||
<!-- connect button -->
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</p>
|
||||
<span class="pull-right">
|
||||
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
|
||||
<button type="submit" class="btn btn-primary" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !connect button -->
|
||||
</div>
|
||||
<!-- !local-endpoint -->
|
||||
<!-- remote-endpoint -->
|
||||
<div ng-if="formValues.endpointType === 'remote'" style="margin-top: 25px;">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-3 control-label text-left">Name</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- endpoint-url-input -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_url" class="col-sm-3 control-label text-left">Endpoint URL</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-url-input -->
|
||||
<!-- tls-checkbox -->
|
||||
<div class="form-group">
|
||||
<label for="tls" class="col-sm-3 control-label text-left">TLS</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="checkbox" name="tls" ng-model="formValues.TLS">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-certs -->
|
||||
<div ng-if="formValues.TLS">
|
||||
<!-- ca-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-9">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCACert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ca-input -->
|
||||
<!-- cert-input -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-3 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-9">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cert-input -->
|
||||
<!-- key-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-9">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSKey.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !key-input -->
|
||||
</div>
|
||||
<!-- !tls-certs -->
|
||||
<!-- connect button -->
|
||||
<div class="form-group" style="margin-top: 10px;">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</p>
|
||||
<span class="pull-right">
|
||||
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !connect button -->
|
||||
</div>
|
||||
<!-- !remote-endpoint -->
|
||||
</form>
|
||||
<!-- !init-endpoint form -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !init-endpoint panel -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !simple box -->
|
||||
</div>
|
||||
80
app/components/endpointInit/endpointInitController.js
Normal file
80
app/components/endpointInit/endpointInitController.js
Normal file
@@ -0,0 +1,80 @@
|
||||
angular.module('endpointInit', [])
|
||||
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Messages',
|
||||
function ($scope, $state, EndpointService, StateManager, Messages) {
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false
|
||||
};
|
||||
$scope.formValues = {
|
||||
endpointType: "remote",
|
||||
Name: '',
|
||||
URL: '',
|
||||
TLS: false,
|
||||
TLSCACert: null,
|
||||
TLSCert: null,
|
||||
TLSKey: null
|
||||
};
|
||||
|
||||
if (!_.isEmpty($scope.applicationState.endpoint)) {
|
||||
$state.go('dashboard');
|
||||
}
|
||||
|
||||
$scope.cleanError = function() {
|
||||
$scope.state.error = '';
|
||||
};
|
||||
|
||||
$scope.createLocalEndpoint = function() {
|
||||
$('#initEndpointSpinner').show();
|
||||
$scope.state.error = '';
|
||||
var name = "local";
|
||||
var URL = "unix:///var/run/docker.sock";
|
||||
var TLS = false;
|
||||
EndpointService.createLocalEndpoint(name, URL, TLS, true).then(function success(data) {
|
||||
StateManager.updateEndpointState(false)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
EndpointService.deleteEndpoint(0)
|
||||
.then(function success() {
|
||||
$('#initEndpointSpinner').hide();
|
||||
$scope.state.error = 'Unable to connect to the Docker endpoint';
|
||||
});
|
||||
});
|
||||
}, function error(err) {
|
||||
$('#initEndpointSpinner').hide();
|
||||
$scope.state.error = 'Unable to create endpoint';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createRemoteEndpoint = function() {
|
||||
$('#initEndpointSpinner').show();
|
||||
$scope.state.error = '';
|
||||
var name = $scope.formValues.Name;
|
||||
var URL = $scope.formValues.URL;
|
||||
var TLS = $scope.formValues.TLS;
|
||||
var TLSCAFile = $scope.formValues.TLSCACert;
|
||||
var TLSCertFile = $scope.formValues.TLSCert;
|
||||
var TLSKeyFile = $scope.formValues.TLSKey;
|
||||
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true)
|
||||
.then(function success(data) {
|
||||
StateManager.updateEndpointState(false)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
EndpointService.deleteEndpoint(0)
|
||||
.then(function success() {
|
||||
$('#initEndpointSpinner').hide();
|
||||
$scope.state.error = 'Unable to connect to the Docker endpoint';
|
||||
});
|
||||
});
|
||||
}, function error(err) {
|
||||
$('#initEndpointSpinner').hide();
|
||||
$scope.state.uploadInProgress = false;
|
||||
$scope.state.error = err.msg;
|
||||
}, function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
||||
183
app/components/endpoints/endpoints.html
Normal file
183
app/components/endpoints/endpoints.html
Normal file
@@ -0,0 +1,183 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Endpoints">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="endpoints" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadEndpointsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Endpoint management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title="Add a new endpoint">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- endpoint-url-input -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_url" class="col-sm-2 control-label text-left">Endpoint URL</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-url-input -->
|
||||
<!-- tls-checkbox -->
|
||||
<div class="form-group">
|
||||
<label for="tls" class="col-sm-2 control-label text-left">TLS</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="checkbox" name="tls" ng-model="formValues.TLS">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-certs -->
|
||||
<div ng-if="formValues.TLS">
|
||||
<!-- ca-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCACert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ca-input -->
|
||||
<!-- cert-input -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cert-input -->
|
||||
<!-- key-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSKey.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !key-input -->
|
||||
</div>
|
||||
<!-- !tls-certs -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="addEndpoint()">Add endpoint</button>
|
||||
<i id="createEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plug" title="Endpoints">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="endpoints" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="endpoints" ng-click="order('URL')">
|
||||
URL
|
||||
<span ng-show="sortType == 'URL' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'URL' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="endpoints" ng-click="order('TLS')">
|
||||
TLS
|
||||
<span ng-show="sortType == 'TLS' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'TLS' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
|
||||
<td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td>
|
||||
<td>{{ endpoint.URL | stripprotocol }}</td>
|
||||
<td><i class="fa fa-shield" aria-hidden="true" ng-if="endpoint.TLS"></i></td>
|
||||
<td>
|
||||
<span ng-if="endpoint.Id !== activeEndpoint.Id">
|
||||
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
|
||||
</span>
|
||||
<span class="small text-muted" ng-if="endpoint.Id === activeEndpoint.Id">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i> You cannot edit the active endpoint
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!endpoints">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="endpoints.length == 0">
|
||||
<td colspan="5" class="text-center text-muted">No endpoints available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="endpoints" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
104
app/components/endpoints/endpointsController.js
Normal file
104
app/components/endpoints/endpointsController.js
Normal file
@@ -0,0 +1,104 @@
|
||||
angular.module('endpoints', [])
|
||||
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Messages', 'Pagination',
|
||||
function ($scope, $state, EndpointService, Messages, Pagination) {
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false,
|
||||
selectedItemCount: 0,
|
||||
pagination_count: Pagination.getPaginationCount('endpoints')
|
||||
};
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
URL: '',
|
||||
TLS: false,
|
||||
TLSCACert: null,
|
||||
TLSCert: null,
|
||||
TLSKey: null
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('endpoints', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addEndpoint = function() {
|
||||
$scope.state.error = '';
|
||||
var name = $scope.formValues.Name;
|
||||
var URL = $scope.formValues.URL;
|
||||
var TLS = $scope.formValues.TLS;
|
||||
var TLSCAFile = $scope.formValues.TLSCACert;
|
||||
var TLSCertFile = $scope.formValues.TLSCert;
|
||||
var TLSKeyFile = $scope.formValues.TLSKey;
|
||||
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, false).then(function success(data) {
|
||||
Messages.send("Endpoint created", name);
|
||||
$state.reload();
|
||||
}, function error(err) {
|
||||
$scope.state.uploadInProgress = false;
|
||||
$scope.state.error = err.msg;
|
||||
}, function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
$('#loadEndpointsSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadEndpointsSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.endpoints, function (endpoint) {
|
||||
if (endpoint.Checked) {
|
||||
counter = counter + 1;
|
||||
EndpointService.deleteEndpoint(endpoint.Id).then(function success(data) {
|
||||
Messages.send("Endpoint deleted", endpoint.Name);
|
||||
var index = $scope.endpoints.indexOf(endpoint);
|
||||
$scope.endpoints.splice(index, 1);
|
||||
complete();
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, 'Unable to remove endpoint');
|
||||
complete();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function fetchEndpoints() {
|
||||
$('#loadEndpointsSpinner').show();
|
||||
EndpointService.endpoints().then(function success(data) {
|
||||
$scope.endpoints = data;
|
||||
EndpointService.getActive().then(function success(data) {
|
||||
$scope.activeEndpoint = data;
|
||||
$('#loadEndpointsSpinner').hide();
|
||||
}, function error(err) {
|
||||
$('#loadEndpointsSpinner').hide();
|
||||
Messages.error("Failure", err, "Unable to retrieve active endpoint");
|
||||
});
|
||||
}, function error(err) {
|
||||
$('#loadEndpointsSpinner').hide();
|
||||
Messages.error("Failure", err, "Unable to retrieve endpoints");
|
||||
$scope.endpoints = [];
|
||||
});
|
||||
}
|
||||
|
||||
fetchEndpoints();
|
||||
}]);
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="events" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadEventsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Events</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -12,7 +13,14 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-history" title="Events">
|
||||
<div class="pull-right">
|
||||
<i id="loadEventsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
@@ -49,13 +57,16 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="event in (events | filter:state.filter | orderBy:sortType:sortReverse)">
|
||||
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
||||
<td>{{ event.Time|getisodatefromtimestamp }}</td>
|
||||
<td>{{ event.Type }}</td>
|
||||
<td>{{ event.Details }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="events" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
angular.module('events', [])
|
||||
.controller('EventsController', ['$scope', 'Settings', 'Messages', 'Events',
|
||||
function ($scope, Settings, Messages, Events) {
|
||||
.controller('EventsController', ['$scope', 'Messages', 'Events', 'Pagination',
|
||||
function ($scope, Messages, Events, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('events');
|
||||
$scope.sortType = 'Time';
|
||||
$scope.sortReverse = true;
|
||||
|
||||
@@ -10,6 +11,10 @@ function ($scope, Settings, Messages, Events) {
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('events', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
var from = moment().subtract(24, 'hour').unix();
|
||||
var to = moment().unix();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Images > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
|
||||
<a ui-sref="images">Images</a> > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<rd-widget-header icon="fa fa-tags" title="Image tags"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div style="margin: 5px 10px;">
|
||||
<span ng-repeat="tag in RepoTags" class="label label-primary image-tag">
|
||||
<span ng-repeat="tag in RepoTags" class="label label-primary image-tag space-right">
|
||||
<a data-toggle="tooltip" class="interactive" title="Push to registry" ng-click="pushImage(tag)">
|
||||
<i class="fa fa-upload white-icon" aria-hidden="true"></i>
|
||||
</a>
|
||||
@@ -82,7 +82,7 @@
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ image.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)">Delete this image</button>
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this image</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="image.Parent">
|
||||
@@ -130,7 +130,7 @@
|
||||
<tr ng-if="image.ContainerConfig.ExposedPorts">
|
||||
<td>EXPOSE</td>
|
||||
<td>
|
||||
<span class="label label-default tag" ng-repeat="port in exposedPorts">
|
||||
<span class="label label-default space-right" ng-repeat="port in exposedPorts">
|
||||
{{ port }}
|
||||
</span>
|
||||
</td>
|
||||
@@ -138,7 +138,7 @@
|
||||
<tr ng-if="image.ContainerConfig.Volumes">
|
||||
<td>VOLUME</td>
|
||||
<td>
|
||||
<span class="label label-default tag" ng-repeat="volume in volumes">
|
||||
<span class="label label-default space-right" ng-repeat="volume in volumes">
|
||||
{{ volume }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -21,9 +21,9 @@ function ($scope, $stateParams, $state, Image, ImageHelper, Messages) {
|
||||
|
||||
$scope.tagImage = function() {
|
||||
$('#loadingViewSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var imageConfig = ImageHelper.createImageConfig(image, registry);
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
|
||||
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
Messages.send('Image successfully tagged');
|
||||
$('#loadingViewSpinner').hide();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="images" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadImagesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Images</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -50,12 +51,19 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-clone" title="Images">
|
||||
<div class="pull-right">
|
||||
<i id="loadImagesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
@@ -66,7 +74,9 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="images" ng-click="order('Id')">
|
||||
Id
|
||||
@@ -98,7 +108,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
|
||||
<td><a ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a></td>
|
||||
<td>
|
||||
@@ -107,8 +117,17 @@
|
||||
<td>{{ image.VirtualSize|humansize }}</td>
|
||||
<td>{{ image.Created|getisodatefromtimestamp }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!images">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="images.length == 0">
|
||||
<td colspan="5" class="text-center text-muted">No images available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="images" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
angular.module('images', [])
|
||||
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages',
|
||||
function ($scope, $state, Config, Image, Messages) {
|
||||
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'ImageHelper', 'Messages', 'Pagination',
|
||||
function ($scope, $state, Config, Image, ImageHelper, Messages, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('images');
|
||||
$scope.sortType = 'RepoTags';
|
||||
$scope.sortReverse = true;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
@@ -16,6 +17,19 @@ function ($scope, $state, Config, Image, Messages) {
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('images', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.selectItems = function (allSelected) {
|
||||
angular.forEach($scope.state.filteredImages, function (image) {
|
||||
if (image.Checked !== allSelected) {
|
||||
image.Checked = allSelected;
|
||||
$scope.selectItem(image);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
@@ -24,24 +38,11 @@ function ($scope, $state, Config, Image, Messages) {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$scope.pullImage = function() {
|
||||
$('#pullImageSpinner').show();
|
||||
var image = _.toLower($scope.config.Image);
|
||||
var registry = _.toLower($scope.config.Registry);
|
||||
var imageConfig = createImageConfig(image, registry);
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
|
||||
Image.create(imageConfig, function (data) {
|
||||
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
|
||||
if (err) {
|
||||
@@ -50,7 +51,7 @@ function ($scope, $state, Config, Image, Messages) {
|
||||
Messages.error('Error', {}, detail.error);
|
||||
} else {
|
||||
$('#pullImageSpinner').hide();
|
||||
$state.go('images', {}, {reload: true});
|
||||
$state.reload();
|
||||
}
|
||||
}, function (e) {
|
||||
$('#pullImageSpinner').hide();
|
||||
@@ -72,7 +73,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);
|
||||
@@ -97,11 +98,11 @@ function ($scope, $state, Config, Image, Messages) {
|
||||
}, function (e) {
|
||||
$('#loadImagesSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve images");
|
||||
$scope.images = [];
|
||||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.availableRegistries = c.registries;
|
||||
fetchImages();
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
angular.module('dashboard')
|
||||
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', function ($scope, $cookieStore, Settings, Config) {
|
||||
angular.module('main', [])
|
||||
.controller('MainController', ['$scope', '$cookieStore', 'StateManager',
|
||||
function ($scope, $cookieStore, StateManager) {
|
||||
|
||||
/**
|
||||
* Sidebar Toggle & Cookie Control
|
||||
*/
|
||||
var mobileView = 992;
|
||||
|
||||
$scope.getWidth = function() {
|
||||
return window.innerWidth;
|
||||
};
|
||||
|
||||
$scope.config = Config;
|
||||
$scope.applicationState = StateManager.getState();
|
||||
|
||||
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
|
||||
if (newValue >= mobileView) {
|
||||
@@ -32,6 +33,4 @@ angular.module('dashboard')
|
||||
window.onresize = function() {
|
||||
$scope.$apply();
|
||||
};
|
||||
|
||||
$scope.uiVersion = Settings.uiVersion;
|
||||
}]);
|
||||
@@ -3,122 +3,91 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
|
||||
<a ui-sref="networks">Networks</a> > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-9 col-md-9 col-xs-9">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-sitemap"></i>
|
||||
</div>
|
||||
<div class="title">{{ network.Name }}</div>
|
||||
<div class="comment">Name</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-3 col-xs-3">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-cogs"></i>
|
||||
</div>
|
||||
<div class="title">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button class="btn btn-default" disabled>Connect container...</button>
|
||||
<button class="btn btn-danger" ng-click="remove(id)">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="comment">
|
||||
Actions
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Network details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Id</td>
|
||||
<td>{{ network.Id }}</td>
|
||||
<td>Name</td>
|
||||
<td>{{ network.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scope</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ network.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this network</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Driver</td>
|
||||
<td>{{ network.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPAM</td>
|
||||
<td>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<td>Driver</td>
|
||||
<td>{{ network.IPAM.Driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subnet</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gateway</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td>Scope</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Containers</td>
|
||||
<td>
|
||||
<table class="table table-striped" ng-repeat="(Id, container) in network.Containers">
|
||||
<tr>
|
||||
<td>Id</td>
|
||||
<td><a ui-sref="container({id: Id})">{{ Id }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EndpointID</td>
|
||||
<td>{{ container.EndpointID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MacAddress</td>
|
||||
<td>{{ container.MacAddress}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv4Address</td>
|
||||
<td>{{ container.IPv4Address}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IPv6Address</td>
|
||||
<td>{{ container.IPv6Address}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button ng-click="disconnect(network.Id, Id)" class="btn btn-danger">
|
||||
Disconnect from network
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<tr ng-if="network.IPAM.Config[0].Subnet">
|
||||
<td>Subnet</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Options</td>
|
||||
<tr ng-if="network.IPAM.Config[0].Gateway">
|
||||
<td>Gateway</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="!(network.Options | emptyobject)">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cogs" title="Network options"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr ng-repeat="(key, value) in network.Options">
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row" ng-if="!(network.Containers | emptyobject)">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-server" title="Containers in network"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Container Name</th>
|
||||
<th>IPv4 Address</th>
|
||||
<th>IPv6 Address</th>
|
||||
<th>MacAddress</th>
|
||||
<th>Actions</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="container in containersInNetwork">
|
||||
<td><a ui-sref="container({id: container.Id})">{{ container.Name }}</a></td>
|
||||
<td>{{ container.IPv4Address || '-' }}</td>
|
||||
<td>{{ container.IPv6Address || '-' }}</td>
|
||||
<td>{{ container.MacAddress || '-' }}</td>
|
||||
<td>
|
||||
<table role="table" class="table table-striped">
|
||||
<tr ng-repeat="(k, v) in network.Options">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(network, container.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Leave Network</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -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', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Messages',
|
||||
function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, 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) {
|
||||
@@ -31,12 +19,82 @@ function ($scope, Network, Messages, $state, $stateParams) {
|
||||
});
|
||||
};
|
||||
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.get({id: $stateParams.id}, function (d) {
|
||||
$scope.network = d;
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve network info");
|
||||
$scope.containerLeaveNetwork = function containerLeaveNetwork(network, containerId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.disconnect({id: $stateParams.id}, { Container: containerId, Force: false }, function (d) {
|
||||
if (d.message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Error", {}, d.message);
|
||||
} else {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Container left network", $stateParams.id);
|
||||
$state.go('network', {id: network.Id}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to disconnect container from network");
|
||||
});
|
||||
};
|
||||
|
||||
function getNetwork() {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.get({id: $stateParams.id}, function success(data) {
|
||||
$scope.network = data;
|
||||
getContainersInNetwork(data);
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", err, "Unable to retrieve network info");
|
||||
});
|
||||
}
|
||||
|
||||
function filterContainersInNetwork(network, containers) {
|
||||
if ($scope.containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(containers, $scope.containersToHideLabels);
|
||||
}
|
||||
var containersInNetwork = [];
|
||||
containers.forEach(function(container) {
|
||||
var containerInNetwork = network.Containers[container.Id];
|
||||
containerInNetwork.Id = container.Id;
|
||||
// Name is not available in Docker 1.9
|
||||
if (!containerInNetwork.Name) {
|
||||
containerInNetwork.Name = $filter('trimcontainername')(container.Names[0]);
|
||||
}
|
||||
containersInNetwork.push(containerInNetwork);
|
||||
});
|
||||
$scope.containersInNetwork = containersInNetwork;
|
||||
}
|
||||
|
||||
function getContainersInNetwork(network) {
|
||||
if (network.Containers) {
|
||||
if ($scope.applicationState.endpoint.apiVersion < 1.24) {
|
||||
Container.query({}, function success(data) {
|
||||
var containersInNetwork = data.filter(function filter(container) {
|
||||
if (container.HostConfig.NetworkMode === network.Name) {
|
||||
return container;
|
||||
}
|
||||
});
|
||||
filterContainersInNetwork(network, containersInNetwork);
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", err, "Unable to retrieve containers in network");
|
||||
});
|
||||
} else {
|
||||
Container.query({
|
||||
filters: {network: [$stateParams.id]}
|
||||
}, function success(data) {
|
||||
filterContainersInNetwork(network, data);
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", err, "Unable to retrieve containers in network");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
getNetwork();
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -3,97 +3,157 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="networks" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadNetworksSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Networks</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Networks">
|
||||
<div class="pull-right">
|
||||
<i id="loadNetworksSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
<a class="btn btn-default" type="button" ui-sref="actions.create.network">Add network</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Id')">
|
||||
Id
|
||||
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Scope')">
|
||||
Scope
|
||||
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Driver')">
|
||||
Driver
|
||||
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Driver')">
|
||||
IPAM Driver
|
||||
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')">
|
||||
IPAM Subnet
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
|
||||
IPAM Gateway
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
|
||||
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
|
||||
<td>{{ network.Id }}</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
<td>{{ network.Driver }}</td>
|
||||
<td>{{ network.IPAM.Driver }}</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title="Add a network">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="network_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
|
||||
<button type="button" class="btn btn-default btn-sm" ui-sref="actions.create.network">Advanced settings...</button>
|
||||
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Networks">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Id')">
|
||||
Id
|
||||
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Scope')">
|
||||
Scope
|
||||
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Driver')">
|
||||
Driver
|
||||
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Driver')">
|
||||
IPAM Driver
|
||||
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')">
|
||||
IPAM Subnet
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
|
||||
IPAM Gateway
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="network in ( state.filteredNetworks = (networks | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="network.Checked" ng-change="selectItem(network)"/></td>
|
||||
<td><a ui-sref="network({id: network.Id})">{{ network.Name|truncate:40}}</a></td>
|
||||
<td>{{ network.Id }}</td>
|
||||
<td>{{ network.Scope }}</td>
|
||||
<td>{{ network.Driver }}</td>
|
||||
<td>{{ network.IPAM.Driver }}</td>
|
||||
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
|
||||
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!networks">
|
||||
<td colspan="8" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="networks.length == 0">
|
||||
<td colspan="8" class="text-center text-muted">No networks available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="networks" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
angular.module('networks', [])
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages',
|
||||
function ($scope, $state, Network, Config, Messages) {
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Pagination',
|
||||
function ($scope, $state, Network, Config, Messages, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.state.advancedSettings = false;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
$scope.formValues = {
|
||||
Subnet: '',
|
||||
Gateway: ''
|
||||
$scope.config = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
$scope.config = {
|
||||
Name: '',
|
||||
IPAM: {
|
||||
Config: []
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('networks', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
function prepareNetworkConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
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.reload();
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createNetworkSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to create network');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
@@ -24,6 +51,15 @@ function ($scope, $state, Network, Config, Messages) {
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.selectItems = function(allSelected) {
|
||||
angular.forEach($scope.state.filteredNetworks, function (network) {
|
||||
if (network.Checked !== allSelected) {
|
||||
network.Checked = allSelected;
|
||||
$scope.selectItem(network);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
@@ -69,8 +105,11 @@ function ($scope, $state, Network, Config, Messages) {
|
||||
}, function (e) {
|
||||
$('#loadNetworksSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve networks");
|
||||
$scope.networks = [];
|
||||
});
|
||||
}
|
||||
|
||||
fetchNetworks();
|
||||
Config.$promise.then(function (c) {
|
||||
fetchNetworks();
|
||||
});
|
||||
}]);
|
||||
|
||||
273
app/components/node/node.html
Normal file
273
app/components/node/node.html
Normal file
@@ -0,0 +1,273 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Node details">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="node({id: node.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="swarm">Swarm nodes</a> > <a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="!node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div ng-if="loading">
|
||||
<i class="fa fa-cog fa-spin"></i> Loading...
|
||||
</div>
|
||||
|
||||
<rd-widget ng-if="!loading">
|
||||
<rd-widget-header icon="fa-object-group" title="Node does not exist"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<p>It looks like the node you wish to inspect does not exist.</p>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Node specification"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<input type="text" class="input-sm" ng-model="node.Name" placeholder="e.g. my-manager" ng-change="updateNodeAttribute(node, 'Name')">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Host name</td>
|
||||
<td>{{ node.Hostname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role</td>
|
||||
<td>{{ node.Role }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Availability</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<select name="nodeAvailability" class="selectpicker form-control" ng-model="node.Availability" ng-change="updateNodeAttribute(node, 'Availability')">
|
||||
<option value="active">Active</option>
|
||||
<option value="pause">Pause</option>
|
||||
<option value="drain">Drain</option>
|
||||
</select>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td><span class="label label-{{ node.Status|nodestatusbadge }}">{{ node.Status }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<p class="small text-muted">
|
||||
View the Docker Swarm mode Node documentation <a href="https://docs.docker.com/engine/swarm/manage-nodes/" target="self">here</a>.
|
||||
</p>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="!hasChanges(node, ['Name', 'Availability'])" ng-click="updateNode(node)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node && node.Role === 'manager'">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Manager status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Leader</td>
|
||||
<td>
|
||||
<span ng-if="node.Leader"><i class="fa fa-check green-icon" aria-hidden="true"></i> Yes</span>
|
||||
<span ng-if="!node.Leader"><i class="fa fa-times red-icon" aria-hidden="true"></i> No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reachability</td>
|
||||
<td>{{ node.Reachability }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Manager address</td>
|
||||
<td>{{ node.ManagerAddr }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Node description"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CPU</td>
|
||||
<td>{{ node.CPUs / 1000000000 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory</td>
|
||||
<td>{{ node.Memory|humansize: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Platform</td>
|
||||
<td>{{ node.PlatformOS }} {{ node.PlatformArchitecture }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Docker Engine version</td>
|
||||
<td>{{ node.EngineVersion }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Node labels">
|
||||
<div class="nopadding">
|
||||
<a class="btn btn-default btn-sm pull-right" ng-click="addLabel(node)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</a>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body ng-if="!node.Labels || node.Labels.length === 0">
|
||||
<p>There are no labels for this node.</p>
|
||||
</rd-widget-body>
|
||||
<rd-widget-body classes="no-padding" ng-if="node.Labels && node.Labels.length > 0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="label in node.Labels">
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(node, label)">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(node, label)">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeLabel(node, $index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(node, ['Labels'])" ng-click="updateNode(node)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(node)">Reset changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="node && tasks.length > 0">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Associated tasks">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>
|
||||
<a ui-sref="node" ng-click="order('Status')">
|
||||
Status
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="node" ng-click="order('Slot')">
|
||||
Slot
|
||||
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="node" ng-click="order('Image')">
|
||||
Image
|
||||
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="node" ng-click="order('Updated')">
|
||||
Last update
|
||||
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
|
||||
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
|
||||
<td>{{ task.Slot }}</td>
|
||||
<td>{{ task.Image }}</td>
|
||||
<td>{{ task.Updated|getisodate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="tasks" class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
112
app/components/node/nodeController.js
Normal file
112
app/components/node/nodeController.js
Normal file
@@ -0,0 +1,112 @@
|
||||
angular.module('node', [])
|
||||
.controller('NodeController', ['$scope', '$state', '$stateParams', 'LabelHelper', 'Node', 'NodeHelper', 'Task', 'Pagination', 'Messages',
|
||||
function ($scope, $state, $stateParams, LabelHelper, Node, NodeHelper, Task, Pagination, Messages) {
|
||||
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('node_tasks');
|
||||
$scope.loading = true;
|
||||
$scope.tasks = [];
|
||||
$scope.displayNode = false;
|
||||
$scope.sortType = 'Status';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
var originalNode = {};
|
||||
var editedKeys = [];
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('node_tasks', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.updateNodeAttribute = function updateNodeAttribute(node, key) {
|
||||
editedKeys.push(key);
|
||||
};
|
||||
$scope.addLabel = function addLabel(node) {
|
||||
node.Labels.push({ key: '', value: '', originalValue: '', originalKey: '' });
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
};
|
||||
$scope.removeLabel = function removeLabel(node, index) {
|
||||
var removedElement = node.Labels.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
}
|
||||
};
|
||||
$scope.updateLabel = function updateLabel(node, label) {
|
||||
if (label.value !== label.originalValue || label.key !== label.originalKey) {
|
||||
$scope.updateNodeAttribute(node, 'Labels');
|
||||
}
|
||||
};
|
||||
|
||||
$scope.hasChanges = function(node, elements) {
|
||||
if (!elements) {
|
||||
elements = Object.keys(originalNode);
|
||||
}
|
||||
var hasChanges = false;
|
||||
elements.forEach(function(key) {
|
||||
hasChanges = hasChanges || ((editedKeys.indexOf(key) >= 0) && node[key] !== originalNode[key]);
|
||||
});
|
||||
return hasChanges;
|
||||
};
|
||||
|
||||
$scope.cancelChanges = function(node) {
|
||||
editedKeys.forEach(function(key) {
|
||||
node[key] = originalNode[key];
|
||||
});
|
||||
editedKeys = [];
|
||||
};
|
||||
|
||||
$scope.updateNode = function updateNode(node) {
|
||||
var config = NodeHelper.nodeToConfig(node.Model);
|
||||
config.Name = node.Name;
|
||||
config.Availability = node.Availability;
|
||||
config.Role = node.Role;
|
||||
config.Labels = LabelHelper.fromKeyValueToLabelHash(node.Labels);
|
||||
|
||||
Node.update({ id: node.Id, version: node.Version }, config, function (data) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.send("Node successfully updated", "Node updated");
|
||||
$state.go('node', {id: node.Id}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.error("Failure", e, "Failed to update node");
|
||||
});
|
||||
};
|
||||
|
||||
function loadNodeAndTasks() {
|
||||
$scope.loading = true;
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
Node.get({ id: $stateParams.id}, function(d) {
|
||||
if (d.message) {
|
||||
Messages.error("Failure", e, "Unable to inspect the node");
|
||||
} else {
|
||||
var node = new NodeViewModel(d);
|
||||
originalNode = angular.copy(node);
|
||||
$scope.node = node;
|
||||
getTasks(d);
|
||||
}
|
||||
$scope.loading = false;
|
||||
});
|
||||
} else {
|
||||
$scope.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getTasks(node) {
|
||||
if (node) {
|
||||
Task.query({filters: {node: [node.ID]}}, function (tasks) {
|
||||
$scope.tasks = tasks.map(function (task) {
|
||||
return new TaskViewModel(task, [node]);
|
||||
});
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to retrieve tasks associated to the node");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadNodeAndTasks();
|
||||
|
||||
}]);
|
||||
293
app/components/service/service.html
Normal file
293
app/components/service/service.html
Normal file
@@ -0,0 +1,293 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Service details">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="service({id: service.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="services">Services</a> > <a ui-sref="service({id: service.Id})">{{ service.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Service details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td ng-if="!service.EditName">
|
||||
{{ service.Name }}
|
||||
<a href="" data-toggle="tooltip" title="Edit service name" ng-click="service.EditName = true;"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
<td ng-if="service.EditName">
|
||||
<input type="text" class="containerNameInput" ng-model="service.newServiceName">
|
||||
<a class="interactive" ng-click="service.EditName = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="renameService(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ service.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeService()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this service</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scheduling mode</td>
|
||||
<td>{{ service.Mode }}</td>
|
||||
</tr>
|
||||
<tr ng-if="service.Mode === 'replicated'">
|
||||
<td>Replicas</td>
|
||||
<td>
|
||||
<span ng-if="service.Mode === 'replicated' && !service.EditReplicas">
|
||||
{{ service.Replicas }}
|
||||
<a class="interactive" ng-click="service.EditReplicas = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
|
||||
</span>
|
||||
<span ng-if="service.Mode === 'replicated' && service.EditReplicas">
|
||||
<input class="input-sm" type="number" ng-model="service.newServiceReplicas" />
|
||||
<a class="interactive" ng-click="service.EditReplicas = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td ng-if="!service.EditImage">
|
||||
{{ service.Image }}
|
||||
<a href="" data-toggle="tooltip" title="Edit service image" ng-click="service.EditImage = true;"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
|
||||
<td ng-if="service.EditImage">
|
||||
<input type="text" class="containerNameInput" ng-model="service.newServiceImage">
|
||||
<a class="interactive" ng-click="service.EditImage = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="changeServiceImage(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.Ports">
|
||||
<td>Published ports</td>
|
||||
<td>
|
||||
<div ng-repeat="mapping in service.Ports">
|
||||
{{ mapping.TargetPort }} <i class="fa fa-long-arrow-right"></i> {{ mapping.PublishedPort }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.EnvironmentVariables">
|
||||
<td>Environment variables</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-11 nopadding">
|
||||
<span class="label label-default interactive fit-text-size" ng-click="addEnvironmentVariable(service)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
|
||||
</span>
|
||||
</div>
|
||||
<!-- environment-variable-input-list -->
|
||||
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
|
||||
<div ng-repeat="var in service.EnvironmentVariables" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="var.key" ng-disabled="var.added" placeholder="e.g. FOO">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="var.value" ng-change="updateEnvironmentVariable(service, var)" placeholder="e.g. bar">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable(service, $index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !environment-variable-input-list -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Labels</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-11 nopadding">
|
||||
<span class="label label-default interactive fit-text-size" ng-click="addLabel(service)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in service.ServiceLabels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeLabel(service, $index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Container labels</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-11 nopadding">
|
||||
<span class="label label-default interactive fit-text-size" ng-click="addContainerLabel(service)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-11 form-inline nopadding" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in service.ServiceContainerLabels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.key" placeholder="e.g. com.example.foo" ng-change="updateLabel(service, label)">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon fit-text-size">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar" ng-change="updateLabel(service, label)">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removeContainerLabel(service, $index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Update Parallelism</td>
|
||||
<td>
|
||||
<span ng-if="!service.EditParallelism">
|
||||
{{ service.UpdateParallelism }}
|
||||
<a class="interactive" ng-click="service.EditParallelism = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Change</a>
|
||||
</span>
|
||||
<span ng-if="service.EditParallelism">
|
||||
<input class="input-sm" type="number" ng-model="service.newServiceUpdateParallelism" />
|
||||
<a class="interactive" ng-click="service.EditParallelism = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="changeParallelism(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Update Delay</td>
|
||||
<td>
|
||||
<span ng-if="!service.EditDelay">
|
||||
{{ service.UpdateDelay }}
|
||||
<a class="interactive" ng-click="service.EditDelay = true;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Change</a>
|
||||
</span>
|
||||
<span ng-if="service.EditDelay">
|
||||
<input class="input-sm" type="number" ng-model="service.newServiceUpdateDelay" />
|
||||
<a class="interactive" ng-click="service.EditDelay = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="changeUpdateDelay(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Update Failure Action</td>
|
||||
<td>
|
||||
<div class="form-group">
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="continue" ng-change="changeUpdateFailureAction(service)">
|
||||
Continue
|
||||
</label>
|
||||
<label class="radio-inline">
|
||||
<input type="radio" name="failure_action" ng-model="service.newServiceUpdateFailureAction" value="pause" ng-change="changeUpdateFailureAction(service)">
|
||||
Pause
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer ng-if="service.hasChanges">
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" ng-click="updateService(service)">Save changes</button>
|
||||
<button type="button" class="btn btn-default" ng-click="cancelChanges(service)">Reset</button>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="tasks.length > 0">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Associated tasks">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>
|
||||
<a ui-sref="service" ng-click="order('Status')">
|
||||
Status
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="service" ng-click="order('Slot')">
|
||||
Slot
|
||||
<span ng-show="sortType == 'Slot' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Slot' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th ng-if="displayNode">
|
||||
<a ui-sref="service" ng-click="order('Node')">
|
||||
Node
|
||||
<span ng-show="sortType == 'Node' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Node' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="service" ng-click="order('Updated')">
|
||||
Last update
|
||||
<span ng-show="sortType == 'Updated' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Updated' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="task in (filteredTasks = ( tasks | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><a ui-sref="task({ id: task.Id })">{{ task.Id }}</a></td>
|
||||
<td><span class="label label-{{ task.Status|taskstatusbadge }}">{{ task.Status }}</span></td>
|
||||
<td>{{ task.Slot }}</td>
|
||||
<td ng-if="displayNode">{{ task.Node }}</td>
|
||||
<td>{{ task.Updated|getisodate }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="tasks" class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
237
app/components/service/serviceController.js
Normal file
237
app/components/service/serviceController.js
Normal file
@@ -0,0 +1,237 @@
|
||||
angular.module('service', [])
|
||||
.controller('ServiceController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Task', 'Node', 'Messages', 'Pagination',
|
||||
function ($scope, $stateParams, $state, Service, ServiceHelper, Task, Node, Messages, Pagination) {
|
||||
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
|
||||
$scope.service = {};
|
||||
$scope.tasks = [];
|
||||
$scope.displayNode = false;
|
||||
$scope.sortType = 'Status';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
var previousServiceValues = {};
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('service_tasks', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.renameService = function renameService(service) {
|
||||
updateServiceAttribute(service, 'Name', service.newServiceName || service.name);
|
||||
service.EditName = false;
|
||||
};
|
||||
$scope.changeServiceImage = function changeServiceImage(service) {
|
||||
updateServiceAttribute(service, 'Image', service.newServiceImage || service.image);
|
||||
service.EditImage = false;
|
||||
};
|
||||
$scope.scaleService = function scaleService(service) {
|
||||
var replicas = service.newServiceReplicas === null || isNaN(service.newServiceReplicas) ? service.Replicas : service.newServiceReplicas;
|
||||
updateServiceAttribute(service, 'Replicas', replicas);
|
||||
service.EditReplicas = false;
|
||||
};
|
||||
|
||||
$scope.addEnvironmentVariable = function addEnvironmentVariable(service) {
|
||||
service.EnvironmentVariables.push({ key: '', value: '', originalValue: '' });
|
||||
service.hasChanges = true;
|
||||
};
|
||||
$scope.removeEnvironmentVariable = function removeEnvironmentVariable(service, index) {
|
||||
var removedElement = service.EnvironmentVariables.splice(index, 1);
|
||||
service.hasChanges = service.hasChanges || removedElement !== null;
|
||||
};
|
||||
$scope.updateEnvironmentVariable = function updateEnvironmentVariable(service, variable) {
|
||||
service.hasChanges = service.hasChanges || variable.value !== variable.originalValue;
|
||||
};
|
||||
$scope.addLabel = function addLabel(service) {
|
||||
service.hasChanges = true;
|
||||
service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
|
||||
};
|
||||
$scope.removeLabel = function removeLabel(service, index) {
|
||||
var removedElement = service.ServiceLabels.splice(index, 1);
|
||||
service.hasChanges = service.hasChanges || removedElement !== null;
|
||||
};
|
||||
$scope.updateLabel = function updateLabel(service, label) {
|
||||
service.hasChanges = service.hasChanges || label.value !== label.originalValue;
|
||||
};
|
||||
$scope.addContainerLabel = function addContainerLabel(service) {
|
||||
service.hasChanges = true;
|
||||
service.ServiceContainerLabels.push({ key: '', value: '', originalValue: '' });
|
||||
};
|
||||
$scope.removeContainerLabel = function removeContainerLabel(service, index) {
|
||||
var removedElement = service.ServiceContainerLabels.splice(index, 1);
|
||||
service.hasChanges = service.hasChanges || removedElement !== null;
|
||||
};
|
||||
|
||||
$scope.changeParallelism = function changeParallelism(service) {
|
||||
updateServiceAttribute(service, 'UpdateParallelism', service.newServiceUpdateParallelism);
|
||||
service.EditParallelism = false;
|
||||
};
|
||||
$scope.changeUpdateDelay = function changeUpdateDelay(service) {
|
||||
updateServiceAttribute(service, 'UpdateDelay', service.newServiceUpdateDelay);
|
||||
service.EditDelay = false;
|
||||
};
|
||||
$scope.changeUpdateFailureAction = function changeUpdateFailureAction(service) {
|
||||
updateServiceAttribute(service, 'UpdateFailureAction', service.newServiceUpdateFailureAction);
|
||||
};
|
||||
|
||||
$scope.cancelChanges = function changeServiceImage(service) {
|
||||
Object.keys(previousServiceValues).forEach(function(attribute) {
|
||||
service[attribute] = previousServiceValues[attribute]; // reset service values
|
||||
service['newService' + attribute] = previousServiceValues[attribute]; // reset edit fields
|
||||
});
|
||||
previousServiceValues = {}; // clear out all changes
|
||||
// clear out environment variable changes
|
||||
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
|
||||
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
|
||||
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
|
||||
|
||||
service.hasChanges = false;
|
||||
};
|
||||
|
||||
$scope.updateService = function updateService(service) {
|
||||
$('#loadServicesSpinner').show();
|
||||
var config = ServiceHelper.serviceToConfig(service.Model);
|
||||
config.Name = service.newServiceName;
|
||||
config.Labels = translateServiceLabelsToLabels(service.ServiceLabels);
|
||||
config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
|
||||
config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
|
||||
config.TaskTemplate.ContainerSpec.Image = service.newServiceImage;
|
||||
if (service.Mode === 'replicated') {
|
||||
config.Mode.Replicated.Replicas = service.Replicas;
|
||||
}
|
||||
|
||||
config.UpdateConfig = {
|
||||
Parallelism: service.newServiceUpdateParallelism,
|
||||
Delay: service.newServiceUpdateDelay,
|
||||
FailureAction: service.newServiceUpdateFailureAction
|
||||
};
|
||||
|
||||
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.send("Service successfully updated", "Service updated");
|
||||
$state.go('service', {id: service.Id}, {reload: true});
|
||||
}, function (e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to update 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;
|
||||
service.newServiceImage = service.Image;
|
||||
service.newServiceReplicas = service.Replicas;
|
||||
service.newServiceUpdateParallelism = service.UpdateParallelism;
|
||||
service.newServiceUpdateDelay = service.UpdateDelay;
|
||||
service.newServiceUpdateFailureAction = service.UpdateFailureAction;
|
||||
|
||||
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
|
||||
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
|
||||
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
|
||||
|
||||
$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");
|
||||
});
|
||||
}
|
||||
|
||||
function updateServiceAttribute(service, name, newValue) {
|
||||
// ensure we only capture the original previous value, in case we update the attribute multiple times
|
||||
if (!previousServiceValues[name]) {
|
||||
previousServiceValues[name] = service[name];
|
||||
}
|
||||
// update the attribute
|
||||
service[name] = newValue;
|
||||
service.hasChanges = true;
|
||||
}
|
||||
|
||||
function translateEnvironmentVariables(env) {
|
||||
if (env) {
|
||||
var variables = [];
|
||||
env.forEach(function(variable) {
|
||||
var idx = variable.indexOf('=');
|
||||
var keyValue = [variable.slice(0,idx), variable.slice(idx+1)];
|
||||
var originalValue = (keyValue.length > 1) ? keyValue[1] : '';
|
||||
variables.push({ key: keyValue[0], value: originalValue, originalValue: originalValue, added: true});
|
||||
});
|
||||
return variables;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function translateEnvironmentVariablesToEnv(env) {
|
||||
if (env) {
|
||||
var variables = [];
|
||||
env.forEach(function(variable) {
|
||||
if (variable.key && variable.key !== '' && variable.value && variable.value !== '') {
|
||||
variables.push(variable.key + '=' + variable.value);
|
||||
}
|
||||
});
|
||||
return variables;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function translateLabelsToServiceLabels(Labels) {
|
||||
var labels = [];
|
||||
if (Labels) {
|
||||
Object.keys(Labels).forEach(function(key) {
|
||||
labels.push({ key: key, value: Labels[key], originalValue: Labels[key], added: true});
|
||||
});
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
function translateServiceLabelsToLabels(labels) {
|
||||
var Labels = {};
|
||||
if (labels) {
|
||||
labels.forEach(function(label) {
|
||||
Labels[label.key] = label.value;
|
||||
});
|
||||
}
|
||||
return Labels;
|
||||
}
|
||||
|
||||
fetchServiceDetails();
|
||||
}]);
|
||||
95
app/components/services/services.html
Normal file
95
app/components/services/services.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Service list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="services" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadServicesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Services</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-list-alt" title="Services">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
<a class="btn btn-default btn-responsive" type="button" ui-sref="actions.create.service">Add service</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>
|
||||
<a ui-sref="services" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="services" ng-click="order('Image')">
|
||||
Image
|
||||
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="services" ng-click="order('Mode')">
|
||||
Scheduling mode
|
||||
<span ng-show="sortType == 'Mode' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Mode' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="service in (state.filteredServices = ( services | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="service.Checked" ng-change="selectItem(service)"/></td>
|
||||
<td><a ui-sref="service({id: service.Id})">{{ service.Name }}</a></td>
|
||||
<td>{{ service.Image }}</td>
|
||||
<td>
|
||||
{{ service.Mode }}
|
||||
<span ng-if="service.Mode === 'replicated' && !service.Scale">
|
||||
<code data-toggle="tooltip" title="Replicas">{{ service.Replicas }}</code>
|
||||
<a class="interactive" ng-click="service.Scale = true; service.ReplicaCount = service.Replicas;"><i class="fa fa-arrows-v" aria-hidden="true"></i> Scale</a>
|
||||
</span>
|
||||
<span ng-if="service.Mode === 'replicated' && service.Scale">
|
||||
<input class="input-sm" type="number" ng-model="service.Replicas" />
|
||||
<a class="interactive" ng-click="service.Scale = false;"><i class="fa fa-times"></i></a>
|
||||
<a class="interactive" ng-click="scaleService(service)"><i class="fa fa-check-square-o"></i></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!services">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="services.length == 0">
|
||||
<td colspan="4" class="text-center text-muted">No services available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="services" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
88
app/components/services/servicesController.js
Normal file
88
app/components/services/servicesController.js
Normal file
@@ -0,0 +1,88 @@
|
||||
angular.module('services', [])
|
||||
.controller('ServicesController', ['$scope', '$stateParams', '$state', 'Service', 'ServiceHelper', 'Messages', 'Pagination',
|
||||
function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('services');
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('services', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$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.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.reload();
|
||||
}, function (e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
service.Scale = false;
|
||||
service.Replicas = service.ReplicaCount;
|
||||
Messages.error("Failure", e, "Unable to scale service");
|
||||
});
|
||||
};
|
||||
|
||||
$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");
|
||||
$scope.services = [];
|
||||
});
|
||||
}
|
||||
|
||||
fetchServices();
|
||||
}]);
|
||||
67
app/components/settings/settings.html
Normal file
67
app/components/settings/settings.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Settings">
|
||||
</rd-header-title>
|
||||
<rd-header-content>Settings</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-lock" title="Change user password"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- current-password-input -->
|
||||
<div class="form-group">
|
||||
<label for="current_password" class="col-sm-2 control-label text-left">Current password</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input type="password" class="form-control" ng-model="formValues.currentPassword" id="current_password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !current-password-input -->
|
||||
<div class="form-group" style="margin-left: 5px;">
|
||||
<p>
|
||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword.length >= 8]" aria-hidden="true"></i>
|
||||
Your new password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
<!-- new-password-input -->
|
||||
<div class="form-group">
|
||||
<label for="new_password" class="col-sm-2 control-label text-left">New password</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input type="password" class="form-control" ng-model="formValues.newPassword" id="new_password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !new-password-input -->
|
||||
<!-- confirm-password-input -->
|
||||
<div class="form-group">
|
||||
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
|
||||
<div class="col-sm-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input type="password" class="form-control" ng-model="formValues.confirmPassword" id="confirm_password">
|
||||
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword !== '' && formValues.newPassword === formValues.confirmPassword]" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !confirm-password-input -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.currentPassword || formValues.newPassword.length < 8 || formValues.newPassword !== formValues.confirmPassword" ng-click="updatePassword()">Update password</button>
|
||||
</div>
|
||||
<div class="col-sm-10">
|
||||
<p class="pull-left text-danger" ng-if="invalidPassword" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Current password is not valid
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
30
app/components/settings/settingsController.js
Normal file
30
app/components/settings/settingsController.js
Normal file
@@ -0,0 +1,30 @@
|
||||
angular.module('settings', [])
|
||||
.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Users', 'Messages',
|
||||
function ($scope, $state, $sanitize, Users, Messages) {
|
||||
$scope.formValues = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
};
|
||||
|
||||
$scope.updatePassword = function() {
|
||||
$scope.invalidPassword = false;
|
||||
$scope.error = false;
|
||||
var currentPassword = $sanitize($scope.formValues.currentPassword);
|
||||
Users.checkPassword({ username: $scope.username, password: currentPassword }, function (d) {
|
||||
if (d.valid) {
|
||||
var newPassword = $sanitize($scope.formValues.newPassword);
|
||||
Users.update({ username: $scope.username, password: newPassword }, function (d) {
|
||||
Messages.send("Success", "Password successfully updated");
|
||||
$state.reload();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to update password");
|
||||
});
|
||||
} else {
|
||||
$scope.invalidPassword = true;
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to check password validity");
|
||||
});
|
||||
};
|
||||
}]);
|
||||
66
app/components/sidebar/sidebar.html
Normal file
66
app/components/sidebar/sidebar.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- Sidebar -->
|
||||
<div id="sidebar-wrapper">
|
||||
<ul class="sidebar">
|
||||
<li class="sidebar-main">
|
||||
<a ng-click="toggleSidebar()" class="interactive">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
|
||||
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
|
||||
<span class="menu-icon glyphicon glyphicon-transfer"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-title">
|
||||
<span>Active endpoint</span>
|
||||
</li>
|
||||
<li class="sidebar-title">
|
||||
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)">
|
||||
</select>
|
||||
</li>
|
||||
<li class="sidebar-title"><span>Endpoint actions</span></li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<a ui-sref="services" ui-sref-active="active">Services <span class="menu-icon fa fa-list-alt"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="containers" ui-sref-active="active">Containers <span class="menu-icon fa fa-server"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="images" ui-sref-active="active">Images <span class="menu-icon fa fa-clone"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="networks" ui-sref-active="active">Networks <span class="menu-icon fa fa-sitemap"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')">
|
||||
<a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||
<a ui-sref="docker" ui-sref-active="active">Docker <span class="menu-icon fa fa-th"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-title"><span>Portainer settings</span></li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="settings" ui-sref-active="active">Password <span class="menu-icon fa fa-lock"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list">
|
||||
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
<div class="col-xs-12">
|
||||
<a href="https://github.com/portainer/portainer" target="_blank">
|
||||
<i class="fa fa-github" aria-hidden="true"></i>
|
||||
Portainer {{ uiVersion }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Sidebar -->
|
||||
42
app/components/sidebar/sidebarController.js
Normal file
42
app/components/sidebar/sidebarController.js
Normal file
@@ -0,0 +1,42 @@
|
||||
angular.module('sidebar', [])
|
||||
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'Messages',
|
||||
function ($scope, $state, Settings, Config, EndpointService, StateManager, Messages) {
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
});
|
||||
|
||||
$scope.uiVersion = Settings.uiVersion;
|
||||
|
||||
$scope.switchEndpoint = function(endpoint) {
|
||||
EndpointService.setActive(endpoint.Id).then(function success(data) {
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
$state.reload();
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, "Unable to connect to the Docker endpoint");
|
||||
});
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, "Unable to switch to new endpoint");
|
||||
});
|
||||
};
|
||||
|
||||
function fetchEndpoints() {
|
||||
EndpointService.endpoints().then(function success(data) {
|
||||
$scope.endpoints = data;
|
||||
EndpointService.getActive().then(function success(data) {
|
||||
angular.forEach($scope.endpoints, function (endpoint) {
|
||||
if (endpoint.Id === data.Id) {
|
||||
$scope.activeEndpoint = endpoint;
|
||||
}
|
||||
});
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, "Unable to retrieve active endpoint");
|
||||
});
|
||||
}, function error(err) {
|
||||
$scope.endpoints = [];
|
||||
});
|
||||
}
|
||||
|
||||
fetchEndpoints();
|
||||
}]);
|
||||
@@ -1,7 +1,7 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Container stats"></rd-header-title>
|
||||
<rd-header-content>
|
||||
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="widget-icon grey pull-left">
|
||||
<i class="fa fa-tasks"></i>
|
||||
<i class="fa fa-server"></i>
|
||||
</div>
|
||||
<div class="title">{{ container.Name|trimcontainername }}</div>
|
||||
<div class="comment">
|
||||
@@ -54,20 +54,40 @@
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Processes"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-tasks" title="Processes">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-repeat="title in containerTop.Titles">{{title}}</th>
|
||||
<th ng-repeat="title in containerTop.Titles">
|
||||
<a ui-sref="stats({id: container.Id})" ng-click="order(title)">
|
||||
{{title}}
|
||||
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="processInfos in containerTop.Processes">
|
||||
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
||||
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="containerTop.Processes" class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
angular.module('stats', [])
|
||||
.controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
|
||||
function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
|
||||
.controller('StatsController', ['Pagination', '$scope', 'Messages', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
|
||||
function (Pagination, $scope, Messages, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
|
||||
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
|
||||
// TODO: Force memory scale to 0 - max memory
|
||||
$scope.ps_args = '';
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
|
||||
$scope.sortType = 'CMD';
|
||||
$scope.sortReverse = false;
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
|
||||
};
|
||||
$scope.getTop = function () {
|
||||
ContainerTop.get($stateParams.id, {
|
||||
ps_args: $scope.ps_args
|
||||
@@ -87,7 +98,7 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
},
|
||||
{
|
||||
scaleLabel: function (valueObj) {
|
||||
return humansizeFilter(parseInt(valueObj.value, 10));
|
||||
return humansizeFilter(parseInt(valueObj.value, 10), 2);
|
||||
},
|
||||
responsive: true
|
||||
//scaleOverride: true,
|
||||
@@ -100,12 +111,18 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
datasets: [networkRxDataset, networkTxDataset]
|
||||
}, {
|
||||
scaleLabel: function (valueObj) {
|
||||
return humansizeFilter(parseInt(valueObj.value, 10));
|
||||
return humansizeFilter(parseInt(valueObj.value, 10), 2);
|
||||
},
|
||||
responsive: true
|
||||
});
|
||||
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
|
||||
|
||||
function setUpdateStatsTimeout() {
|
||||
if(!destroyed) {
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
Container.stats({id: $stateParams.id}, function (d) {
|
||||
var arr = Object.keys(d).map(function (key) {
|
||||
@@ -121,15 +138,17 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
updateCpuChart(d);
|
||||
updateMemoryChart(d);
|
||||
updateNetworkChart(d);
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
setUpdateStatsTimeout();
|
||||
}, function () {
|
||||
Messages.error('Unable to retrieve stats', {}, 'Is this container running?');
|
||||
timeout = $timeout(updateStats, 5000);
|
||||
setUpdateStatsTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
var destroyed = false;
|
||||
var timeout;
|
||||
$scope.$on('$destroy', function () {
|
||||
destroyed = true;
|
||||
$timeout.cancel(timeout);
|
||||
});
|
||||
|
||||
@@ -154,16 +173,18 @@ function (Settings, $scope, Messages, $timeout, Container, ContainerTop, $stateP
|
||||
$scope.networkName = Object.keys(data.networks)[0];
|
||||
data.network = data.networks[$scope.networkName];
|
||||
}
|
||||
var rxBytes = 0, txBytes = 0;
|
||||
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
|
||||
// These will be zero on first call, ignore to prevent large graph spike
|
||||
rxBytes = data.network.rx_bytes - lastRxBytes;
|
||||
txBytes = data.network.tx_bytes - lastTxBytes;
|
||||
if(data.network) {
|
||||
var rxBytes = 0, txBytes = 0;
|
||||
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
|
||||
// These will be zero on first call, ignore to prevent large graph spike
|
||||
rxBytes = data.network.rx_bytes - lastRxBytes;
|
||||
txBytes = data.network.tx_bytes - lastTxBytes;
|
||||
}
|
||||
lastRxBytes = data.network.rx_bytes;
|
||||
lastTxBytes = data.network.tx_bytes;
|
||||
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
|
||||
networkChart.removeData();
|
||||
}
|
||||
lastRxBytes = data.network.rx_bytes;
|
||||
lastTxBytes = data.network.tx_bytes;
|
||||
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
|
||||
networkChart.removeData();
|
||||
}
|
||||
|
||||
function calculateCPUPercent(stats) {
|
||||
|
||||
@@ -16,13 +16,14 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nodes</td>
|
||||
<td>{{ swarm.Nodes }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ swarm.Nodes }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ info.Swarm.Nodes }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Images</td>
|
||||
<td>{{ info.Images }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Swarm version</td>
|
||||
<td>{{ docker.Version|swarmversion }}</td>
|
||||
</tr>
|
||||
@@ -30,27 +31,29 @@
|
||||
<td>Docker API version</td>
|
||||
<td>{{ docker.ApiVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Strategy</td>
|
||||
<td>{{ swarm.Strategy }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total CPU</td>
|
||||
<td>{{ info.NCPU }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ info.NCPU }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ totalCPU }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total memory</td>
|
||||
<td>{{ info.MemTotal|humansize }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ info.MemTotal|humansize: 2 }}</td>
|
||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">{{ totalMemory|humansize: 2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Operating system</td>
|
||||
<td>{{ info.OperatingSystem }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Kernel version</td>
|
||||
<td>{{ info.KernelVersion }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<td>Go version</td>
|
||||
<td>{{ docker.GoVersion }}</td>
|
||||
</tr>
|
||||
@@ -60,19 +63,31 @@
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-hdd-o" title="Node status"></rd-widget-header>
|
||||
<rd-widget-header icon="fa-hdd-o" title="Node status">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Name')">
|
||||
<a ui-sref="swarm" ng-click="order('name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<span ng-show="sortType == 'name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
@@ -90,30 +105,30 @@
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('IP')">
|
||||
<a ui-sref="swarm" ng-click="order('ip')">
|
||||
IP
|
||||
<span ng-show="sortType == 'IP' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<span ng-show="sortType == 'ip' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'ip' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Engine')">
|
||||
<a ui-sref="swarm" ng-click="order('version')">
|
||||
Engine
|
||||
<span ng-show="sortType == 'Engine' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Engine' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<span ng-show="sortType == 'version' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'version' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Status')">
|
||||
<a ui-sref="swarm" ng-click="order('status')">
|
||||
Status
|
||||
<span ng-show="sortType == 'Status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<span ng-show="sortType == 'status' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'status' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<tr dir-paginate="node in (state.filteredNodes = (swarm.Status | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td>{{ node.name }}</td>
|
||||
<td>{{ node.cpu }}</td>
|
||||
<td>{{ node.memory }}</td>
|
||||
@@ -123,6 +138,88 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-hdd-o" title="Node status">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Description.Hostname')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Description.Hostname' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Description.Hostname' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Spec.Role')">
|
||||
Role
|
||||
<span ng-show="sortType == 'Spec.Role' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Spec.Role' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Description.Resources.NanoCPUs')">
|
||||
CPU
|
||||
<span ng-show="sortType == 'Description.Resources.NanoCPUs' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Description.Resources.NanoCPUs' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Description.Resources.MemoryBytes')">
|
||||
Memory
|
||||
<span ng-show="sortType == 'Description.Resources.MemoryBytes' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Description.Resources.MemoryBytes' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('Description.Engine.EngineVersion')">
|
||||
Engine
|
||||
<span ng-show="sortType == 'Description.Engine.EngineVersion' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Description.Engine.EngineVersion' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="swarm" ng-click="order('node.Status.State')">
|
||||
Status
|
||||
<span ng-show="sortType == 'node.Status.State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'node.Status.State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><a ui-sref="node({id: node.ID})">{{ node.Description.Hostname }}</a></td>
|
||||
<td>{{ node.Spec.Role }}</td>
|
||||
<td>{{ node.Description.Resources.NanoCPUs / 1000000000 }}</td>
|
||||
<td>{{ node.Description.Resources.MemoryBytes|humansize }}</td>
|
||||
<td>{{ node.Description.Engine.EngineVersion }}</td>
|
||||
<td><span class="label label-{{ node.Status.State|nodestatusbadge }}">{{ node.Status.State }}</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
||||
@@ -1,62 +1,85 @@
|
||||
angular.module('swarm', [])
|
||||
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Settings',
|
||||
function ($scope, Info, Version, Settings) {
|
||||
.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Pagination',
|
||||
function ($scope, Info, Version, Node, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
|
||||
$scope.sortType = 'Spec.Role';
|
||||
$scope.sortReverse = false;
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
$scope.swarm = {};
|
||||
$scope.totalCPU = 0;
|
||||
$scope.totalMemory = 0;
|
||||
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
$scope.info = {};
|
||||
$scope.docker = {};
|
||||
$scope.swarm = {};
|
||||
$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;
|
||||
};
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('swarm_nodes', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
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 ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
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 nodes = systemStatus[0][1] === 'primary' ? systemStatus[3][1] : systemStatus[4][1];
|
||||
var node_count = parseInt(nodes, 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 if connected to a primary
|
||||
// If connected to a replica, information for node1 is available at element #5
|
||||
// The next 10 elements are information related to the node
|
||||
var node_offset = info[0][1] === 'primary' ? 4 : 5;
|
||||
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);
|
||||
}
|
||||
}]);
|
||||
|
||||
50
app/components/task/task.html
Normal file
50
app/components/task/task.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Task details">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="services">Services</a> > <a ui-sref="service({id: task.ServiceID})">{{ serviceName }}</a> > {{ task.ID }}
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Task status"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{{ task.ID }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>State</td>
|
||||
<td><span class="label label-{{ task.Status.State|taskstatusbadge }}">{{ task.Status.State }}</span></td>
|
||||
</tr>
|
||||
<tr ng-if="task.Status.Err">
|
||||
<td>Error message</td>
|
||||
<td><code>{{ task.Status.Err }}</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Image</td>
|
||||
<td>{{ task.Spec.ContainerSpec.Image }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slot</td>
|
||||
<td>{{ task.Slot }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ task.CreatedAt|getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="task.Status.ContainerStatus.ContainerID">
|
||||
<td>Container ID</td>
|
||||
<td>{{ task.Status.ContainerStatus.ContainerID }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
29
app/components/task/taskController.js
Normal file
29
app/components/task/taskController.js
Normal file
@@ -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();
|
||||
}]);
|
||||
@@ -3,42 +3,28 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="templates" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadTemplatesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Templates</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div id="selectedTemplate" class="row" ng-if="state.selectedTemplate">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-rocket" title="Available templates">
|
||||
<div class="pull-right">
|
||||
<i id="loadTemplatesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<div class="template-list">
|
||||
<div ng-repeat="tpl in templates" class="container-template hvr-grow" id="template_{{ $index }}" ng-click="selectTemplate($index)">
|
||||
<img class="logo" ng-src="{{ tpl.logo }}" />
|
||||
<div class="title">{{ tpl.title }}</div>
|
||||
<div class="description">{{ tpl.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="selectedTemplate">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cogs" title="Configuration"></rd-widget-header>
|
||||
<rd-widget-custom-header icon="state.selectedTemplate.logo" title="state.selectedTemplate.image">
|
||||
</rd-widget-custom-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0">
|
||||
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">When using Swarm, we recommend deploying containers in a shared network. Looks like you don't have any shared network, head over the <a ui-sref="networks">networks view</a> to create one.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<div class="col-sm-12">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
<span class="small text-muted">App templates cannot be used with swarm-mode at the moment. You can still use them to quickly deploy containers to the Docker host.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- name-and-network-inputs -->
|
||||
<div class="form-group">
|
||||
<label for="image_registry" class="col-sm-2 control-label text-left">Name</label>
|
||||
@@ -47,35 +33,113 @@
|
||||
</div>
|
||||
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
||||
<div class="col-sm-4">
|
||||
<select class="selectpicker form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
||||
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
||||
<option disabled hidden value="">Select a network</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-and-network-inputs -->
|
||||
<div ng-repeat="var in selectedTemplate.env" ng-if="!var.set" class="form-group">
|
||||
<div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
|
||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||
<div class="col-sm-10">
|
||||
<select ng-if="!swarm && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
||||
<select ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<select ng-if="swarm && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
||||
<select ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||
<option selected disabled hidden value="">Select a container</option>
|
||||
</select>
|
||||
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<a class="small interactive" ng-if="!state.showAdvancedOptions" ng-click="state.showAdvancedOptions = true;">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i> Show advanced options
|
||||
</a>
|
||||
<a class="small interactive" ng-if="state.showAdvancedOptions" ng-click="state.showAdvancedOptions = false;">
|
||||
<i class="fa fa-minus space-right" aria-hidden="true"></i> Hide advanced options
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="state.showAdvancedOptions">
|
||||
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
|
||||
<div class="col-sm-11" style="margin-top: 5px;">
|
||||
<span class="label label-default interactive" ng-click="addPortBinding()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
|
||||
</span>
|
||||
</div>
|
||||
<!-- port-mapping-input-list -->
|
||||
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="portBinding in formValues.ports" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">host</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">container</span>
|
||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<div class="input-group col-sm-1 input-group-sm">
|
||||
<select class="form-control" ng-model="portBinding.protocol">
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
|
||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !port-mapping-input-list -->
|
||||
</div>
|
||||
<!-- !port-mapping -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default btn-sm" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
||||
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="selectedTemplate">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12" style="text-align: center;">
|
||||
<div>
|
||||
<i id="createContainerSpinner" class="fa fa-cog fa-3x fa-spin" style="margin-bottom: 5px; display: none;"></i>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-lg" ng-disabled="!formValues.network" ng-click="createTemplate()">Create</button>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-rocket" title="Available templates">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<div class="template-list">
|
||||
<div dir-paginate="tpl in templates | itemsPerPage: state.pagination_count" class="container-template hvr-underline-from-center" id="template_{{ tpl.index }}" ng-click="selectTemplate(tpl.index)">
|
||||
<img class="logo" ng-src="{{ tpl.logo }}" />
|
||||
<div class="title">{{ tpl.title }}</div>
|
||||
<div class="description">{{ tpl.description }}</div>
|
||||
</div>
|
||||
<div ng-if="!templates" class="text-center text-muted">
|
||||
Loading...
|
||||
</div>
|
||||
<div ng-if="templates.length == 0" class="text-center text-muted">
|
||||
No templates available.
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="templates">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
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) {
|
||||
$scope.templates = [];
|
||||
$scope.selectedTemplate = null;
|
||||
.controller('TemplatesController', ['$scope', '$q', '$state', '$filter', '$anchorScroll', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'Templates', 'TemplateHelper', 'Messages', 'Pagination',
|
||||
function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, Templates, TemplateHelper, Messages, Pagination) {
|
||||
$scope.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false,
|
||||
pagination_count: Pagination.getPaginationCount('templates')
|
||||
};
|
||||
$scope.formValues = {
|
||||
network: "",
|
||||
name: ""
|
||||
name: "",
|
||||
ports: []
|
||||
};
|
||||
|
||||
var selectedItem = -1;
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('templates', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.addPortBinding = function() {
|
||||
$scope.formValues.ports.push({ hostPort: '', containerPort: '', protocol: 'tcp' });
|
||||
};
|
||||
|
||||
$scope.removePortBinding = function(index) {
|
||||
$scope.formValues.ports.splice(index, 1);
|
||||
};
|
||||
|
||||
// TODO: centralize, already present in createContainerController
|
||||
function createContainer(config) {
|
||||
Container.create(config, function (d) {
|
||||
@@ -75,6 +91,26 @@ function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image
|
||||
};
|
||||
}
|
||||
|
||||
function preparePortBindings(config, ports) {
|
||||
var bindings = {};
|
||||
ports.forEach(function (portBinding) {
|
||||
if (portBinding.containerPort) {
|
||||
var key = portBinding.containerPort + "/" + portBinding.protocol;
|
||||
var binding = {};
|
||||
if (portBinding.hostPort && portBinding.hostPort.indexOf(':') > -1) {
|
||||
var hostAndPort = portBinding.hostPort.split(':');
|
||||
binding.HostIp = hostAndPort[0];
|
||||
binding.HostPort = hostAndPort[1];
|
||||
} else {
|
||||
binding.HostPort = portBinding.hostPort;
|
||||
}
|
||||
bindings[key] = [binding];
|
||||
config.ExposedPorts[key] = {};
|
||||
}
|
||||
});
|
||||
config.HostConfig.PortBindings = bindings;
|
||||
}
|
||||
|
||||
function createConfigFromTemplate(template) {
|
||||
var containerConfig = getInitialConfiguration();
|
||||
containerConfig.Image = template.image;
|
||||
@@ -83,7 +119,7 @@ function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image
|
||||
if (v.value || v.set) {
|
||||
var val;
|
||||
if (v.type && v.type === 'container') {
|
||||
if ($scope.swarm && $scope.formValues.network.Scope === 'global') {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') {
|
||||
val = $filter('swarmcontainername')(v.value);
|
||||
} else {
|
||||
var container = v.value;
|
||||
@@ -96,15 +132,19 @@ function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image
|
||||
}
|
||||
});
|
||||
}
|
||||
if (template.ports) {
|
||||
template.ports.forEach(function (p) {
|
||||
containerConfig.ExposedPorts[p] = {};
|
||||
containerConfig.HostConfig.PortBindings[p] = [{ HostPort: ""}];
|
||||
});
|
||||
}
|
||||
preparePortBindings(containerConfig, $scope.formValues.ports);
|
||||
prepareImageConfig(containerConfig, template);
|
||||
return containerConfig;
|
||||
}
|
||||
|
||||
function prepareImageConfig(config, template) {
|
||||
var image = template.image;
|
||||
var registry = template.registry || '';
|
||||
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
|
||||
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
|
||||
$scope.imageConfig = imageConfig;
|
||||
}
|
||||
|
||||
function prepareVolumeQueries(template, containerConfig) {
|
||||
var volumeQueries = [];
|
||||
if (template.volumes) {
|
||||
@@ -129,15 +169,11 @@ function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image
|
||||
|
||||
$scope.createTemplate = function() {
|
||||
$('#createContainerSpinner').show();
|
||||
var template = $scope.selectedTemplate;
|
||||
var template = $scope.state.selectedTemplate;
|
||||
var containerConfig = createConfigFromTemplate(template);
|
||||
var imageConfig = {
|
||||
fromImage: template.image.split(':')[0],
|
||||
tag: template.image.split(':')[1] ? template.image.split(':')[1] : 'latest'
|
||||
};
|
||||
var createVolumeQueries = prepareVolumeQueries(template, containerConfig);
|
||||
$q.all(createVolumeQueries).then(function (d) {
|
||||
pullImageAndCreateContainer(imageConfig, containerConfig);
|
||||
pullImageAndCreateContainer($scope.imageConfig, containerConfig);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -145,30 +181,36 @@ function ($scope, $q, $state, $filter, Config, Container, ContainerHelper, Image
|
||||
$('#template_' + id).toggleClass("container-template--selected");
|
||||
if (selectedItem === id) {
|
||||
selectedItem = -1;
|
||||
$scope.selectedTemplate = null;
|
||||
$scope.state.selectedTemplate = null;
|
||||
} else {
|
||||
$('#template_' + selectedItem).toggleClass("container-template--selected");
|
||||
selectedItem = id;
|
||||
$scope.selectedTemplate = $scope.templates[id];
|
||||
var selectedTemplate = $scope.templates[id];
|
||||
$scope.state.selectedTemplate = selectedTemplate;
|
||||
$scope.formValues.ports = selectedTemplate.ports ? TemplateHelper.getPortBindings(selectedTemplate.ports) : [];
|
||||
$anchorScroll('selectedTemplate');
|
||||
}
|
||||
};
|
||||
|
||||
function initTemplates() {
|
||||
Templates.get(function (data) {
|
||||
$scope.templates = data;
|
||||
$scope.templates = data.map(function(tpl,index){
|
||||
tpl.index = index;
|
||||
return tpl;
|
||||
});
|
||||
$('#loadTemplatesSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadTemplatesSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve apps list");
|
||||
$scope.templates = [];
|
||||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if ($scope.swarm) {
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="volumes" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadVolumesSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Volumes</rd-header-content>
|
||||
</rd-header>
|
||||
@@ -11,12 +12,19 @@
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-cubes" title="Volumes">
|
||||
<div class="pull-right">
|
||||
<i id="loadVolumesSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
<a class="btn btn-default" type="button" ui-sref="actions.create.volume">Add volume</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
@@ -28,7 +36,9 @@
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="volumes" ng-click="order('Name')">
|
||||
Name
|
||||
@@ -53,14 +63,23 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse))">
|
||||
<tr dir-paginate="volume in (state.filteredVolumes = (volumes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||
<td><input type="checkbox" ng-model="volume.Checked" ng-change="selectItem(volume)"/></td>
|
||||
<td>{{ volume.Name|truncate:50 }}</td>
|
||||
<td>{{ volume.Driver }}</td>
|
||||
<td>{{ volume.Mountpoint }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!volumes">
|
||||
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="volumes.length == 0">
|
||||
<td colspan="4" class="text-center text-muted">No volumes available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="volumes" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
angular.module('volumes', [])
|
||||
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages',
|
||||
function ($scope, $state, Volume, Messages) {
|
||||
.controller('VolumesController', ['$scope', '$state', 'Volume', 'Messages', 'Pagination',
|
||||
function ($scope, $state, Volume, Messages, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('volumes');
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = true;
|
||||
|
||||
$scope.config = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('volumes', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.selectItems = function (allSelected) {
|
||||
angular.forEach($scope.state.filteredVolumes, function (volume) {
|
||||
if (volume.Checked !== allSelected) {
|
||||
volume.Checked = allSelected;
|
||||
$scope.selectItem(volume);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
@@ -36,9 +49,13 @@ function ($scope, $state, Volume, Messages) {
|
||||
if (volume.Checked) {
|
||||
counter = counter + 1;
|
||||
Volume.remove({name: volume.Name}, function (d) {
|
||||
Messages.send("Volume deleted", volume.Name);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
if (d.message) {
|
||||
Messages.error("Unable to remove volume", {}, d.message);
|
||||
} else {
|
||||
Messages.send("Volume deleted", volume.Name);
|
||||
var index = $scope.volumes.indexOf(volume);
|
||||
$scope.volumes.splice(index, 1);
|
||||
}
|
||||
complete();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to remove volume");
|
||||
@@ -51,11 +68,12 @@ function ($scope, $state, Volume, Messages) {
|
||||
function fetchVolumes() {
|
||||
$('#loadVolumesSpinner').show();
|
||||
Volume.query({}, function (d) {
|
||||
$scope.volumes = d.Volumes;
|
||||
$scope.volumes = d.Volumes || [];
|
||||
$('#loadVolumesSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadVolumesSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve volumes");
|
||||
$scope.volumes = [];
|
||||
});
|
||||
}
|
||||
fetchVolumes();
|
||||
|
||||
@@ -4,7 +4,7 @@ angular
|
||||
var directive = {
|
||||
requires: '^rdHeader',
|
||||
transclude: true,
|
||||
template: '<div class="breadcrumb-links" ng-transclude></div>',
|
||||
template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right"><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u>log out <i class="fa fa-sign-out" aria-hidden="true"></i></u></a></div></div>',
|
||||
restrict: 'E'
|
||||
};
|
||||
return directive;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user