Compare commits

..

22 Commits

Author SHA1 Message Date
Anthony Lapenna 0aa84422da chore(version): bump version number 2016-08-18 15:49:18 +12:00
Anthony Lapenna f744b4c234 fix(jshint): fix lint issue 2016-08-18 13:37:10 +12:00
Anthony Lapenna c2ffb73358 feat(image): use a dropdown box to list available registries when tagging an image 2016-08-18 08:51:35 +12:00
Anthony Lapenna ada649b05a Merge branch 'docs140-logo-flag' into internal 2016-08-17 21:53:36 +12:00
Anthony Lapenna 7e5a9313a0 docs(logo): add documentation about the --logo flag 2016-08-17 21:51:39 +12:00
Anthony Lapenna 62eda46bac Revert "refactor(container-creation): remove changes related to internal version"
This reverts commit 61d7b4f64c.
2016-08-17 19:35:41 +12:00
Anthony Lapenna 76c179948e Merge branch 'fix122-invalid-port-specification' into internal 2016-08-17 19:35:38 +12:00
Anthony Lapenna ff04044001 fix(container-creation): allow to specify an address in the host port binding 2016-08-17 19:30:21 +12:00
Anthony Lapenna a752e8bc54 Merge branch 'fix123-container-creation-missing-port-bindings' into internal 2016-08-17 19:06:17 +12:00
Anthony Lapenna f14cbc5967 fix(container-creation): fix unregistered ports bindings when creating a container 2016-08-17 18:57:38 +12:00
Anthony Lapenna 7ebf842601 Merge branch 'docs-reverse-proxy' into internal 2016-08-17 18:32:26 +12:00
Anthony Lapenna 8dcac532d6 docs(reverse-proxy): add reverse proxy instructions 2016-08-17 18:30:21 +12:00
Anthony Lapenna 39d3753acb Merge branch 'feat111-exec-protocol-awareness' into internal 2016-08-17 18:24:10 +12:00
Anthony Lapenna 1e13aa660e feat(console): add protocol awareness to switch between ws:// and wss:// 2016-08-17 18:22:31 +12:00
Anthony Lapenna 0bd599f98a fix(api): remove git conflict trace 2016-08-17 18:16:01 +12:00
Anthony Lapenna f6e05538d8 Revert "refactor(global): revert merge with internal (#133)"
This reverts commit eefa7ca138.
2016-08-17 18:09:55 +12:00
Anthony Lapenna 0acbc10095 Merge branch 'feat125-tag-image' into internal 2016-08-17 18:06:09 +12:00
Anthony Lapenna aa1017c260 feat(image): add the ability to tag an image 2016-08-17 18:04:29 +12:00
Anthony Lapenna 9cb7483e0a Merge branch 'refactor-remove-binary' into internal 2016-08-17 13:58:15 +12:00
Anthony Lapenna 7d4ae1a6fd refactor(api): remove the binary from versioning 2016-08-17 13:56:09 +12:00
Anthony Lapenna fa5c1d16b6 Merge branch 'refactor124-response-handlers' into internal 2016-08-17 13:53:57 +12:00
Anthony Lapenna c928af71a4 refactor(handlers): remove duplicated code 2016-08-17 13:42:33 +12:00
228 changed files with 3382 additions and 15644 deletions
-44
View File
@@ -1,44 +0,0 @@
<!--
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:
+7 -3
View File
@@ -1,6 +1,10 @@
logs/*
!.gitkeep
*.esproj/*
node_modules
bower_components
.idea
*.iml
dist
portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp
dist/*
ui-for-docker-checksum.txt
+1 -1
View File
@@ -1 +1 @@
portainer
dockerui
-79
View File
@@ -1,79 +0,0 @@
# 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
* **advanced**: a task that require a deep understanding of the project codebase
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
* advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced
### 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
+8
View File
@@ -0,0 +1,8 @@
FROM scratch
COPY dist /
VOLUME /data
EXPOSE 9000
ENTRYPOINT ["/ui-for-docker"]
+17 -17
View File
@@ -1,24 +1,24 @@
Portainer: Copyright (c) 2016 Portainer.io
Cloudinovasi-ui: Copyright (c) 2016 Cloudinovasi
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.
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:
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 above copyright notice and this permission notice shall be included in all
copies or substantial portions of 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.
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.
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)
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (anthonylapenna at cloudinovasi dot id)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1
View File
@@ -0,0 +1 @@
web: dockerui -p ":$PORT" -e "$DOCKER_ENDPOINT"
+151 -31
View File
@@ -1,51 +1,171 @@
# Cloudinovasi UI for Docker
<p align="center">
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
</p>
This UI is dedicated to CloudInovasi internal usage.
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/latest/?badge=stable)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
A fork of the amazing UI for Docker by Michael Crosby and Kevan Ahlquist (https://github.com/kevana/ui-for-docker) using the rdash-angular theme (https://github.com/rdash/rdash-angular).
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your Docker host or Swarm cluster.
![Dashboard](/dashboard.png)
**_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).
UI For Docker is a web interface for the Docker Remote API. The goal is to provide a pure client side implementation so it is effortless to connect and manage docker.
**_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*.
## Goals
* Minimal dependencies - I really want to keep this project a pure html/js app.
* Consistency - The web UI should be consistent with the commands found on the docker CLI.
## Demo
## Supported Docker versions
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
The current Docker version support policy is the following: `N` to `N-2` included where `N` is the latest version.
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
At the moment, the following versions are supported: 1.9, 1.10 & 1.11.
Please note that the public demo cluster is **reset every 15min**.
## Run
## Getting started
### Quickstart
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
* [Documentation](https://portainer.readthedocs.io)
1. Run: `docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui`
## Getting help
2. Open your browser to `http://<dockerd host ip>:9000`
* 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/
Bind mounting the Unix socket into the UI For Docker container is much more secure than exposing your docker daemon over TCP.
## Reporting bugs and contributing
The `--privileged` flag is required for hosts using SELinux.
* 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!
### Specify socket to connect to Docker daemon
## Limitations
By default UI For Docker connects to the Docker daemon with`/var/run/docker.sock`. For this to work you need to bind mount the unix socket into the container with `-v /var/run/docker.sock:/var/run/docker.sock`.
**_Portainer_** has full support for the following Docker versions:
You can use the `--host`, `-H` flags to change this socket:
* Docker 1.10 to Docker 17.03 (including `swarm-mode`)
* Docker Swarm >= 1.2.3
```
# Connect to a tcp socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H tcp://127.0.0.1:2375
```
Partial support for the following Docker versions (some features may not be available):
```
# Connect to another unix socket:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -H unix:///path/to/docker.sock
```
* Docker 1.9
### Swarm support
**Supported Swarm version: 1.2.3**
You can access a specific view for you Swarm cluster by defining the `--swarm` flag:
```
# Connect to a tcp socket and enable Swarm:
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -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 UI For Docker is served on
UI For Docker listens on port 9000 by default. If you run UI For Docker inside a container then you can bind the container's internal port to any external address and port:
```
# Expose UI For Docker 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/cloudinovasi-ui
```
### Access a Docker engine protected via TLS
Ensure that you have access to the CA, the cert and the public key used to access your Docker engine.
These files will need to be named `ca.pem`, `cert.pem` and `key.pem` respectively. Store them somewhere on your disk and mount a volume containing these files inside the UI container:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify
```
You can also use the `--tlscacert`, `--tlscert` and `--tlskey` flags if you want to change the default path to the CA, certificate and key file respectively:
```
$ docker run -d -p 9000:9000 cloudinovasi/cloudinovasi-ui -v /path/to/certs:/certs -H https://my-docker-host.domain:2376 --tlsverify --tlscacert /certs/myCa.pem --tlscert /certs/myCert.pem --tlskey /certs/myKey.pem
```
*Note*: Replace `/path/to/certs` to the path to the certificate files on your disk.
### 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/cloudinovasi-ui --logo "https://www.docker.com/sites/all/themes/docker/assets/images/brand-full.svg"
```
The custom logo will replace the CloudInovasi 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/cloudinovasi-ui -l owner=acme
```
### Custom Docker registries support
You can specify the support of others registries than DockerHub by using the `--registries` or `-r` options and specifying a registry using the format *REGISTRY_NAME=REGISTRY_ADDRESS*.
For example, if I want the registry 'myCustomRegistry' pointing to *myregistry.domain.com:5000* available in the UI:
```
$ docker run -d -p 9000:9000 --privileged -v /var/run/docker.sock:/var/run/docker.sock cloudinovasi/cloudinovasi-ui -r myCustomRegistry=myregistry.domain.com:5000
```
### Reverse proxy configuration
Has been tested with Nginx 1.11.
Use the following configuration to host the UI at `myhost.mydomain.com/dockerui`:
```nginx
upstream cloudinovasi-ui {
server ADDRESS:PORT;
}
server {
listen 80;
location /dockerui/ {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://cloudinovasi-ui/;
}
location /dockerui/ws/ {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://cloudinovasi-ui/ws/;
}
}
```
Replace `ADDRESS:PORT` with the CloudInovasi UI container details.
### Available options
The following options are available for the `ui-for-docker` binary:
* `--host`, `-H`: Docker daemon endpoint (default: `"unix:///var/run/docker.sock"`)
* `--bind`, `-p`: Address and port to serve UI For Docker (default: `":9000"`)
* `--data`, `-d`: Path to the data folder (default: `"."`)
* `--assets`, `-a`: Path to the assets (default: `"."`)
* `--swarm`, `-s`: Swarm cluster support (default: `false`)
* `--registries`, `-r`: Available registries in the UI (format *REGISTRY_NAME=REGISTRY_ADDRESS*)
* `--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
+57
View File
@@ -0,0 +1,57 @@
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
}
apiConfig struct {
Endpoint string
BindAddress string
AssetPath string
DataPath string
SwarmSupport bool
TLSEnabled bool
TLSCACertPath string
TLSCertPath string
TLSKeyPath 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,
}
}
-76
View File
@@ -1,76 +0,0 @@
package bolt
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
)
type Migrator struct {
UserService *UserService
EndpointService *EndpointService
ResourceControlService *ResourceControlService
VersionService *VersionService
CurrentDBVersion int
store *Store
}
func NewMigrator(store *Store, version int) *Migrator {
return &Migrator{
UserService: store.UserService,
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
VersionService: store.VersionService,
CurrentDBVersion: version,
store: store,
}
}
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.CurrentDBVersion == 0 {
err := m.updateAdminUser()
if err != nil {
return err
}
}
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
}
return nil
}
func (m *Migrator) updateAdminUser() error {
u, err := m.UserService.UserByUsername("admin")
if err == nil {
admin := &portainer.User{
Username: "admin",
Password: u.Password,
Role: portainer.AdministratorRole,
}
err = m.UserService.CreateUser(admin)
if err != nil {
return err
}
err = m.removeLegacyAdminUser()
if err != nil {
return err
}
} else if err != nil && err != portainer.ErrUserNotFound {
return err
}
return nil
}
func (m *Migrator) removeLegacyAdminUser() error {
return m.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err := bucket.Delete([]byte("admin"))
if err != nil {
return err
}
return nil
})
}
-136
View File
@@ -1,136 +0,0 @@
package bolt
import (
"log"
"os"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer"
)
// 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
ResourceControlService *ResourceControlService
VersionService *VersionService
db *bolt.DB
checkForDataMigration bool
}
const (
databaseFileName = "portainer.db"
versionBucketName = "version"
userBucketName = "users"
endpointBucketName = "endpoints"
containerResourceControlBucketName = "containerResourceControl"
serviceResourceControlBucketName = "serviceResourceControl"
volumeResourceControlBucketName = "volumeResourceControl"
)
// NewStore initializes a new Store and the associated services
func NewStore(storePath string) (*Store, error) {
store := &Store{
Path: storePath,
UserService: &UserService{},
EndpointService: &EndpointService{},
ResourceControlService: &ResourceControlService{},
VersionService: &VersionService{},
}
store.UserService.store = store
store.EndpointService.store = store
store.ResourceControlService.store = store
store.VersionService.store = store
_, err := os.Stat(storePath + "/" + databaseFileName)
if err != nil && os.IsNotExist(err) {
store.checkForDataMigration = false
} else if err != nil {
return nil, err
} else {
store.checkForDataMigration = true
}
return store, nil
}
// 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(versionBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(userBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(containerResourceControlBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(serviceResourceControlBucketName))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(volumeResourceControlBucketName))
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
}
// MigrateData automatically migrate the data based on the DBVersion.
func (store *Store) MigrateData() error {
if !store.checkForDataMigration {
err := store.VersionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return err
}
return nil
}
version, err := store.VersionService.DBVersion()
if err == portainer.ErrDBVersionNotFound {
version = 0
} else if err != nil {
return err
}
if version < portainer.DBVersion {
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
migrator := NewMigrator(store, version)
err = migrator.Migrate()
if err != nil {
return err
}
}
return nil
}
-154
View File
@@ -1,154 +0,0 @@
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
}
// 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
}
// Synchronize creates, updates and deletes endpoints inside a single transaction.
func (service *EndpointService) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(endpointBucketName))
for _, endpoint := range toCreate {
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toUpdate {
err := marshalAndStoreEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
for _, endpoint := range toDelete {
err := bucket.Delete(internal.Itob(int(endpoint.ID)))
if err != nil {
return err
}
}
return 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))
err := storeNewEndpoint(endpoint, bucket)
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
})
}
func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
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
}
func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error {
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
return marshalAndStoreEndpoint(endpoint, bucket)
}
-47
View File
@@ -1,47 +0,0 @@
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)
}
// MarshalResourceControl encodes a resource control object to binary format.
func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) {
return json.Marshal(rc)
}
// UnmarshalResourceControl decodes a resource control object from a binary data.
func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error {
return json.Unmarshal(data, rc)
}
// 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
}
-110
View File
@@ -1,110 +0,0 @@
package bolt
import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/internal"
"github.com/boltdb/bolt"
)
// ResourceControlService represents a service for managing resource controls.
type ResourceControlService struct {
store *Store
}
func getBucketNameByResourceControlType(rcType portainer.ResourceControlType) string {
bucketName := containerResourceControlBucketName
if rcType == portainer.ServiceResourceControl {
bucketName = serviceResourceControlBucketName
} else if rcType == portainer.VolumeResourceControl {
bucketName = volumeResourceControlBucketName
}
return bucketName
}
// ResourceControl returns a resource control object by resource ID
func (service *ResourceControlService) ResourceControl(resourceID string, rcType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var data []byte
bucketName := getBucketNameByResourceControlType(rcType)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get([]byte(resourceID))
if value == nil {
return nil
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return nil, err
}
if data == nil {
return nil, nil
}
var rc portainer.ResourceControl
err = internal.UnmarshalResourceControl(data, &rc)
if err != nil {
return nil, err
}
return &rc, nil
}
// ResourceControls returns all resource control objects
func (service *ResourceControlService) ResourceControls(rcType portainer.ResourceControlType) ([]portainer.ResourceControl, error) {
var rcs = make([]portainer.ResourceControl, 0)
bucketName := getBucketNameByResourceControlType(rcType)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var rc portainer.ResourceControl
err := internal.UnmarshalResourceControl(v, &rc)
if err != nil {
return err
}
rcs = append(rcs, rc)
}
return nil
})
if err != nil {
return nil, err
}
return rcs, nil
}
// CreateResourceControl creates a new resource control
func (service *ResourceControlService) CreateResourceControl(resourceID string, rc *portainer.ResourceControl, rcType portainer.ResourceControlType) error {
bucketName := getBucketNameByResourceControlType(rcType)
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := internal.MarshalResourceControl(rc)
if err != nil {
return err
}
err = bucket.Put([]byte(resourceID), data)
if err != nil {
return err
}
return nil
})
}
// DeleteResourceControl deletes a resource control object by resource ID
func (service *ResourceControlService) DeleteResourceControl(resourceID string, rcType portainer.ResourceControlType) error {
bucketName := getBucketNameByResourceControlType(rcType)
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
err := bucket.Delete([]byte(resourceID))
if err != nil {
return err
}
return nil
})
}
-170
View File
@@ -1,170 +0,0 @@
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 ID
func (service *UserService) User(ID portainer.UserID) (*portainer.User, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
value := bucket.Get(internal.Itob(int(ID)))
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
}
// UserByUsername returns a user by username.
func (service *UserService) UserByUsername(username string) (*portainer.User, error) {
var user *portainer.User
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var u portainer.User
err := internal.UnmarshalUser(v, &u)
if err != nil {
return err
}
if u.Username == username {
user = &u
}
}
if user == nil {
return portainer.ErrUserNotFound
}
return nil
})
if err != nil {
return nil, err
}
return user, nil
}
// Users return an array containing all the users.
func (service *UserService) Users() ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalUser(v, &user)
if err != nil {
return err
}
users = append(users, user)
}
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
// UsersByRole return an array containing all the users with the specified role.
func (service *UserService) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var user portainer.User
err := internal.UnmarshalUser(v, &user)
if err != nil {
return err
}
if user.Role == role {
users = append(users, user)
}
}
return nil
})
if err != nil {
return nil, err
}
return users, nil
}
// UpdateUser saves a user.
func (service *UserService) UpdateUser(ID portainer.UserID, 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(internal.Itob(int(ID)), data)
if err != nil {
return err
}
return nil
})
}
// CreateUser creates a new user.
func (service *UserService) CreateUser(user *portainer.User) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
id, _ := bucket.NextSequence()
user.ID = portainer.UserID(id)
data, err := internal.MarshalUser(user)
if err != nil {
return err
}
err = bucket.Put(internal.Itob(int(user.ID)), data)
if err != nil {
return err
}
return nil
})
}
// DeleteUser deletes a user.
func (service *UserService) DeleteUser(ID portainer.UserID) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(userBucketName))
err := bucket.Delete(internal.Itob(int(ID)))
if err != nil {
return err
}
return nil
})
}
-58
View File
@@ -1,58 +0,0 @@
package bolt
import (
"strconv"
"github.com/portainer/portainer"
"github.com/boltdb/bolt"
)
// EndpointService represents a service for managing users.
type VersionService struct {
store *Store
}
const (
DBVersionKey = "DB_VERSION"
)
// DBVersion the stored database version.
func (service *VersionService) DBVersion() (int, error) {
var data []byte
err := service.store.db.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(versionBucketName))
value := bucket.Get([]byte(DBVersionKey))
if value == nil {
return portainer.ErrDBVersionNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
})
if err != nil {
return 0, err
}
dbVersion, err := strconv.Atoi(string(data))
if err != nil {
return 0, err
}
return dbVersion, nil
}
// StoreDBVersion store the database version.
func (service *VersionService) StoreDBVersion(version int) error {
return service.store.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(versionBucketName))
data := []byte(strconv.Itoa(version))
err := bucket.Put([]byte(DBVersionKey), data)
if err != nil {
return err
}
return nil
})
}
-115
View File
@@ -1,115 +0,0 @@
package cli
import (
"time"
"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")
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
)
// 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')),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
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(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(),
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 != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
err := validateEndpoint(*flags.Endpoint)
if err != nil {
return err
}
err = validateExternalEndpoints(*flags.ExternalEndpoints)
if err != nil {
return err
}
err = validateSyncInterval(*flags.SyncInterval)
if err != nil {
return err
}
return nil
}
func validateEndpoint(endpoint string) error {
if endpoint != "" {
if !strings.HasPrefix(endpoint, "unix://") && !strings.HasPrefix(endpoint, "tcp://") {
return errInvalidEnpointProtocol
}
if strings.HasPrefix(endpoint, "unix://") {
socketPath := strings.TrimPrefix(endpoint, "unix://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketNotFound
}
return err
}
}
}
return nil
}
func validateExternalEndpoints(externalEndpoints string) error {
if externalEndpoints != "" {
if _, err := os.Stat(externalEndpoints); err != nil {
if os.IsNotExist(err) {
return errEndpointsFileNotFound
}
return err
}
}
return nil
}
func validateSyncInterval(syncInterval string) error {
if syncInterval != defaultSyncInterval {
_, err := time.ParseDuration(syncInterval)
if err != nil {
return errInvalidSyncInterval
}
}
return nil
}
-17
View File
@@ -1,17 +0,0 @@
// +build !windows
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultSyncInterval = "60s"
)
-15
View File
@@ -1,15 +0,0 @@
package cli
const (
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultSyncInterval = "60s"
)
-163
View File
@@ -1,163 +0,0 @@
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/cron"
"github.com/portainer/portainer/crypto"
"github.com/portainer/portainer/file"
"github.com/portainer/portainer/http"
"github.com/portainer/portainer/jwt"
"log"
)
func initCLI() *portainer.CLIFlags {
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)
}
return flags
}
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := file.NewService(dataStorePath, "")
if err != nil {
log.Fatal(err)
}
return fileService
}
func initStore(dataStorePath string) *bolt.Store {
store, err := bolt.NewStore(dataStorePath)
if err != nil {
log.Fatal(err)
}
err = store.Open()
if err != nil {
log.Fatal(err)
}
err = store.MigrateData()
if err != nil {
log.Fatal(err)
}
return store
}
func initJWTService(authenticationEnabled bool) portainer.JWTService {
if authenticationEnabled {
jwtService, err := jwt.NewService()
if err != nil {
log.Fatal(err)
}
return jwtService
}
return nil
}
func initCryptoService() portainer.CryptoService {
return &crypto.Service{}
}
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
authorizeEndpointMgmt := true
if externalEnpointFile != "" {
authorizeEndpointMgmt = false
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
endpointWatcher := cron.NewWatcher(endpointService, syncInterval)
err := endpointWatcher.WatchEndpointFile(externalEnpointFile)
if err != nil {
log.Fatal(err)
}
}
return authorizeEndpointMgmt
}
func initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Settings {
return &portainer.Settings{
HiddenLabels: *flags.Labels,
Logo: *flags.Logo,
Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
}
}
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
endpoints, err := endpointService.Endpoints()
if err != nil {
log.Fatal(err)
}
return &endpoints[0]
}
func main() {
flags := initCLI()
fileService := initFileService(*flags.Data)
store := initStore(*flags.Data)
defer store.Close()
jwtService := initJWTService(!*flags.NoAuth)
cryptoService := initCryptoService()
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
settings := initSettings(authorizeEndpointMgmt, flags)
if *flags.Endpoint != "" {
var endpoints []portainer.Endpoint
endpoints, err := store.EndpointService.Endpoints()
if err != nil {
log.Fatal(err)
}
if len(endpoints) == 0 {
endpoint := &portainer.Endpoint{
Name: "primary",
URL: *flags.Endpoint,
TLS: *flags.TLSVerify,
TLSCACertPath: *flags.TLSCacert,
TLSCertPath: *flags.TLSCert,
TLSKeyPath: *flags.TLSKey,
}
err = store.EndpointService.CreateEndpoint(endpoint)
if err != nil {
log.Fatal(err)
}
} else {
log.Println("Instance already has defined endpoints. Skipping the endpoint defined via CLI.")
}
}
var server portainer.Server = &http.Server{
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
Settings: settings,
TemplatesURL: *flags.Templates,
AuthDisabled: *flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
UserService: store.UserService,
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
}
log.Printf("Starting Portainer on %s", *flags.Addr)
err := server.Start()
if err != nil {
log.Fatal(err)
}
}
-174
View File
@@ -1,174 +0,0 @@
package cron
import (
"encoding/json"
"io/ioutil"
"log"
"os"
"strings"
"github.com/portainer/portainer"
)
type (
endpointSyncJob struct {
logger *log.Logger
endpointService portainer.EndpointService
endpointFilePath string
}
synchronization struct {
endpointsToCreate []*portainer.Endpoint
endpointsToUpdate []*portainer.Endpoint
endpointsToDelete []*portainer.Endpoint
}
)
const (
// ErrEmptyEndpointArray is an error raised when the external endpoint source array is empty.
ErrEmptyEndpointArray = portainer.Error("External endpoint source is empty")
)
func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob {
return endpointSyncJob{
logger: log.New(os.Stderr, "", log.LstdFlags),
endpointService: endpointService,
endpointFilePath: endpointFilePath,
}
}
func endpointSyncError(err error, logger *log.Logger) bool {
if err != nil {
logger.Printf("Endpoint synchronization error: %s", err)
return true
}
return false
}
func isValidEndpoint(endpoint *portainer.Endpoint) bool {
if endpoint.Name != "" && endpoint.URL != "" {
if !strings.HasPrefix(endpoint.URL, "unix://") && !strings.HasPrefix(endpoint.URL, "tcp://") {
return false
}
return true
}
return false
}
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
for idx, v := range endpoints {
if endpoint.Name == v.Name && isValidEndpoint(&v) {
return idx
}
}
return -1
}
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var endpoint *portainer.Endpoint
if original.URL != updated.URL || original.TLS != updated.TLS ||
(updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) ||
(updated.TLS && original.TLSCertPath != updated.TLSCertPath) ||
(updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) {
endpoint = original
endpoint.URL = updated.URL
if updated.TLS {
endpoint.TLS = true
endpoint.TLSCACertPath = updated.TLSCACertPath
endpoint.TLSCertPath = updated.TLSCertPath
endpoint.TLSKeyPath = updated.TLSKeyPath
} else {
endpoint.TLS = false
endpoint.TLSCACertPath = ""
endpoint.TLSCertPath = ""
endpoint.TLSKeyPath = ""
}
}
return endpoint
}
func (sync synchronization) requireSync() bool {
if len(sync.endpointsToCreate) != 0 || len(sync.endpointsToUpdate) != 0 || len(sync.endpointsToDelete) != 0 {
return true
}
return false
}
// TMP: endpointSyncJob method to access logger, should be generic
func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
endpointsToCreate := make([]*portainer.Endpoint, 0)
endpointsToUpdate := make([]*portainer.Endpoint, 0)
endpointsToDelete := make([]*portainer.Endpoint, 0)
for idx := range storedEndpoints {
fidx := endpointExists(&storedEndpoints[idx], fileEndpoints)
if fidx != -1 {
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
if endpoint != nil {
job.logger.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
endpointsToUpdate = append(endpointsToUpdate, endpoint)
} else {
job.logger.Printf("No change detected for a stored endpoint. [name: %v] [url: %v]\n", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
}
} else {
job.logger.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
}
}
for idx, endpoint := range fileEndpoints {
if endpoint.Name == "" || endpoint.URL == "" {
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
continue
}
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
if sidx == -1 {
job.logger.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
}
}
return &synchronization{
endpointsToCreate: endpointsToCreate,
endpointsToUpdate: endpointsToUpdate,
endpointsToDelete: endpointsToDelete,
}
}
func (job endpointSyncJob) Sync() error {
data, err := ioutil.ReadFile(job.endpointFilePath)
if endpointSyncError(err, job.logger) {
return err
}
var fileEndpoints []portainer.Endpoint
err = json.Unmarshal(data, &fileEndpoints)
if endpointSyncError(err, job.logger) {
return err
}
if len(fileEndpoints) == 0 {
return ErrEmptyEndpointArray
}
storedEndpoints, err := job.endpointService.Endpoints()
if endpointSyncError(err, job.logger) {
return err
}
sync := job.prepareSyncData(storedEndpoints, fileEndpoints)
if sync.requireSync() {
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
if endpointSyncError(err, job.logger) {
return err
}
job.logger.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
}
return nil
}
func (job endpointSyncJob) Run() {
job.logger.Println("Endpoint synchronization job started.")
err := job.Sync()
endpointSyncError(err, job.logger)
}
-40
View File
@@ -1,40 +0,0 @@
package cron
import (
"github.com/portainer/portainer"
"github.com/robfig/cron"
)
// Watcher represents a service for managing crons.
type Watcher struct {
Cron *cron.Cron
EndpointService portainer.EndpointService
syncInterval string
}
// NewWatcher initializes a new service.
func NewWatcher(endpointService portainer.EndpointService, syncInterval string) *Watcher {
return &Watcher{
Cron: cron.New(),
EndpointService: endpointService,
syncInterval: syncInterval,
}
}
// WatchEndpointFile starts a cron job to synchronize the endpoints from a file
func (watcher *Watcher) WatchEndpointFile(endpointFilePath string) error {
job := newEndpointSyncJob(endpointFilePath, watcher.EndpointService)
err := job.Sync()
if err != nil {
return err
}
err = watcher.Cron.AddJob("@every "+watcher.syncInterval, job)
if err != nil {
return err
}
watcher.Cron.Start()
return nil
}
-22
View File
@@ -1,22 +0,0 @@
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
View File
@@ -0,0 +1,48 @@
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)
})
}
-48
View File
@@ -1,48 +0,0 @@
package portainer
// General errors.
const (
ErrUnauthorized = Error("Unauthorized")
ErrResourceAccessDenied = Error("Access denied to resource")
)
// User errors.
const (
ErrUserNotFound = Error("User not found")
ErrUserAlreadyExists = Error("User already exists")
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
)
// Endpoint errors.
const (
ErrEndpointNotFound = Error("Endpoint not found")
ErrEndpointAccessDenied = Error("Access denied to endpoint")
)
// Version errors.
const (
ErrDBVersionNotFound = Error("DB version not found")
)
// Crypto errors.
const (
ErrCryptoHashFailure = Error("Unable to hash data")
)
// JWT errors.
const (
ErrSecretGeneration = Error("Unable to generate secret key")
ErrInvalidJWTToken = Error("Invalid JWT token")
ErrMissingContextData = Error("Unable to find JWT data in request context")
)
// 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
View File
@@ -0,0 +1,24 @@
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
View File
@@ -1,142 +0,0 @@
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
}
+16 -10
View File
@@ -1,40 +1,46 @@
package cli
package main
import (
"github.com/portainer/portainer"
"fmt"
"gopkg.in/alecthomas/kingpin.v2"
"strings"
)
type pairList []portainer.Pair
// pair defines a key/value pair
type pair struct {
Name string `json:"name"`
Value string `json:"value"`
}
// Set implementation for a list of portainer.Pair
// pairList defines an array of Label
type pairList []pair
// Set implementation for Labels
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(portainer.Pair)
p := new(pair)
p.Name = parts[0]
p.Value = parts[1]
*l = append(*l, *p)
return nil
}
// String implementation for a list of pair
// String implementation for Labels
func (l *pairList) String() string {
return ""
}
// IsCumulative implementation for a list of pair
// IsCumulative implementation for Labels
func (l *pairList) IsCumulative() bool {
return true
}
func pairs(s kingpin.Settings) (target *[]portainer.Pair) {
target = new([]portainer.Pair)
// LabelParser defines a custom parser for Labels flags
func pairs(s kingpin.Settings) (target *[]pair) {
target = new([]pair)
s.SetValue((*pairList)(target))
return
}
+75
View File
@@ -0,0 +1,75 @@
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)
})
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
}
@@ -1,4 +1,4 @@
package http
package main
import (
"bytes"
@@ -6,86 +6,12 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/portainer/portainer"
"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
EndpointService portainer.EndpointService
}
// 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")
edpID := qry.Get("endpointId")
parsedID, err := strconv.Atoi(edpID)
if err != nil {
log.Printf("Unable to parse endpoint ID: %s", err)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
log.Printf("Unable to retrieve endpoint: %s", err)
return
}
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
log.Printf("Unable to parse endpoint URL: %s", err)
return
}
var host string
if endpointURL.Scheme == "tcp" {
host = endpointURL.Host
} else if endpointURL.Scheme == "unix" {
host = endpointURL.Path
}
// Should not be managed here
var tlsConfig *tls.Config
if endpoint.TLS {
tlsConfig, err = createTLSConfiguration(endpoint.TLSCACertPath,
endpoint.TLSCertPath,
endpoint.TLSKeyPath)
if err != nil {
log.Fatalf("Unable to create TLS configuration: %s", err)
return
}
}
if err := hijack(host, endpointURL.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
-109
View File
@@ -1,109 +0,0 @@
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
authDisabled bool
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")
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
// when the server has been started with the --no-auth flag
ErrAuthDisabled = portainer.Error("Authentication is disabled")
)
// NewAuthHandler returns a new instance of AuthHandler.
func NewAuthHandler(mw *middleWareService) *AuthHandler {
h := &AuthHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/auth",
mw.public(http.HandlerFunc(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
}
if handler.authDisabled {
Error(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
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.UserByUsername(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{
ID: u.ID,
Username: u.Username,
Role: u.Role,
}
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"`
}
-114
View File
@@ -1,114 +0,0 @@
package http
import (
"strconv"
"github.com/portainer/portainer"
"log"
"net/http"
"net/url"
"os"
"github.com/gorilla/mux"
"github.com/orcaman/concurrent-map"
)
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
type DockerHandler struct {
*mux.Router
Logger *log.Logger
EndpointService portainer.EndpointService
ProxyFactory ProxyFactory
proxies cmap.ConcurrentMap
}
// NewDockerHandler returns a new instance of DockerHandler.
func NewDockerHandler(mw *middleWareService, resourceControlService portainer.ResourceControlService) *DockerHandler {
h := &DockerHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
ProxyFactory: ProxyFactory{
ResourceControlService: resourceControlService,
},
proxies: cmap.New(),
}
h.PathPrefix("/{id}/").Handler(
mw.authenticated(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
return h
}
func checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
for _, authorizedUserID := range endpoint.AuthorizedUsers {
if authorizedUserID == userID {
return true
}
}
return false
}
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
parsedID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
endpointID := portainer.EndpointID(parsedID)
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData.Role != portainer.AdministratorRole && !checkEndpointAccessControl(endpoint, tokenData.ID) {
Error(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
return
}
var proxy http.Handler
item, ok := handler.proxies.Get(string(endpointID))
if !ok {
proxy, err = handler.createAndRegisterEndpointProxy(endpoint)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
} else {
proxy = item.(http.Handler)
}
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
}
func (handler *DockerHandler) createAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
var proxy http.Handler
endpointURL, err := url.Parse(endpoint.URL)
if err != nil {
return nil, err
}
if endpointURL.Scheme == "tcp" {
if endpoint.TLS {
proxy, err = handler.ProxyFactory.newHTTPSProxy(endpointURL, endpoint)
if err != nil {
return nil, err
}
} else {
proxy = handler.ProxyFactory.newHTTPProxy(endpointURL)
}
} else {
// Assume unix:// scheme
proxy = handler.ProxyFactory.newSocketProxy(endpointURL.Path)
}
handler.proxies.Set(string(endpoint.ID), proxy)
return proxy, nil
}
-121
View File
@@ -1,121 +0,0 @@
package http
import (
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"github.com/portainer/portainer"
)
// ProxyFactory is a factory to create reverse proxies to Docker endpoints
type ProxyFactory struct {
ResourceControlService portainer.ResourceControlService
}
// 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.
// It also adds an extra Transport to the proxy to allow Portainer to rewrite the responses.
func (factory *ProxyFactory) 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", "")
}
}
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
transport: &http.Transport{},
}
return &httputil.ReverseProxy{Director: director, Transport: transport}
}
func (factory *ProxyFactory) newHTTPProxy(u *url.URL) http.Handler {
u.Scheme = "http"
return factory.newSingleHostReverseProxyWithHostHeader(u)
}
func (factory *ProxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
u.Scheme = "https"
proxy := factory.newSingleHostReverseProxyWithHostHeader(u)
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
if err != nil {
return nil, err
}
proxy.Transport.(*proxyTransport).transport.TLSClientConfig = config
return proxy, nil
}
func (factory *ProxyFactory) newSocketProxy(path string) http.Handler {
return &unixSocketHandler{path, &proxyTransport{
ResourceControlService: factory.ResourceControlService,
}}
}
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
type unixSocketHandler struct {
path string
transport *proxyTransport
}
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()
err = h.transport.proxyDockerRequests(r, res)
if err != nil {
Error(w, err, http.StatusInternalServerError, nil)
return
}
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)
}
}
-335
View File
@@ -1,335 +0,0 @@
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
authorizeEndpointManagement bool
EndpointService portainer.EndpointService
FileService portainer.FileService
}
const (
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
// when the server has been started with the --external-endpoints flag
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
)
// NewEndpointHandler returns a new instance of EndpointHandler.
func NewEndpointHandler(mw *middleWareService) *EndpointHandler {
h := &EndpointHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/endpoints",
mw.administrator(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
h.Handle("/endpoints",
mw.authenticated(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
mw.administrator(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
mw.administrator(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}/access",
mw.administrator(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
h.Handle("/endpoints/{id}",
mw.administrator(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
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
}
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData == nil {
Error(w, portainer.ErrInvalidJWTToken, http.StatusBadRequest, handler.Logger)
return
}
var allowedEndpoints []portainer.Endpoint
if tokenData.Role != portainer.AdministratorRole {
allowedEndpoints = make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
for _, authorizedUserID := range endpoint.AuthorizedUsers {
if authorizedUserID == tokenData.ID {
allowedEndpoints = append(allowedEndpoints, endpoint)
break
}
}
}
} else {
allowedEndpoints = endpoints
}
encodeJSON(w, allowedEndpoints, handler.Logger)
}
// handlePostEndpoints handles POST requests on /endpoints
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
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,
AuthorizedUsers: []portainer.UserID{},
}
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
}
}
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
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
}
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)
}
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
func (handler *EndpointHandler) handlePutEndpointAccess(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 putEndpointAccessRequest
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, 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
}
authorizedUserIDs := []portainer.UserID{}
for _, value := range req.AuthorizedUsers {
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
}
endpoint.AuthorizedUsers = authorizedUserIDs
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putEndpointAccessRequest struct {
AuthorizedUsers []int `valid:"-"`
}
// handlePutEndpoint handles PUT requests on /endpoints/:id
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
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, 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
}
if req.Name != "" {
endpoint.Name = req.Name
}
if req.URL != "" {
endpoint.URL = req.URL
}
if req.TLS {
endpoint.TLS = true
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 {
endpoint.TLS = false
endpoint.TLSCACertPath = ""
endpoint.TLSCertPath = ""
endpoint.TLSKeyPath = ""
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:"-"`
URL string `valid:"-"`
TLS bool `valid:"-"`
}
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
if !handler.authorizeEndpointManagement {
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
return
}
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.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
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
View File
@@ -1,36 +0,0 @@
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)
}
-88
View File
@@ -1,88 +0,0 @@
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")
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
// ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse
ErrEmptyResponseBody = portainer.Error("Empty response body")
)
// 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)
}
}
-119
View File
@@ -1,119 +0,0 @@
package http
import (
"context"
"github.com/portainer/portainer"
"net/http"
"strings"
)
type (
// middleWareService represents a service to manage HTTP middlewares
middleWareService struct {
jwtService portainer.JWTService
authDisabled bool
}
contextKey int
)
const (
contextAuthenticationKey contextKey = iota
)
func extractTokenDataFromRequestContext(request *http.Request) (*portainer.TokenData, error) {
contextData := request.Context().Value(contextAuthenticationKey)
if contextData == nil {
return nil, portainer.ErrMissingContextData
}
tokenData := contextData.(*portainer.TokenData)
return tokenData, nil
}
// public defines a chain of middleware for public endpoints (no authentication required)
func (service *middleWareService) public(h http.Handler) http.Handler {
h = mwSecureHeaders(h)
return h
}
// authenticated defines a chain of middleware for private endpoints (authentication required)
func (service *middleWareService) authenticated(h http.Handler) http.Handler {
h = service.mwCheckAuthentication(h)
h = mwSecureHeaders(h)
return h
}
// administrator defines a chain of middleware for private administrator restricted endpoints
// (authentication and role admin required)
func (service *middleWareService) administrator(h http.Handler) http.Handler {
h = mwCheckAdministratorRole(h)
h = service.mwCheckAuthentication(h)
h = mwSecureHeaders(h)
return h
}
// mwSecureHeaders provides secure headers middleware for handlers
func mwSecureHeaders(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)
})
}
// mwCheckAdministratorRole check the role of the user associated to the request
func mwCheckAdministratorRole(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
return
}
if tokenData.Role != portainer.AdministratorRole {
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
return
}
next.ServeHTTP(w, r)
})
}
// mwCheckAuthentication provides Authentication middleware for handlers
func (service *middleWareService) mwCheckAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var tokenData *portainer.TokenData
if !service.authDisabled {
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
}
var err error
tokenData, err = service.jwtService.ParseAndVerifyToken(token)
if err != nil {
Error(w, err, http.StatusUnauthorized, nil)
return
}
} else {
tokenData = &portainer.TokenData{
Role: portainer.AdministratorRole,
}
}
ctx := context.WithValue(r.Context(), contextAuthenticationKey, tokenData)
next.ServeHTTP(w, r.WithContext(ctx))
return
})
}
-664
View File
@@ -1,664 +0,0 @@
package http
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"path"
"strconv"
"strings"
"github.com/portainer/portainer"
)
type (
proxyTransport struct {
transport *http.Transport
ResourceControlService portainer.ResourceControlService
}
resourceControlMetadata struct {
OwnerID portainer.UserID `json:"OwnerId"`
}
)
func (p *proxyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
response, err := p.transport.RoundTrip(req)
if err != nil {
return response, err
}
err = p.proxyDockerRequests(req, response)
return response, err
}
func (p *proxyTransport) proxyDockerRequests(request *http.Request, response *http.Response) error {
path := request.URL.Path
if strings.HasPrefix(path, "/containers") {
return p.handleContainerRequests(request, response)
} else if strings.HasPrefix(path, "/services") {
return p.handleServiceRequests(request, response)
} else if strings.HasPrefix(path, "/volumes") {
return p.handleVolumeRequests(request, response)
}
return nil
}
func (p *proxyTransport) handleContainerRequests(request *http.Request, response *http.Response) error {
requestPath := request.URL.Path
tokenData, err := extractTokenDataFromRequestContext(request)
if err != nil {
return err
}
if requestPath == "/containers/prune" && tokenData.Role != portainer.AdministratorRole {
return writeAccessDeniedResponse(response)
}
if requestPath == "/containers/json" {
if tokenData.Role == portainer.AdministratorRole {
return p.decorateContainerResponse(response)
}
return p.proxyContainerResponseWithResourceControl(response, tokenData.ID)
}
// /containers/{id}/action
if match, _ := path.Match("/containers/*/*", requestPath); match {
if tokenData.Role != portainer.AdministratorRole {
resourceID := path.Base(path.Dir(requestPath))
return p.proxyContainerResponseWithAccessControl(response, tokenData.ID, resourceID)
}
}
return nil
}
func (p *proxyTransport) handleServiceRequests(request *http.Request, response *http.Response) error {
requestPath := request.URL.Path
tokenData, err := extractTokenDataFromRequestContext(request)
if err != nil {
return err
}
if requestPath == "/services" {
if tokenData.Role == portainer.AdministratorRole {
return p.decorateServiceResponse(response)
}
return p.proxyServiceResponseWithResourceControl(response, tokenData.ID)
}
// /services/{id}
if match, _ := path.Match("/services/*", requestPath); match {
if tokenData.Role != portainer.AdministratorRole {
resourceID := path.Base(requestPath)
return p.proxyServiceResponseWithAccessControl(response, tokenData.ID, resourceID)
}
}
// /services/{id}/action
if match, _ := path.Match("/services/*/*", requestPath); match {
if tokenData.Role != portainer.AdministratorRole {
resourceID := path.Base(path.Dir(requestPath))
return p.proxyServiceResponseWithAccessControl(response, tokenData.ID, resourceID)
}
}
return nil
}
func (p *proxyTransport) handleVolumeRequests(request *http.Request, response *http.Response) error {
requestPath := request.URL.Path
tokenData, err := extractTokenDataFromRequestContext(request)
if err != nil {
return err
}
if requestPath == "/volumes" {
if tokenData.Role == portainer.AdministratorRole {
return p.decorateVolumeResponse(response)
}
return p.proxyVolumeResponseWithResourceControl(response, tokenData.ID)
}
if requestPath == "/volumes/prune" && tokenData.Role != portainer.AdministratorRole {
return writeAccessDeniedResponse(response)
}
// /volumes/{name}
if match, _ := path.Match("/volumes/*", requestPath); match {
if tokenData.Role != portainer.AdministratorRole {
resourceID := path.Base(requestPath)
return p.proxyVolumeResponseWithAccessControl(response, tokenData.ID, resourceID)
}
}
return nil
}
func (p *proxyTransport) proxyContainerResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
rcs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
if err != nil {
return err
}
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
if err != nil {
return err
}
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
return writeAccessDeniedResponse(response)
}
return nil
}
func (p *proxyTransport) proxyServiceResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
if err != nil {
return err
}
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
if err != nil {
return err
}
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
return writeAccessDeniedResponse(response)
}
return nil
}
func (p *proxyTransport) proxyVolumeResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
if err != nil {
return err
}
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
if err != nil {
return err
}
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
return writeAccessDeniedResponse(response)
}
return nil
}
func (p *proxyTransport) decorateContainerResponse(response *http.Response) error {
responseData, err := getResponseData(response)
if err != nil {
return err
}
containers, err := p.decorateContainers(responseData)
if err != nil {
return err
}
err = rewriteContainerResponse(response, containers)
if err != nil {
return err
}
return nil
}
func (p *proxyTransport) proxyContainerResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
responseData, err := getResponseData(response)
if err != nil {
return err
}
containers, err := p.filterContainers(userID, responseData)
if err != nil {
return err
}
err = rewriteContainerResponse(response, containers)
if err != nil {
return err
}
return nil
}
func (p *proxyTransport) decorateServiceResponse(response *http.Response) error {
responseData, err := getResponseData(response)
if err != nil {
return err
}
services, err := p.decorateServices(responseData)
if err != nil {
return err
}
err = rewriteServiceResponse(response, services)
if err != nil {
return err
}
return nil
}
func (p *proxyTransport) proxyServiceResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
responseData, err := getResponseData(response)
if err != nil {
return err
}
volumes, err := p.filterServices(userID, responseData)
if err != nil {
return err
}
err = rewriteServiceResponse(response, volumes)
if err != nil {
return err
}
return nil
}
func (p *proxyTransport) decorateVolumeResponse(response *http.Response) error {
responseData, err := getResponseData(response)
if err != nil {
return err
}
volumes, err := p.decorateVolumes(responseData)
if err != nil {
return err
}
err = rewriteVolumeResponse(response, volumes)
if err != nil {
return err
}
return nil
}
func (p *proxyTransport) proxyVolumeResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
responseData, err := getResponseData(response)
if err != nil {
return err
}
volumes, err := p.filterVolumes(userID, responseData)
if err != nil {
return err
}
err = rewriteVolumeResponse(response, volumes)
if err != nil {
return err
}
return nil
}
func (p *proxyTransport) decorateContainers(responseData interface{}) ([]interface{}, error) {
responseDataArray := responseData.([]interface{})
containerRCs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
if err != nil {
return nil, err
}
serviceRCs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
if err != nil {
return nil, err
}
decoratedResources := make([]interface{}, 0)
for _, container := range responseDataArray {
jsonObject := container.(map[string]interface{})
containerID := jsonObject["Id"].(string)
containerRC := getRCByResourceID(containerID, containerRCs)
if containerRC != nil {
decoratedObject := decorateWithResourceControlMetadata(jsonObject, containerRC.OwnerID)
decoratedResources = append(decoratedResources, decoratedObject)
continue
}
containerLabels := jsonObject["Labels"]
if containerLabels != nil {
jsonLabels := containerLabels.(map[string]interface{})
serviceID := jsonLabels["com.docker.swarm.service.id"]
if serviceID != nil {
serviceRC := getRCByResourceID(serviceID.(string), serviceRCs)
if serviceRC != nil {
decoratedObject := decorateWithResourceControlMetadata(jsonObject, serviceRC.OwnerID)
decoratedResources = append(decoratedResources, decoratedObject)
continue
}
}
}
decoratedResources = append(decoratedResources, container)
}
return decoratedResources, nil
}
func (p *proxyTransport) filterContainers(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
responseDataArray := responseData.([]interface{})
containerRCs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
if err != nil {
return nil, err
}
serviceRCs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
if err != nil {
return nil, err
}
userOwnedContainerIDs, err := getResourceIDsOwnedByUser(userID, containerRCs)
if err != nil {
return nil, err
}
userOwnedServiceIDs, err := getResourceIDsOwnedByUser(userID, serviceRCs)
if err != nil {
return nil, err
}
publicContainers := getPublicContainers(responseDataArray, containerRCs, serviceRCs)
filteredResources := make([]interface{}, 0)
for _, container := range responseDataArray {
jsonObject := container.(map[string]interface{})
containerID := jsonObject["Id"].(string)
if isStringInArray(containerID, userOwnedContainerIDs) {
decoratedObject := decorateWithResourceControlMetadata(jsonObject, userID)
filteredResources = append(filteredResources, decoratedObject)
continue
}
containerLabels := jsonObject["Labels"]
if containerLabels != nil {
jsonLabels := containerLabels.(map[string]interface{})
serviceID := jsonLabels["com.docker.swarm.service.id"]
if serviceID != nil && isStringInArray(serviceID.(string), userOwnedServiceIDs) {
decoratedObject := decorateWithResourceControlMetadata(jsonObject, userID)
filteredResources = append(filteredResources, decoratedObject)
}
}
}
filteredResources = append(filteredResources, publicContainers...)
return filteredResources, nil
}
func decorateWithResourceControlMetadata(object map[string]interface{}, userID portainer.UserID) map[string]interface{} {
metadata := make(map[string]interface{})
metadata["ResourceControl"] = resourceControlMetadata{
OwnerID: userID,
}
object["Portainer"] = metadata
return object
}
func (p *proxyTransport) decorateServices(responseData interface{}) ([]interface{}, error) {
responseDataArray := responseData.([]interface{})
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
if err != nil {
return nil, err
}
decoratedResources := make([]interface{}, 0)
for _, service := range responseDataArray {
jsonResource := service.(map[string]interface{})
resourceID := jsonResource["ID"].(string)
serviceRC := getRCByResourceID(resourceID, rcs)
if serviceRC != nil {
decoratedObject := decorateWithResourceControlMetadata(jsonResource, serviceRC.OwnerID)
decoratedResources = append(decoratedResources, decoratedObject)
continue
}
decoratedResources = append(decoratedResources, service)
}
return decoratedResources, nil
}
func (p *proxyTransport) filterServices(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
responseDataArray := responseData.([]interface{})
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
if err != nil {
return nil, err
}
userOwnedServiceIDs, err := getResourceIDsOwnedByUser(userID, rcs)
if err != nil {
return nil, err
}
publicServices := getPublicResources(responseDataArray, rcs, "ID")
filteredResources := make([]interface{}, 0)
for _, res := range responseDataArray {
jsonResource := res.(map[string]interface{})
resourceID := jsonResource["ID"].(string)
if isStringInArray(resourceID, userOwnedServiceIDs) {
decoratedObject := decorateWithResourceControlMetadata(jsonResource, userID)
filteredResources = append(filteredResources, decoratedObject)
}
}
filteredResources = append(filteredResources, publicServices...)
return filteredResources, nil
}
func (p *proxyTransport) decorateVolumes(responseData interface{}) ([]interface{}, error) {
var responseDataArray []interface{}
jsonObject := responseData.(map[string]interface{})
if jsonObject["Volumes"] != nil {
responseDataArray = jsonObject["Volumes"].([]interface{})
}
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
if err != nil {
return nil, err
}
decoratedResources := make([]interface{}, 0)
for _, volume := range responseDataArray {
jsonResource := volume.(map[string]interface{})
resourceID := jsonResource["Name"].(string)
volumeRC := getRCByResourceID(resourceID, rcs)
if volumeRC != nil {
decoratedObject := decorateWithResourceControlMetadata(jsonResource, volumeRC.OwnerID)
decoratedResources = append(decoratedResources, decoratedObject)
continue
}
decoratedResources = append(decoratedResources, volume)
}
return decoratedResources, nil
}
func (p *proxyTransport) filterVolumes(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
var responseDataArray []interface{}
jsonObject := responseData.(map[string]interface{})
if jsonObject["Volumes"] != nil {
responseDataArray = jsonObject["Volumes"].([]interface{})
}
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
if err != nil {
return nil, err
}
userOwnedVolumeIDs, err := getResourceIDsOwnedByUser(userID, rcs)
if err != nil {
return nil, err
}
publicVolumes := getPublicResources(responseDataArray, rcs, "Name")
filteredResources := make([]interface{}, 0)
for _, res := range responseDataArray {
jsonResource := res.(map[string]interface{})
resourceID := jsonResource["Name"].(string)
if isStringInArray(resourceID, userOwnedVolumeIDs) {
decoratedObject := decorateWithResourceControlMetadata(jsonResource, userID)
filteredResources = append(filteredResources, decoratedObject)
}
}
filteredResources = append(filteredResources, publicVolumes...)
return filteredResources, nil
}
func getResourceIDsOwnedByUser(userID portainer.UserID, rcs []portainer.ResourceControl) ([]string, error) {
ownedResources := make([]string, 0)
for _, rc := range rcs {
if rc.OwnerID == userID {
ownedResources = append(ownedResources, rc.ResourceID)
}
}
return ownedResources, nil
}
func getOwnedServiceContainers(responseData []interface{}, serviceRCs []portainer.ResourceControl) []interface{} {
ownedContainers := make([]interface{}, 0)
for _, res := range responseData {
jsonResource := res.(map[string]map[string]interface{})
swarmServiceID := jsonResource["Labels"]["com.docker.swarm.service.id"]
if swarmServiceID != nil {
resourceID := swarmServiceID.(string)
if isResourceIDInRCs(resourceID, serviceRCs) {
ownedContainers = append(ownedContainers, res)
}
}
}
return ownedContainers
}
func getPublicContainers(responseData []interface{}, containerRCs []portainer.ResourceControl, serviceRCs []portainer.ResourceControl) []interface{} {
publicContainers := make([]interface{}, 0)
for _, container := range responseData {
jsonObject := container.(map[string]interface{})
containerID := jsonObject["Id"].(string)
if !isResourceIDInRCs(containerID, containerRCs) {
containerLabels := jsonObject["Labels"]
if containerLabels != nil {
jsonLabels := containerLabels.(map[string]interface{})
serviceID := jsonLabels["com.docker.swarm.service.id"]
if serviceID == nil {
publicContainers = append(publicContainers, container)
} else if serviceID != nil && !isResourceIDInRCs(serviceID.(string), serviceRCs) {
publicContainers = append(publicContainers, container)
}
} else {
publicContainers = append(publicContainers, container)
}
}
}
return publicContainers
}
func getPublicResources(responseData []interface{}, rcs []portainer.ResourceControl, resourceIDKey string) []interface{} {
publicResources := make([]interface{}, 0)
for _, res := range responseData {
jsonResource := res.(map[string]interface{})
resourceID := jsonResource[resourceIDKey].(string)
if !isResourceIDInRCs(resourceID, rcs) {
publicResources = append(publicResources, res)
}
}
return publicResources
}
func isStringInArray(target string, array []string) bool {
for _, element := range array {
if element == target {
return true
}
}
return false
}
func isResourceIDInRCs(resourceID string, rcs []portainer.ResourceControl) bool {
for _, rc := range rcs {
if resourceID == rc.ResourceID {
return true
}
}
return false
}
func getRCByResourceID(resourceID string, rcs []portainer.ResourceControl) *portainer.ResourceControl {
for _, rc := range rcs {
if resourceID == rc.ResourceID {
return &rc
}
}
return nil
}
func getResponseData(response *http.Response) (interface{}, error) {
var data interface{}
if response.Body != nil {
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
err = response.Body.Close()
if err != nil {
return nil, err
}
err = json.Unmarshal(body, &data)
if err != nil {
return nil, err
}
return data, nil
}
return nil, ErrEmptyResponseBody
}
func writeAccessDeniedResponse(response *http.Response) error {
return rewriteResponse(response, portainer.ErrResourceAccessDenied, 403)
}
func rewriteContainerResponse(response *http.Response, responseData interface{}) error {
return rewriteResponse(response, responseData, 200)
}
func rewriteServiceResponse(response *http.Response, responseData interface{}) error {
return rewriteResponse(response, responseData, 200)
}
func rewriteVolumeResponse(response *http.Response, responseData interface{}) error {
data := map[string]interface{}{}
data["Volumes"] = responseData
return rewriteResponse(response, data, 200)
}
func rewriteResponse(response *http.Response, newContent interface{}, statusCode int) error {
jsonData, err := json.Marshal(newContent)
if err != nil {
return err
}
body := ioutil.NopCloser(bytes.NewReader(jsonData))
response.StatusCode = statusCode
response.Body = body
response.ContentLength = int64(len(jsonData))
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
return nil
}
-71
View File
@@ -1,71 +0,0 @@
package http
import (
"github.com/portainer/portainer"
"net/http"
)
// Server implements the portainer.Server interface
type Server struct {
BindAddress string
AssetsPath string
AuthDisabled bool
EndpointManagement bool
UserService portainer.UserService
EndpointService portainer.EndpointService
ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
FileService portainer.FileService
Settings *portainer.Settings
TemplatesURL string
Handler *Handler
}
// Start starts the HTTP server
func (server *Server) Start() error {
middleWareService := &middleWareService{
jwtService: server.JWTService,
authDisabled: server.AuthDisabled,
}
var authHandler = NewAuthHandler(middleWareService)
authHandler.UserService = server.UserService
authHandler.CryptoService = server.CryptoService
authHandler.JWTService = server.JWTService
authHandler.authDisabled = server.AuthDisabled
var userHandler = NewUserHandler(middleWareService)
userHandler.UserService = server.UserService
userHandler.CryptoService = server.CryptoService
userHandler.ResourceControlService = server.ResourceControlService
var settingsHandler = NewSettingsHandler(middleWareService)
settingsHandler.settings = server.Settings
var templatesHandler = NewTemplatesHandler(middleWareService)
templatesHandler.templatesURL = server.TemplatesURL
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
dockerHandler.EndpointService = server.EndpointService
var websocketHandler = NewWebSocketHandler()
websocketHandler.EndpointService = server.EndpointService
var endpointHandler = NewEndpointHandler(middleWareService)
endpointHandler.authorizeEndpointManagement = server.EndpointManagement
endpointHandler.EndpointService = server.EndpointService
endpointHandler.FileService = server.FileService
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,
}
return http.ListenAndServe(server.BindAddress, server.Handler)
}
-40
View File
@@ -1,40 +0,0 @@
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
settings *portainer.Settings
}
// NewSettingsHandler returns a new instance of SettingsHandler.
func NewSettingsHandler(mw *middleWareService) *SettingsHandler {
h := &SettingsHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/settings",
mw.public(http.HandlerFunc(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)
}
-50
View File
@@ -1,50 +0,0 @@
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
templatesURL string
}
// NewTemplatesHandler returns a new instance of TemplatesHandler.
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
h := &TemplatesHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/templates",
mw.authenticated(http.HandlerFunc(h.handleGetTemplates)))
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)
}
-71
View File
@@ -1,71 +0,0 @@
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
}
// NewUploadHandler returns a new instance of UploadHandler.
func NewUploadHandler(mw *middleWareService) *UploadHandler {
h := &UploadHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}",
mw.authenticated(http.HandlerFunc(h.handlePostUploadTLS)))
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)
}
}
-480
View File
@@ -1,480 +0,0 @@
package http
import (
"strconv"
"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
ResourceControlService portainer.ResourceControlService
CryptoService portainer.CryptoService
}
// NewUserHandler returns a new instance of UserHandler.
func NewUserHandler(mw *middleWareService) *UserHandler {
h := &UserHandler{
Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/users",
mw.administrator(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
h.Handle("/users",
mw.administrator(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
h.Handle("/users/{id}",
mw.administrator(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
h.Handle("/users/{id}",
mw.authenticated(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
h.Handle("/users/{id}",
mw.administrator(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
h.Handle("/users/{id}/passwd",
mw.authenticated(http.HandlerFunc(h.handlePostUserPasswd)))
h.Handle("/users/{userId}/resources/{resourceType}",
mw.authenticated(http.HandlerFunc(h.handlePostUserResource))).Methods(http.MethodPost)
h.Handle("/users/{userId}/resources/{resourceType}/{resourceId}",
mw.authenticated(http.HandlerFunc(h.handleDeleteUserResource))).Methods(http.MethodDelete)
h.Handle("/users/admin/check",
mw.public(http.HandlerFunc(h.handleGetAdminCheck)))
h.Handle("/users/admin/init",
mw.public(http.HandlerFunc(h.handlePostAdminInit)))
return h
}
// handlePostUsers handles POST requests on /users
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
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
}
var role portainer.UserRole
if req.Role == 1 {
role = portainer.AdministratorRole
} else {
role = portainer.StandardUserRole
}
user, err := handler.UserService.UserByUsername(req.Username)
if err != nil && err != portainer.ErrUserNotFound {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if user != nil {
Error(w, portainer.ErrUserAlreadyExists, http.StatusConflict, handler.Logger)
return
}
user = &portainer.User{
Username: req.Username,
Role: role,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.CreateUser(user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type postUsersRequest struct {
Username string `valid:"alphanum,required"`
Password string `valid:"required"`
Role int `valid:"required"`
}
// handleGetUsers handles GET requests on /users
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
users, err := handler.UserService.Users()
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
for i := range users {
users[i].Password = ""
}
encodeJSON(w, users, handler.Logger)
}
// handlePostUserPasswd handles POST requests on /users/:id/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)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
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(portainer.UserID(userID))
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/:id
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
user, err := handler.UserService.User(portainer.UserID(userID))
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/:id
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
return
}
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
}
if req.Password == "" && req.Role == 0 {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
user, err := handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound {
Error(w, err, http.StatusNotFound, handler.Logger)
return
} else if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if req.Password != "" {
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
}
if req.Role != 0 {
if tokenData.Role != portainer.AdministratorRole {
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
return
}
if req.Role == 1 {
user.Role = portainer.AdministratorRole
} else {
user.Role = portainer.StandardUserRole
}
}
err = handler.UserService.UpdateUser(user.ID, user)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
type putUserRequest struct {
Password string `valid:"-"`
Role int `valid:"-"`
}
// 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
}
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if len(users) == 0 {
Error(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
return
}
}
// 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.UserByUsername("admin")
if err == portainer.ErrUserNotFound {
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
}
user.Password, err = handler.CryptoService.Hash(req.Password)
if err != nil {
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
return
}
err = handler.UserService.CreateUser(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"`
}
// handleDeleteUser handles DELETE requests on /users/:id
func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
userID, err := strconv.Atoi(id)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
_, err = handler.UserService.User(portainer.UserID(userID))
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.UserService.DeleteUser(portainer.UserID(userID))
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
// handlePostUserResource handles POST requests on /users/:userId/resources/:resourceType
func (handler *UserHandler) handlePostUserResource(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := vars["userId"]
resourceType := vars["resourceType"]
uid, err := strconv.Atoi(userID)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
var rcType portainer.ResourceControlType
if resourceType == "container" {
rcType = portainer.ContainerResourceControl
} else if resourceType == "service" {
rcType = portainer.ServiceResourceControl
} else if resourceType == "volume" {
rcType = portainer.VolumeResourceControl
} else {
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
return
}
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData.ID != portainer.UserID(uid) {
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
var req postUserResourceRequest
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
}
resource := portainer.ResourceControl{
OwnerID: portainer.UserID(uid),
ResourceID: req.ResourceID,
AccessLevel: portainer.RestrictedResourceAccessLevel,
}
err = handler.ResourceControlService.CreateResourceControl(req.ResourceID, &resource, rcType)
if err != nil {
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
return
}
}
type postUserResourceRequest struct {
ResourceID string `valid:"required"`
}
// handleDeleteUserResource handles DELETE requests on /users/:userId/resources/:resourceType/:resourceId
func (handler *UserHandler) handleDeleteUserResource(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := vars["userId"]
resourceID := vars["resourceId"]
resourceType := vars["resourceType"]
uid, err := strconv.Atoi(userID)
if err != nil {
Error(w, err, http.StatusBadRequest, handler.Logger)
return
}
var rcType portainer.ResourceControlType
if resourceType == "container" {
rcType = portainer.ContainerResourceControl
} else if resourceType == "service" {
rcType = portainer.ServiceResourceControl
} else if resourceType == "volume" {
rcType = portainer.VolumeResourceControl
} else {
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
return
}
tokenData, err := extractTokenDataFromRequestContext(r)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
}
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(uid) {
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
return
}
err = handler.ResourceControlService.DeleteResourceControl(resourceID, rcType)
if err != nil {
Error(w, err, http.StatusInternalServerError, handler.Logger)
return
}
}
-79
View File
@@ -1,79 +0,0 @@
package jwt
import (
"github.com/portainer/portainer"
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gorilla/securecookie"
)
// Service represents a service for managing JWT tokens.
type Service struct {
secret []byte
}
type claims struct {
UserID int `json:"id"`
Username string `json:"username"`
Role int `json:"role"`
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{
int(data.ID),
data.Username,
int(data.Role),
jwt.StandardClaims{
ExpiresAt: expireToken,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, cl)
signedToken, err := token.SignedString(service.secret)
if err != nil {
return "", err
}
return signedToken, nil
}
// ParseAndVerifyToken parses a JWT token and verify its validity. It returns an error if token is invalid.
func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, error) {
parsedToken, err := jwt.ParseWithClaims(token, &claims{}, 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 {
if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid {
tokenData := &portainer.TokenData{
ID: portainer.UserID(cl.UserID),
Username: cl.Username,
Role: portainer.UserRole(cl.Role),
}
return tokenData, nil
}
}
return nil, portainer.ErrInvalidJWTToken
}
+47
View File
@@ -0,0 +1,47 @@
package main // import "github.com/cloudinovasi/ui-for-docker"
import (
"gopkg.in/alecthomas/kingpin.v2"
)
// main is the entry point of the program
func main() {
kingpin.Version("1.7.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 UI For Docker").Default(":9000").Short('p').String()
assets = kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String()
data = kingpin.Flag("data", "Path to the data").Default(".").Short('d').String()
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'))
registries = pairs(kingpin.Flag("registries", "Supported Docker registries").Short('r'))
logo = kingpin.Flag("logo", "URL for the logo displayed in the UI").String()
)
kingpin.Parse()
apiConfig := apiConfig{
Endpoint: *endpoint,
BindAddress: *addr,
AssetPath: *assets,
DataPath: *data,
SwarmSupport: *swarm,
TLSEnabled: *tlsverify,
TLSCACertPath: *tlscacert,
TLSCertPath: *tlscert,
TLSKeyPath: *tlskey,
}
settings := &Settings{
Swarm: *swarm,
HiddenLabels: *labels,
Registries: *registries,
Logo: *logo,
}
api := newAPI(apiConfig)
api.run(settings)
}
-215
View File
@@ -1,215 +0,0 @@
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
ExternalEndpoints *string
SyncInterval *string
Endpoint *string
Labels *[]Pair
Logo *string
Templates *string
NoAuth *bool
NoAnalytics *bool
TLSVerify *bool
TLSCacert *string
TLSCert *string
TLSKey *string
}
// Settings represents Portainer settings.
Settings struct {
HiddenLabels []Pair `json:"hiddenLabels"`
Logo string `json:"logo"`
Authentication bool `json:"authentication"`
Analytics bool `json:"analytics"`
EndpointManagement bool `json:"endpointManagement"`
}
// User represent a user account.
User struct {
ID UserID `json:"Id"`
Username string `json:"Username"`
Password string `json:"Password,omitempty"`
Role UserRole `json:"Role"`
}
// UserID represents a user identifier
UserID int
// UserRole represents the role of a user. It can be either an administrator
// or a regular user.
UserRole int
// TokenData represents the data embedded in a JWT token.
TokenData struct {
ID UserID
Username string
Role UserRole
}
// 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"`
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
}
// ResourceControl represent a reference to a Docker resource with specific controls
ResourceControl struct {
OwnerID UserID `json:"OwnerId"`
ResourceID string `json:"ResourceId"`
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
}
// ResourceControlType represents a type of resource control.
// Can be one of: container, service or volume.
ResourceControlType int
// ResourceAccessLevel represents the level of control associated to a resource for a specific owner.
// Can be one of: full, restricted, limited.
ResourceAccessLevel int
// 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
MigrateData() error
}
// Server defines the interface to serve the API.
Server interface {
Start() error
}
// UserService represents a service for managing user data.
UserService interface {
User(ID UserID) (*User, error)
UserByUsername(username string) (*User, error)
Users() ([]User, error)
UsersByRole(role UserRole) ([]User, error)
CreateUser(user *User) error
UpdateUser(ID UserID, user *User) error
DeleteUser(ID UserID) error
}
// EndpointService represents a service for managing endpoint data.
EndpointService interface {
Endpoint(ID EndpointID) (*Endpoint, error)
Endpoints() ([]Endpoint, error)
CreateEndpoint(endpoint *Endpoint) error
UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error
DeleteEndpoint(ID EndpointID) error
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
}
// VersionService represents a service for managing version data.
VersionService interface {
DBVersion() (int, error)
StoreDBVersion(version int) error
}
// ResourceControlService represents a service for managing resource control data.
ResourceControlService interface {
ResourceControl(resourceID string, rcType ResourceControlType) (*ResourceControl, error)
ResourceControls(rcType ResourceControlType) ([]ResourceControl, error)
CreateResourceControl(resourceID string, rc *ResourceControl, rcType ResourceControlType) error
DeleteResourceControl(resourceID string, rcType ResourceControlType) 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)
ParseAndVerifyToken(token string) (*TokenData, 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
}
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
EndpointWatcher interface {
WatchEndpointFile(endpointFilePath string) error
}
)
const (
// APIVersion is the version number of Portainer API.
APIVersion = "1.12.2"
// DBVersion is the version number of Portainer database.
DBVersion = 1
)
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
)
const (
_ UserRole = iota
// AdministratorRole represents an administrator user role
AdministratorRole
// StandardUserRole represents a regular user role
StandardUserRole
)
const (
_ ResourceControlType = iota
// ContainerResourceControl represents a resource control for a container
ContainerResourceControl
// ServiceResourceControl represents a resource control for a service
ServiceResourceControl
// VolumeResourceControl represents a resource control for a volume
VolumeResourceControl
)
const (
_ ResourceAccessLevel = iota
// RestrictedResourceAccessLevel represents a restricted access level on a resource (private ownership)
RestrictedResourceAccessLevel
)
+19
View File
@@ -0,0 +1,19 @@
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"`
Registries pairList `json:"registries"`
Logo string `json:"logo"`
}
// configurationHandler 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)
}
+8 -7
View File
@@ -1,26 +1,27 @@
package http
package main
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"log"
)
// createTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func createTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
// newTLSConfig initializes a tls.Config using a CA certificate, a certificate and a key
func newTLSConfig(caCertPath, certPath, keyPath string) *tls.Config {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
log.Fatal(err)
}
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, err
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config := &tls.Config{
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
}
return config, nil
return tlsConfig
}
+47
View File
@@ -0,0 +1,47 @@
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)
}
}
}
+156 -575
View File
@@ -1,576 +1,157 @@
angular.module('portainer.filters', []);
angular.module('portainer.rest', ['ngResource']);
angular.module('portainer.services', []);
angular.module('portainer.helpers', []);
angular.module('portainer', [
'ui.bootstrap',
'ui.router',
'ui.select',
'ngCookies',
'ngSanitize',
'ngFileUpload',
'angularUtils.directives.dirPagination',
'LocalStorageModule',
'angular-jwt',
'angular-google-analytics',
'portainer.templates',
'portainer.filters',
'portainer.rest',
'portainer.helpers',
'portainer.services',
'auth',
'dashboard',
'container',
'containerConsole',
'containerLogs',
'containers',
'createContainer',
'createNetwork',
'createService',
'createVolume',
'docker',
'endpoint',
'endpointAccess',
'endpointInit',
'endpoints',
'events',
'image',
'images',
'main',
'network',
'networks',
'node',
'service',
'services',
'settings',
'sidebar',
'stats',
'swarm',
'task',
'templates',
'user',
'users',
'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider) {
'use strict';
localStorageServiceProvider
.setStorageType('sessionStorage')
.setPrefix('portainer');
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');
AnalyticsProvider.setAccount('@@CONFIG_GA_ID');
AnalyticsProvider.startOffline(true);
$urlRouterProvider.otherwise('/auth');
$uibTooltipProvider.setTriggers({
'mouseenter': 'mouseleave',
'click': 'click',
'focus': 'blur',
'outsideClick': 'outsideClick'
});
angular.module('uifordocker', [
'uifordocker.templates',
'ui.bootstrap',
'ui.router',
'ui.select',
'ngCookies',
'ngSanitize',
'uifordocker.services',
'uifordocker.filters',
'dashboard',
'container',
'containerConsole',
'containerLogs',
'containers',
'createContainer',
'docker',
'events',
'images',
'image',
'stats',
'swarm',
'network',
'networks',
'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $httpProvider) {
'use strict';
$httpProvider.defaults.xsrfCookieName = 'csrfToken';
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
$urlRouterProvider.otherwise('/');
$stateProvider
.state('root', {
abstract: true,
resolve: {
requiresLogin: ['StateManager', function (StateManager) {
var applicationState = StateManager.getState();
return applicationState.application.authentication;
}]
}
})
.state('auth', {
parent: 'root',
url: '/auth',
params: {
logout: false,
error: ''
},
views: {
"content@": {
templateUrl: 'app/components/auth/auth.html',
controller: 'AuthenticationController'
}
},
data: {
requiresLogin: false
}
})
.state('containers', {
parent: 'root',
url: '/containers/',
views: {
"content@": {
templateUrl: 'app/components/containers/containers.html',
controller: 'ContainersController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('container', {
url: "^/containers/:id",
views: {
"content@": {
templateUrl: 'app/components/container/container.html',
controller: 'ContainerController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('stats', {
url: "^/containers/:id/stats",
views: {
"content@": {
templateUrl: 'app/components/stats/stats.html',
controller: 'StatsController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('logs', {
url: "^/containers/:id/logs",
views: {
"content@": {
templateUrl: 'app/components/containerLogs/containerlogs.html',
controller: 'ContainerLogsController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('console', {
url: "^/containers/:id/console",
views: {
"content@": {
templateUrl: 'app/components/containerConsole/containerConsole.html',
controller: 'ContainerConsoleController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('dashboard', {
parent: 'root',
url: '/dashboard',
views: {
"content@": {
templateUrl: 'app/components/dashboard/dashboard.html',
controller: 'DashboardController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('actions', {
abstract: true,
url: "/actions",
views: {
"content@": {
template: '<div ui-view="content@"></div>'
},
"sidebar@": {
template: '<div ui-view="sidebar@"></div>'
}
}
})
.state('actions.create', {
abstract: true,
url: "/create",
views: {
"content@": {
template: '<div ui-view="content@"></div>'
},
"sidebar@": {
template: '<div ui-view="sidebar@"></div>'
}
}
})
.state('actions.create.container', {
url: "/container",
views: {
"content@": {
templateUrl: 'app/components/createContainer/createcontainer.html',
controller: 'CreateContainerController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('actions.create.network', {
url: "/network",
views: {
"content@": {
templateUrl: 'app/components/createNetwork/createnetwork.html',
controller: 'CreateNetworkController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.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'
}
}
})
.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'
}
}
})
.state('docker', {
url: '/docker/',
views: {
"content@": {
templateUrl: 'app/components/docker/docker.html',
controller: 'DockerController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('endpoints', {
url: '/endpoints/',
views: {
"content@": {
templateUrl: 'app/components/endpoints/endpoints.html',
controller: 'EndpointsController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('endpoint', {
url: '^/endpoints/:id',
views: {
"content@": {
templateUrl: 'app/components/endpoint/endpoint.html',
controller: 'EndpointController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('endpoint.access', {
url: '^/endpoints/:id/access',
views: {
"content@": {
templateUrl: 'app/components/endpointAccess/endpointAccess.html',
controller: 'EndpointAccessController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('endpointInit', {
url: '/init/endpoint',
views: {
"content@": {
templateUrl: 'app/components/endpointInit/endpointInit.html',
controller: 'EndpointInitController'
}
}
})
.state('events', {
url: '/events/',
views: {
"content@": {
templateUrl: 'app/components/events/events.html',
controller: 'EventsController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('images', {
url: '/images/',
views: {
"content@": {
templateUrl: 'app/components/images/images.html',
controller: 'ImagesController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('image', {
url: '^/images/:id/',
views: {
"content@": {
templateUrl: 'app/components/image/image.html',
controller: 'ImageController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('networks', {
url: '/networks/',
views: {
"content@": {
templateUrl: 'app/components/networks/networks.html',
controller: 'NetworksController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('network', {
url: '^/networks/:id/',
views: {
"content@": {
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('node', {
url: '^/nodes/:id/',
views: {
"content@": {
templateUrl: 'app/components/node/node.html',
controller: 'NodeController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('services', {
url: '/services/',
views: {
"content@": {
templateUrl: 'app/components/services/services.html',
controller: 'ServicesController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('service', {
url: '^/service/:id/',
views: {
"content@": {
templateUrl: 'app/components/service/service.html',
controller: 'ServiceController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('settings', {
url: '/settings/',
views: {
"content@": {
templateUrl: 'app/components/settings/settings.html',
controller: 'SettingsController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('task', {
url: '^/task/:id',
views: {
"content@": {
templateUrl: 'app/components/task/task.html',
controller: 'TaskController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('templates', {
url: '/templates/',
views: {
"content@": {
templateUrl: 'app/components/templates/templates.html',
controller: 'TemplatesController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('volumes', {
url: '/volumes/',
views: {
"content@": {
templateUrl: 'app/components/volumes/volumes.html',
controller: 'VolumesController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('users', {
url: '/users/',
views: {
"content@": {
templateUrl: 'app/components/users/users.html',
controller: 'UsersController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('user', {
url: '^/users/:id',
views: {
"content@": {
templateUrl: 'app/components/user/user.html',
controller: 'UserController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
.state('swarm', {
url: '/swarm/',
views: {
"content@": {
templateUrl: 'app/components/swarm/swarm.html',
controller: 'SwarmController'
},
"sidebar@": {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
});
// The Docker API likes to return plaintext errors, this catches them and disp
$httpProvider.interceptors.push(function() {
return {
'response': function(response) {
if (typeof(response.data) === 'string' &&
(_.startsWith(response.data, 'Conflict.') || _.startsWith(response.data, 'conflict:'))) {
$.gritter.add({
title: 'Error',
text: $('<div>').text(response.data).html(),
time: 10000
});
}
return response;
}
};
});
}])
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Messages', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Messages, Analytics) {
EndpointProvider.initialize();
StateManager.initialize().then(function success(state) {
if (state.application.authentication) {
authManager.checkAuthOnRefresh();
authManager.redirectWhenUnauthenticated();
Authentication.init();
$rootScope.$on('tokenHasExpired', function($state) {
$state.go('auth', {error: 'Your session has expired'});
});
}
if (state.application.analytics) {
Analytics.offline(false);
Analytics.registerScriptTags();
Analytics.registerTrackers();
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
Analytics.trackPage(toState.url);
Analytics.pageView();
});
}
}, function error(err) {
Messages.error("Failure", err, 'Unable to retrieve application settings');
});
$rootScope.$state = $state;
}])
// 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_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.12.2');
.state('index', {
url: '/',
templateUrl: 'app/components/dashboard/dashboard.html',
controller: 'DashboardController'
})
.state('containers', {
url: '/containers/',
templateUrl: 'app/components/containers/containers.html',
controller: 'ContainersController'
})
.state('container', {
url: "^/containers/:id",
templateUrl: 'app/components/container/container.html',
controller: 'ContainerController'
})
.state('stats', {
url: "^/containers/:id/stats",
templateUrl: 'app/components/stats/stats.html',
controller: 'StatsController'
})
.state('logs', {
url: "^/containers/:id/logs",
templateUrl: 'app/components/containerLogs/containerlogs.html',
controller: 'ContainerLogsController'
})
.state('console', {
url: "^/containers/:id/console",
templateUrl: 'app/components/containerConsole/containerConsole.html',
controller: 'ContainerConsoleController'
})
.state('actions', {
abstract: true,
url: "/actions",
template: '<ui-view/>'
})
.state('actions.create', {
abstract: true,
url: "/create",
template: '<ui-view/>'
})
.state('actions.create.container', {
url: "/container",
templateUrl: 'app/components/createContainer/createcontainer.html',
controller: 'CreateContainerController'
})
.state('actions.create.volume', {
url: "/volume",
templateUrl: 'app/components/createVolume/createvolume.html',
controller: 'CreateVolumeController'
})
.state('actions.create.network', {
url: "/network",
templateUrl: 'app/components/createNetwork/createnetwork.html',
controller: 'CreateNetworkController'
})
.state('docker', {
url: '/docker/',
templateUrl: 'app/components/docker/docker.html',
controller: 'DockerController'
})
.state('events', {
url: '/events/',
templateUrl: 'app/components/events/events.html',
controller: 'EventsController'
})
.state('images', {
url: '/images/',
templateUrl: 'app/components/images/images.html',
controller: 'ImagesController'
})
.state('image', {
url: '^/images/:id/',
templateUrl: 'app/components/image/image.html',
controller: 'ImageController'
})
.state('networks', {
url: '/networks/',
templateUrl: 'app/components/networks/networks.html',
controller: 'NetworksController'
})
.state('network', {
url: '^/networks/:id/',
templateUrl: 'app/components/network/network.html',
controller: 'NetworkController'
})
.state('volumes', {
url: '/volumes/',
templateUrl: 'app/components/volumes/volumes.html',
controller: 'VolumesController'
})
.state('swarm', {
url: '/swarm/',
templateUrl: 'app/components/swarm/swarm.html',
controller: 'SwarmController'
});
// The Docker API likes to return plaintext errors, this catches them and disp
$httpProvider.interceptors.push(function() {
return {
'response': function(response) {
if (typeof(response.data) === 'string' &&
(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;
}
};
});
}])
// 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('UI_VERSION', 'v1.7.0');
-101
View File
@@ -1,101 +0,0 @@
<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>
-115
View File
@@ -1,115 +0,0 @@
angular.module('auth', [])
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Messages',
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, EndpointProvider, Messages) {
$scope.authData = {
username: 'admin',
password: '',
error: ''
};
$scope.initPasswordData = {
password: '',
password_confirmation: '',
error: false
};
if (!$scope.applicationState.application.authentication) {
EndpointService.endpoints()
.then(function success(data) {
if (data.length > 0) {
endpointID = EndpointProvider.endpointID();
if (!endpointID) {
endpointID = data[0].Id;
EndpointProvider.setEndpointID(endpointID);
}
StateManager.updateEndpointState(true)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
});
}
else {
$state.go('endpointInit');
}
}, function error(err) {
Messages.error("Failure", err, 'Unable to retrieve endpoints');
});
} else {
Users.checkAdminUser({}, function () {},
function (e) {
if (e.status === 404) {
$scope.initPassword = true;
} else {
Messages.error("Failure", e, 'Unable to verify administrator account existence');
}
});
}
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;
});
$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(data) {
return EndpointService.endpoints();
})
.then(function success(data) {
var userDetails = Authentication.getUserDetails();
if (data.length > 0) {
endpointID = EndpointProvider.endpointID();
if (!endpointID) {
endpointID = data[0].Id;
EndpointProvider.setEndpointID(endpointID);
}
StateManager.updateEndpointState(true)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
});
}
else if (data.length === 0 && userDetails.role === 1) {
$state.go('endpointInit');
} else if (data.length === 0 && userDetails.role === 2) {
Authentication.logout();
$scope.authData.error = 'User not allowed. Please contact your administrator.';
}
})
.catch(function error(err) {
$scope.authData.error = 'Authentication error';
});
};
}]);
+145 -218
View File
@@ -3,23 +3,72 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-6 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-tasks"></i>
</div>
<div ng-if="!container.edit">
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
Name <a href="" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
</div>
</div>
<div ng-if="container.edit">
<div class="title"><input type="text" class="containerNameInput" ng-model="container.newContainerName"></div>
<div class="comment">
Name
<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>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div ng-class="{true: 'widget-icon green pull-left', false: 'widget-icon red pull-left'}[container.State.Running]">
<i class="fa fa-heartbeat"></i>
</div>
<div class="title">{{ container.State|getstatetext }}</div>
<div class="comment">State</div>
</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-cogs" title="Actions"></rd-widget-header>
<rd-widget-body classes="padding">
<div class="btn-group" role="group" aria-label="...">
<button class="btn btn-success" ng-click="start()" ng-disabled="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-disabled="!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-disabled="!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-disabled="!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-disabled="!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-disabled="!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>
<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-primary" ng-click="commit()">Commit</button>
<button class="btn btn-primary" ng-click="start()" ng-disabled="container.State.Running">Start</button>
<button class="btn btn-primary" ng-click="stop()" ng-disabled="!container.State.Running">Stop</button>
<button class="btn btn-primary" ng-click="kill()" ng-disabled="!container.State.Running">Kill</button>
<button class="btn btn-primary" ng-click="restart()">Restart</button>
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running && !container.State.Paused">Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused">Unpause</button>
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running">Remove</button>
</div>
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-default" type="button" ui-sref="stats({id: container.Id})">Stats</a>
<a class="btn btn-default" type="button" ui-sref="logs({id: container.Id})">Logs</a>
<a class="btn btn-default" type="button" ui-sref="console({id: container.Id})">Console</a>
</div>
</div>
<div class="comment">
Actions
</div>
</rd-widget-body>
</rd-widget>
@@ -27,149 +76,44 @@
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<div class="col-lg-9">
<rd-widget>
<rd-widget-header icon="fa-server" title="Container status"></rd-widget-header>
<rd-widget-header icon="fa-tasks" title="Container status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td ng-if="!container.edit">
{{ container.Name|trimcontainername }}
<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">
<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">
<td>IP address</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
<td>Created</td>
<td>{{ container.Created|getisodate }}</td>
</tr>
<tr>
<td>Status</td>
<td>Path</td>
<td>{{ container.Path }}</td>
</tr>
<tr>
<td>Args</td>
<td>{{ container.Args.join(' ') || 'None' }}</td>
</tr>
<tr>
<td>Exposed Ports</td>
<td>
<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>
<ul>
<li ng-repeat="(k, v) in container.Config.ExposedPorts">{{ k }}</li>
</ul>
</td>
</tr>
<tr ng-if="container.State.Running">
<td>Start time</td>
<td>{{ container.State.StartedAt|getisodate }}</td>
</tr>
<tr ng-if="!container.State.Running">
<td>Finished</td>
<td>{{ container.State.FinishedAt|getisodate }}</td>
</tr>
<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 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>
</tbody>
</table>
</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-clone" title="Create image"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- tag-description -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
You can create an image from this container, this allows you to backup important data or save
helpful configurations. You'll be able to spin up another container based on this image afterward.
</span>
</div>
</div>
<!-- !tag-description -->
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional">
</div>
</div>
<!-- !name-and-registry-inputs -->
<!-- tag-note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">Note: if you don't specify the tag in the image name, <span class="label label-default">latest</span> will be used.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button>
<i id="createImageSpinner" 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-server" title="Container details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Image</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
</tr>
<tr ng-if="portBindings.length > 0">
<td>Port configuration</td>
<td>Environment</td>
<td>
<div ng-repeat="portMapping in portBindings">
{{ portMapping.container }} <i class="fa fa-long-arrow-right"></i> {{ portMapping.host }}
</div>
<ul>
<li ng-repeat="k in container.Config.Env">{{ k }}</li>
</ul>
</td>
</tr>
<tr>
<td>CMD</td>
<td><code>{{ container.Config.Cmd|command }}</code></td>
</tr>
<tr>
<td>ENV</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in container.Config.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
</tr>
</table>
</td>
</tr>
<tr ng-if="!(container.Config.Labels | emptyobject)">
<td>Labels</td>
<td>
<table class="table table-bordered table-condensed">
<table role="table" class="table">
<tr ng-repeat="(k, v) in container.Config.Labels">
<td>{{ k }}</td>
<td>{{ v }}</td>
@@ -177,94 +121,77 @@
</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>
</rd-widget>
</div>
</div>
<div class="row" ng-if="container.HostConfig.Binds.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-cubes" title="Volumes"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<thead>
<tr>
<th>Host</th>
<th>Container</th>
<td>Publish all ports</td>
<td>{{ container.HostConfig.PublishAllPorts }}</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="vol in container.HostConfig.Binds">
<td>{{ vol|key: ':' }}</td>
<td>{{ vol|value: ':' }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</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>
<tr>
<td>Ports</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>
<ul>
<li ng-repeat="(containerport, hostports) in container.NetworkSettings.Ports">
{{ containerport }} =>
<span class="label label-default" style="margin-right: 5px;" ng-repeat="(k,v) in hostports">{{ v.HostIp }}:{{ v.HostPort }}</span>
</li>
</ul>
</td>
</tr>
<tr>
<td>Hostname</td>
<td>{{ container.Config.Hostname }}</td>
</tr>
<tr>
<td>IPAddress</td>
<td>{{ container.NetworkSettings.IPAddress }}</td>
</tr>
<tr>
<td>Cmd</td>
<td>{{ container.Config.Cmd }}</td>
</tr>
<tr>
<td>Entrypoint</td>
<td>{{ container.Config.Entrypoint.join(' ') }}</td>
</tr>
<tr>
<td>Bindings</td>
<td>
<ul>
<li ng-repeat="b in container.HostConfig.Binds">{{ b }}</li>
</ul>
</td>
</tr>
<tr>
<td>Volumes</td>
<td>{{ container.Volumes }}</td>
</tr>
<tr>
<td>SysInitpath</td>
<td>{{ container.SysInitPath }}</td>
</tr>
<tr>
<td>Image</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
</tr>
</tbody>
</table>
<div class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="col-lg-3">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container state details"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr ng-repeat="(key, val) in container.State">
<td>{{key}}</td>
<td ng-if="key === 'StartedAt' || key === 'FinishedAt'">{{val|getisodate}}</td>
<td ng-if="key !== 'StartedAt' && key !== 'FinishedAt'">{{val}}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
<div>
+105 -77
View File
@@ -1,17 +1,13 @@
angular.module('container', [])
.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);
.controller('ContainerController', ['$scope', '$stateParams', '$state', '$filter', 'Container', 'ContainerCommit', 'Image', 'Messages', '$timeout',
function ($scope, $stateParams, $state, $filter, Container, ContainerCommit, Image, Messages, $timeout) {
$scope.changes = [];
$scope.editEnv = false;
$scope.editPorts = false;
$scope.editBinds = false;
$scope.newCfg = {
Env: [],
Ports: {}
};
var update = function () {
@@ -21,38 +17,74 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
$scope.container.edit = false;
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
if (d.State.Running) {
$scope.activityTime = moment.duration(moment(d.State.StartedAt).utc().diff(moment().utc())).humanize();
} else {
$scope.activityTime = moment.duration(moment().utc().diff(moment(d.State.FinishedAt).utc())).humanize();
}
$scope.portBindings = [];
if (d.NetworkSettings.Ports) {
angular.forEach(Object.keys(d.NetworkSettings.Ports), function(portMapping) {
if (d.NetworkSettings.Ports[portMapping]) {
var mapping = {};
mapping.container = portMapping;
mapping.host = d.NetworkSettings.Ports[portMapping][0].HostIp + ':' + d.NetworkSettings.Ports[portMapping][0].HostPort;
$scope.portBindings.push(mapping);
}
// fill up env
if (d.Config.Env) {
$scope.newCfg.Env = d.Config.Env.map(function (entry) {
return {name: entry.split('=')[0], value: entry.split('=')[1]};
});
}
// fill up ports
$scope.newCfg.Ports = {};
angular.forEach(d.Config.ExposedPorts, function(i, port) {
if (d.HostConfig.PortBindings && port in d.HostConfig.PortBindings) {
$scope.newCfg.Ports[port] = d.HostConfig.PortBindings[port];
}
else {
$scope.newCfg.Ports[port] = [];
}
});
// fill up bindings
$scope.newCfg.Binds = [];
var defaultBinds = {};
angular.forEach(d.Config.Volumes, function(value, vol) {
defaultBinds[vol] = { ContPath: vol, HostPath: '', ReadOnly: false, DefaultBind: true };
});
angular.forEach(d.HostConfig.Binds, function(binding, i) {
var mountpoint = binding.split(':')[0];
var vol = binding.split(':')[1] || '';
var ro = binding.split(':').length > 2 && binding.split(':')[2] === 'ro';
var defaultBind = false;
if (vol === '') {
vol = mountpoint;
mountpoint = '';
}
if (vol in defaultBinds) {
delete defaultBinds[vol];
defaultBind = true;
}
$scope.newCfg.Binds.push({ ContPath: vol, HostPath: mountpoint, ReadOnly: ro, DefaultBind: defaultBind });
});
angular.forEach(defaultBinds, function(bind) {
$scope.newCfg.Binds.push(bind);
});
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
};
$scope.start = function () {
$('#loadingViewSpinner').show();
Container.start({id: $scope.container.Id}, {}, function (d) {
Container.start({
id: $scope.container.Id,
HostConfig: $scope.container.HostConfig
}, function (d) {
update();
Messages.send("Container started", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to start container");
Messages.error("Failure", "Container failed to start." + e.data);
});
};
@@ -63,7 +95,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container stopped", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to stop container");
Messages.error("Failure", "Container failed to stop." + e.data);
});
};
@@ -74,26 +106,20 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container killed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to kill container");
Messages.error("Failure", "Container failed to die." + e.data);
});
};
$scope.commit = function () {
$('#createImageSpinner').show();
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();
$('#loadingViewSpinner').show();
ContainerCommit.commit({id: $stateParams.id, repo: $scope.container.Config.Image}, function (d) {
update();
Messages.send("Container commited", $stateParams.id);
}, function (e) {
$('#createImageSpinner').hide();
update();
Messages.error("Failure", e, "Unable to commit container");
Messages.error("Failure", "Container failed to commit." + e.data);
});
};
$scope.pause = function () {
$('#loadingViewSpinner').show();
Container.pause({id: $stateParams.id}, function (d) {
@@ -101,7 +127,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container paused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to pause container");
Messages.error("Failure", "Container failed to pause." + e.data);
});
};
@@ -112,24 +138,19 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container unpaused", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to unpause container");
Messages.error("Failure", "Container failed to unpause." + e.data);
});
};
$scope.remove = function () {
$('#loadingViewSpinner').show();
Container.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", d.message);
}
else {
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
}
update();
$state.go('containers', {}, {reload: true});
Messages.send("Container removed", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to remove container");
Messages.error("Failure", "Container failed to remove." + e.data);
});
};
@@ -140,41 +161,48 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
Messages.send("Container restarted", $stateParams.id);
}, function (e) {
update();
Messages.error("Failure", e, "Unable to restart container");
Messages.error("Failure", "Container failed to restart." + e.data);
});
};
$scope.hasContent = function (data) {
return data !== null && data !== undefined;
};
$scope.getChanges = function () {
$('#loadingViewSpinner').show();
Container.changes({id: $stateParams.id}, function (d) {
$scope.changes = d;
$('#loadingViewSpinner').hide();
});
};
$scope.renameContainer = function () {
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
if (d.message) {
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Unable to rename container", {}, d.message);
// #FIXME fix me later to handle http status to show the correct error message
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (data) {
if (data.name) {
$scope.container.Name = data.name;
Messages.send("Container renamed", $stateParams.id);
} else {
$scope.container.Name = $scope.container.newContainerName;
Messages.send("Container successfully renamed", d.name);
$scope.container.newContainerName = $scope.container.Name;
Messages.error("Failure", "Container failed to rename.");
}
}, function (e) {
Messages.error("Failure", e, 'Unable to rename container');
});
$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");
});
$scope.addEntry = function (array, entry) {
array.push(entry);
};
$scope.rmEntry = function (array, entry) {
var idx = array.indexOf(entry);
array.splice(idx, 1);
};
$scope.toggleEdit = function() {
$scope.edit = !$scope.edit;
};
update();
$scope.getChanges();
}]);
@@ -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 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>
Containers > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
</rd-header-content>
</rd-header>
<div class="row" ng-if="state.loaded">
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-terminal" title="Console">
@@ -16,27 +16,18 @@
</div>
</rd-widget-header>
<rd-widget-body>
<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>
<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>
</div>
<!-- !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 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>
</div>
</div>
</form>
@@ -1,37 +1,21 @@
angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Messages',
function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, EndpointProvider, Messages) {
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Exec', '$timeout', 'Messages', 'errorMsgFilter',
function ($scope, $stateParams, Settings, Container, Exec, $timeout, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.loaded = false;
$scope.state.connected = false;
$scope.state.command = "bash";
$scope.connected = false;
var socket, term;
// Ensure the socket is closed before leaving the view
$scope.$on('$stateChangeStart', function (event, next, current) {
if (socket && socket !== null) {
if (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();
});
@@ -49,28 +33,28 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
};
Container.exec(execConfig, function(d) {
if (d.message) {
$('#loadConsoleSpinner').hide();
Messages.error("Error", {}, d.message);
} else {
if (d.Id) {
var execId = d.Id;
resizeTTY(execId, termHeight, termWidth);
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
var url = window.location.href.split('#')[0] + 'ws/exec?id=' + execId;
if (url.indexOf('https') > -1) {
url = url.replace('https://', 'wss://');
} else {
url = url.replace('http://', 'ws://');
}
initTerm(url, termHeight, termWidth);
} else {
$('#loadConsoleSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#loadConsoleSpinner').hide();
Messages.error("Failure", e, 'Unable to start an exec instance');
Messages.error("Failure", e.data);
});
};
$scope.disconnect = function() {
$scope.state.connected = false;
$scope.connected = false;
if (socket !== null) {
socket.close();
}
@@ -82,11 +66,10 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
function resizeTTY(execId, height, width) {
$timeout(function() {
Exec.resize({id: execId, height: height, width: width}, function (d) {
if (d.message) {
Messages.error('Error', {}, 'Unable to resize TTY');
var error = errorMsgFilter(d);
if (error) {
Messages.error('Error', 'Unable to resize TTY');
}
}, function (e) {
Messages.error("Failure", {}, 'Unable to resize TTY');
});
}, 2000);
@@ -95,26 +78,31 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
function initTerm(url, height, width) {
socket = new WebSocket(url);
$scope.state.connected = true;
$scope.connected = true;
socket.onopen = function(evt) {
$('#loadConsoleSpinner').hide();
term = new Terminal();
term = new Terminal({
cols: width,
rows: height,
cursorBlink: true
});
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.state.connected = false;
$scope.connected = false;
};
socket.onclose = function(evt) {
$scope.state.connected = false;
$scope.connected = false;
// term.write("Session terminated");
// term.destroy();
};
};
}
@@ -13,8 +13,12 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
$scope.container = d;
$('#loadingViewSpinner').hide();
}, function (e) {
if (e.status === 404) {
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve container info");
});
function getLogs() {
@@ -3,7 +3,7 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
Containers > <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-server"></i>
<i class="fa fa-tasks"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">Name</div>
+31 -93
View File
@@ -3,37 +3,29 @@
<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-server" title="Containers">
<rd-widget-header icon="fa-tasks" title="Containers">
<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>
<i id="loadContainersSpinner" 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">
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-success 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-danger 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-danger 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>
<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>
</div>
<a class="btn btn-primary" type="button" ui-sref="actions.create.container"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add container</a>
<a class="btn btn-default" 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>
@@ -45,14 +37,12 @@
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th></th>
<th>
<a ui-sref="containers" ng-click="order('Status')">
State
<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 == 'State' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'State' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
@@ -62,13 +52,6 @@
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" 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 ng-if="state.displayIP">
<a ui-sref="containers" ng-click="order('IP')">
IP Address
@@ -76,87 +59,42 @@
<span ng-show="sortType == 'IP' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<th ng-if="swarm">
<a ui-sref="containers" ng-click="order('Host')">
Host IP
Host
<span ng-show="sortType == 'Host' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Host' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="containers" ng-click="order('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 ui-sref="containers" 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 ng-if="applicationState.application.authentication">
<a ui-sref="containers" ng-click="order('Metadata.ResourceControl.OwnerId')">
Ownership
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Metadata.ResourceControl.OwnerId' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
<th>
<a ui-sref="containers" ng-click="order('Command')">
Command
<span ng-show="sortType == 'Command' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Command' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<tr ng-repeat="container in (state.filteredContainers = ( containers | filter:state.filter | orderBy:sortType:sortReverse))">
<td><input type="checkbox" ng-model="container.Checked" ng-change="selectItem(container)"/></td>
<td><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|truncate: 40}}</a></td>
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: 40}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></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 ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</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.public}}:{{ p.private }}
</a>
<span ng-if="container.Ports.length == 0" >-</span>
</td>
<td ng-if="applicationState.application.authentication">
<span ng-if="!container.Metadata.ResourceControl">
<i class="fa fa-eye" aria-hidden="true"></i>
<span ng-if="container.Labels['com.docker.swarm.service.id']">
Public service
</span>
<span ng-if="!container.Labels['com.docker.swarm.service.id']">
Public
</span>
</span>
<span ng-if="container.Metadata.ResourceControl.OwnerId === user.ID">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
<span ng-if="container.Labels['com.docker.swarm.service.id']">
Private service
</span>
<span ng-if="!container.Labels['com.docker.swarm.service.id']">
Private
<a ng-click="switchOwnership(container)" class="interactive"><i class="fa fa-eye" aria-hidden="true" style="margin-left: 7px;"></i> Switch to public</a>
</span>
</span>
<span ng-if="container.Metadata.ResourceControl && container.Metadata.ResourceControl.OwnerId !== user.ID">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
<span ng-if="container.Labels['com.docker.swarm.service.id']">
Private service <span ng-if="container.Owner">(owner: {{ container.Owner }})</span>
</span>
<span ng-if="!container.Labels['com.docker.swarm.service.id']">
Private <span ng-if="container.Owner">(owner: {{ container.Owner }})</span>
<a ng-click="switchOwnership(container)" class="interactive"><i class="fa fa-eye" aria-hidden="true" style="margin-left: 7px;"></i> Switch to public</a>
</span>
</span>
</td>
</tr>
<tr ng-if="!containers">
<td colspan="9" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="containers.length == 0">
<td colspan="9" class="text-center text-muted">No containers available.</td>
<td ng-if="swarm">{{ container.hostIP }}</td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image }}</a></td>
<td>{{ container.Command|truncate:60 }}</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>
+47 -119
View File
@@ -1,107 +1,38 @@
angular.module('containers', [])
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerHelper', 'Info', 'Settings', 'Messages', 'Config', 'Pagination', 'EntityListService', 'ModalService', 'Authentication', 'ResourceControlService', 'UserService',
function ($q, $scope, $filter, Container, ContainerHelper, Info, Settings, Messages, Config, Pagination, EntityListService, ModalService, Authentication, ResourceControlService, UserService) {
.controller('ContainersController', ['$scope', 'Container', 'Info', 'Settings', 'Messages', 'Config', 'errorMsgFilter',
function ($scope, Container, Info, Settings, Messages, Config, errorMsgFilter) {
$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.sortReverse = true;
$scope.state.selectedItemCount = 0;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('containers', $scope.state.pagination_count);
};
function removeContainerResourceControl(container) {
volumeResourceControlQueries = [];
angular.forEach(container.Mounts, function (volume) {
volumeResourceControlQueries.push(ResourceControlService.removeVolumeResourceControl(container.Metadata.ResourceControl.OwnerId, volume.Name));
});
$q.all(volumeResourceControlQueries)
.then(function success() {
return ResourceControlService.removeContainerResourceControl(container.Metadata.ResourceControl.OwnerId, container.Id);
})
.then(function success() {
delete container.Metadata.ResourceControl;
Messages.send('Ownership changed to public', container.Id);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to change container ownership");
});
}
$scope.switchOwnership = function(container) {
ModalService.confirmContainerOwnershipChange(function (confirmed) {
if(!confirmed) { return; }
removeContainerResourceControl(container);
});
};
function mapUsersToContainers(users) {
angular.forEach($scope.containers, function (container) {
if (container.Metadata) {
var containerRC = container.Metadata.ResourceControl;
if (containerRC && containerRC.OwnerId !== $scope.user.ID) {
angular.forEach(users, function (user) {
if (containerRC.OwnerId === user.Id) {
container.Owner = user.Username;
}
});
}
}
});
}
var update = function (data) {
$('#loadContainersSpinner').show();
var userDetails = Authentication.getUserDetails();
$scope.user = userDetails;
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
if ($scope.containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
if (hiddenLabels) {
containers = hideContainers(d);
}
$scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container);
model.Status = $filter('containerstatus')(model.Status);
EntityListService.rememberPreviousSelection($scope.containers, model, function onSelect(model){
$scope.selectItem(model);
});
if (model.IP) {
$scope.state.displayIP = true;
}
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
if ($scope.swarm) {
model.hostIP = $scope.swarm_hosts[_.split(container.Names[0], '/')[1]];
}
return model;
});
if (userDetails.role === 1) {
UserService.users()
.then(function success(data) {
mapUsersToContainers(data);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to retrieve users");
})
.finally(function final() {
$('#loadContainersSpinner').hide();
});
} else {
$('#loadContainersSpinner').hide();
}
}, function (e) {
$('#loadContainersSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve containers");
$scope.containers = [];
});
};
@@ -119,48 +50,37 @@ angular.module('containers', [])
if (c.Checked) {
counter = counter + 1;
if (action === Container.start) {
action({id: c.Id}, {}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
Container.get({id: c.Id}, function (d) {
c = d;
action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) {
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e.data);
complete();
});
}, function (e) {
Messages.error("Failure", e, "Unable to start container");
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
complete();
});
}
else if (action === Container.remove) {
action({id: c.Id}, function (d) {
if (d.message) {
Messages.send("Error", d.message);
var error = errorMsgFilter(d);
if (error) {
Messages.send("Error", "Unable to remove running container");
}
else {
if (c.Metadata && c.Metadata.ResourceControl) {
ResourceControlService.removeContainerResourceControl(c.Metadata.ResourceControl.OwnerId, c.Id)
.then(function success() {
Messages.send("Container " + msg, c.Id);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to remove container ownership");
});
} else {
Messages.send("Container " + msg, c.Id);
}
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove container');
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');
Messages.error("Failure", e.data);
complete();
});
}
@@ -169,7 +89,7 @@ angular.module('containers', [])
Messages.send("Container " + msg, c.Id);
complete();
}, function (e) {
Messages.error("Failure", e, 'An error occured');
Messages.error("Failure", e.data);
complete();
});
@@ -181,15 +101,6 @@ angular.module('containers', [])
}
};
$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++;
@@ -231,6 +142,21 @@ angular.module('containers', [])
batch($scope.containers, Container.remove, "Removed");
};
var hideContainers = function (containers) {
return containers.filter(function (container) {
var filterContainer = false;
hiddenLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
function retrieveSwarmHostsInfo(data) {
var swarm_hosts = {};
var systemStatus = data.SystemStatus;
@@ -246,9 +172,11 @@ angular.module('containers', [])
return swarm_hosts;
}
$scope.swarm = false;
Config.$promise.then(function (c) {
$scope.containersToHideLabels = c.hiddenLabels;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
hiddenLabels = c.hiddenLabels;
$scope.swarm = c.swarm;
if (c.swarm) {
Info.get({}, function (d) {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: Settings.displayAll ? 1 : 0});
@@ -1,43 +1,36 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference.
angular.module('createContainer', [])
.controller('CreateContainerController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Messages',
function ($scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Messages) {
.controller('CreateContainerController', ['$scope', '$state', 'Config', 'Container', 'Image', 'Volume', 'Network', 'Messages', 'errorMsgFilter',
function ($scope, $state, Config, Container, Image, Volume, Network, Messages, errorMsgFilter) {
$scope.state = {
alwaysPull: true
};
$scope.formValues = {
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
alwaysPull: true,
Console: 'none',
Volumes: [],
Registry: '',
NetworkContainer: '',
Labels: [],
ExtraHosts: []
AvailableRegistries: [],
Registry: ''
};
$scope.imageConfig = {};
$scope.config = {
Image: '',
Env: [],
Cmd: '',
ExposedPorts: {},
HostConfig: {
RestartPolicy: {
Name: 'no'
},
PortBindings: [],
PublishAllPorts: false,
Binds: [],
NetworkMode: 'bridge',
Privileged: false,
ExtraHosts: []
},
Labels: {}
Privileged: false
}
};
$scope.addVolume = function() {
$scope.formValues.Volumes.push({ name: '', containerPath: '', readOnly: false, type: 'volume' });
$scope.formValues.Volumes.push({ name: '', containerPath: '' });
};
$scope.removeVolume = function(index) {
@@ -60,35 +53,25 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
$scope.config.HostConfig.PortBindings.splice(index, 1);
};
$scope.addLabel = function() {
$scope.formValues.Labels.push({ name: '', value: ''});
};
$scope.removeLabel = function(index) {
$scope.formValues.Labels.splice(index, 1);
};
$scope.addExtraHost = function() {
$scope.formValues.ExtraHosts.push({ value: '' });
};
$scope.removeExtraHost = function(index) {
$scope.formValues.ExtraHosts.splice(index, 1);
};
Config.$promise.then(function (c) {
var containersToHideLabels = c.hiddenLabels;
var swarm = c.swarm;
$scope.formValues.AvailableRegistries = c.registries;
Volume.query({}, function (d) {
$scope.availableVolumes = d.Volumes;
var persistedVolumes = d.Volumes.filter(function (volume) {
if (volume.Driver === 'local-persist') {
return volume;
}
});
$scope.availableVolumes = _.uniqBy(persistedVolumes, 'Name');
}, function (e) {
Messages.error("Failure", e, "Unable to retrieve volumes");
Messages.error("Failure", e.data);
});
Network.query({}, function (d) {
var networks = d;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
if (swarm) {
networks = d.filter(function (network) {
if (network.Scope === 'global') {
return network;
@@ -99,80 +82,70 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
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");
Messages.error("Failure", e.data);
});
});
function startContainer(containerID) {
Container.start({id: containerID}, {}, function (cd) {
if (cd.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, cd.message);
} else {
$('#createContainerSpinner').hide();
Messages.send('Container Started', containerID);
$state.go('containers', {}, {reload: true});
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to start container');
});
}
function createContainer(config) {
$('#createContainerSpinner').show();
Container.create(config, function (d) {
if (d.message) {
$('#createContainerSpinner').hide();
Messages.error('Error', {}, d.message);
if (d.Id) {
var reqBody = config.HostConfig || {};
reqBody.id = d.Id;
Container.start(reqBody, function (cd) {
$('#createContainerSpinner').hide();
Messages.send('Container Started', d.Id);
$state.go('containers', {}, {reload: true});
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(e));
});
} else {
if ($scope.formValues.Ownership === 'private') {
ResourceControlService.setContainerResourceControl(Authentication.getUserDetails().ID, d.Id)
.then(function success() {
startContainer(d.Id);
})
.catch(function error(err) {
$('#createContainerSpinner').hide();
Messages.error("Failure", err, 'Unable to apply resource control on container');
});
} else {
startContainer(d.Id);
}
$('#createContainerSpinner').hide();
Messages.error('Error', errorMsgFilter(d));
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error("Failure", e, 'Unable to create container');
Messages.error('Error', errorMsgFilter(e));
});
}
function pullImageAndCreateContainer(config) {
$('#createContainerSpinner').show();
Image.create($scope.imageConfig, function (data) {
createContainer(config);
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);
}
}, function (e) {
$('#createContainerSpinner').hide();
Messages.error('Failure', e, 'Unable to pull image');
Messages.error('Error', 'Unable to pull image ' + 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 = config.Image;
var image = _.toLower(config.Image);
var registry = $scope.formValues.Registry;
var imageConfig = ImageHelper.createImageConfigForContainer(image, registry);
var imageConfig = createImageConfig(image, registry);
config.Image = imageConfig.fromImage + ':' + imageConfig.tag;
$scope.imageConfig = imageConfig;
}
@@ -180,10 +153,10 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
function preparePortBindings(config) {
var bindings = {};
config.HostConfig.PortBindings.forEach(function (portBinding) {
if (portBinding.containerPort) {
if (portBinding.hostPort && portBinding.containerPort) {
var key = portBinding.containerPort + "/" + portBinding.protocol;
var binding = {};
if (portBinding.hostPort && portBinding.hostPort.indexOf(':') > -1) {
if (portBinding.hostPort.indexOf(':') > -1) {
var hostAndPort = portBinding.hostPort.split(':');
binding.HostIp = hostAndPort[0];
binding.HostPort = hostAndPort[1];
@@ -243,59 +216,24 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
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;
$scope.formValues.ExtraHosts.forEach(function (v) {
if (v.value) {
config.HostConfig.ExtraHosts.push(v.value);
}
});
}
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);
config.Cmd = ContainerHelper.commandStringToArray(config.Cmd);
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.formValues.alwaysPull) {
if ($scope.state.alwaysPull) {
pullImageAndCreateContainer(config);
} else {
createContainer(config);
}
};
}]);
@@ -1,7 +1,7 @@
<rd-header>
<rd-header-title title="Create container"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> > Add container
Containers > Add container
</rd-header-content>
</rd-header>
@@ -18,130 +18,82 @@
</div>
</div>
<!-- !name-input -->
<div class="col-sm-12 form-section-title">
Image configuration
</div>
<!-- image-and-registry-inputs -->
<div class="form-group">
<label for="container_image" class="col-sm-1 control-label text-left">Image</label>
<div class="col-sm-11 col-md-6">
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="container_image" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<select class="selectpicker form-control" ng-model="formValues.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
<div class="col-sm-offset-1 col-sm-11">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="state.alwaysPull"> Always pull image before creating
</label>
</div>
</div>
</div>
<!-- !image-and-registry-inputs -->
<!-- always-pull -->
<!-- restart-policy -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Always pull the image
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically try to pull the specified image before creating the container."></portainer-tooltip>
<label class="col-sm-1 control-label text-left">Restart policy</label>
<div class="col-sm-11">
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="no">
Never
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="always">
Always
</label>
<label class="radio-inline">
<input type="radio" name="container_restart_policy" ng-model="config.HostConfig.RestartPolicy.Name" value="on-failure">
<span class="radio-value">On failure</span>
</label>
</div>
</div>
<!-- !always-pull -->
<div class="col-sm-12 form-section-title">
Ports configuration
</div>
<!-- publish-exposed-ports -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Publish all exposed ports
<portainer-tooltip position="bottom" message="When enabled, Portainer will let Docker automatically map a random port on the host to each one defined in the image Dockerfile."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="config.HostConfig.PublishAllPorts"><i></i>
</label>
</div>
</div>
<!-- !publish-exposed-ports -->
<!-- !restart-policy -->
<!-- port-mapping -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
<label for="container_ports" class="col-sm-1 control-label text-left">Port mapping</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map port
</span>
</div>
<!-- port-mapping-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in config.HostConfig.PortBindings" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<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)">
<input type="text" class="form-control" ng-model="portBinding.hostPort" placeholder="e.g. 80 or 1.2.3.4:80">
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 input-group-sm">
<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>
<!-- !container-port -->
<!-- protocol-actions -->
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'tcp'">TCP</label>
<label class="btn btn-primary" ng-model="portBinding.protocol" uib-btn-radio="'udp'">UDP</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
<div class="input-group col-sm-1 input-group-sm">
<select class="selectpicker form-control" ng-model="portBinding.protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
<!-- !protocol-actions -->
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<div class="col-sm-12 form-section-title" ng-if="applicationState.application.authentication">
Access control
</div>
<!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Ownership
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
Private
</label>
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i>
Public
</label>
</div>
</div>
</div>
<!-- !ownership -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="create()">Start container</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="containers">Cancel</a>
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
@@ -151,17 +103,14 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-cog" title="Advanced container settings"></rd-widget-header>
<rd-widget-body>
<ul class="nav nav-pills nav-justified">
<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="#env" data-toggle="tab">Env</a></li>
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
<li class="interactive"><a data-target="#restart-policy" data-toggle="tab">Restart policy</a></li>
<li class="interactive"><a data-target="#runtime" data-toggle="tab">Runtime</a></li>
<ul class="nav nav-tabs">
<li class="active clickable"><a data-target="#command" data-toggle="tab">Command</a></li>
<li class="clickable"><a data-target="#volumes" data-toggle="tab">Volumes</a></li>
<li class="clickable"><a data-target="#network" data-toggle="tab">Network</a></li>
<li class="clickable"><a data-target="#security" data-toggle="tab">Security/Host</a></li>
</ul>
<!-- tab-content -->
<div class="tab-content">
<!-- tab-command -->
@@ -169,7 +118,7 @@
<form class="form-horizontal" style="margin-top: 15px;">
<!-- command-input -->
<div class="form-group">
<label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
<label for="container_command" class="col-sm-1 control-label text-left">Command</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
</div>
@@ -177,7 +126,7 @@
<!-- !command-input -->
<!-- entrypoint-input -->
<div class="form-group">
<label for="container_entrypoint" class="col-sm-2 col-lg-1 control-label text-left">Entry Point</label>
<label for="container_entrypoint" class="col-sm-1 control-label text-left">Entry Point</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Entrypoint" id="container_entrypoint" placeholder="e.g. /bin/sh -c">
</div>
@@ -185,7 +134,7 @@
<!-- !entrypoint-input -->
<!-- workdir-user-input -->
<div class="form-group">
<label for="container_workingdir" class="col-sm-2 col-lg-1 control-label text-left">Working Dir</label>
<label for="container_workingdir" class="col-sm-1 control-label text-left">Working Dir</label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="config.WorkingDir" id="container_workingdir" placeholder="e.g. /myapp">
</div>
@@ -197,8 +146,8 @@
<!-- !workdir-user-input -->
<!-- console -->
<div class="form-group">
<label for="container_console" class="col-sm-2 col-lg-1 control-label text-left">Console</label>
<div class="col-sm-10 col-lg-11">
<label for="container_console" class="col-sm-1 control-label text-left">Console</label>
<div class="col-sm-11">
<div class="col-sm-4">
<label class="radio-inline">
<input type="radio" name="container_console" ng-model="formValues.Console" value="both">
@@ -212,7 +161,7 @@
</label>
</div>
</div>
<div class="col-sm-offset-2 col-sm-10 col-lg-offset-1 col-lg-11">
<div class="col-sm-offset-1 col-sm-11">
<div class="col-sm-4">
<label class="radio-inline">
<input type="radio" name="container_console" ng-model="formValues.Console" value="tty">
@@ -228,6 +177,35 @@
</div>
</div>
<!-- !console -->
<!-- environment-variables -->
<div class="form-group">
<label for="container_env" class="col-sm-1 control-label text-left">Environment variables</label>
<div class="col-sm-11">
<span class="label label-default clickable" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-offset-1 col-sm-11 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in config.Env" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</span>
</div>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
</form>
</div>
<!-- !tab-command -->
@@ -236,64 +214,33 @@
<form class="form-horizontal" style="margin-top: 15px;">
<!-- volumes -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Volume mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
<label for="container_volumes" class="col-sm-1 control-label text-left">Volumes</label>
<div class="col-sm-11" ng-if="availableVolumes.length !== 0">
<span class="label label-default clickable" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> volume
</span>
</div>
<div class="col-sm-11" ng-if="availableVolumes.length === 0" style="margin-top: 5px;">
<span class="small text-muted">You don't have any persistent volumes. Head over the <a ui-sref="volumes">volumes view</a> to create one.</span>
</div>
<!-- volumes-input-list -->
<div class="form-inline" style="margin-top: 10px;">
<div ng-repeat="volume in formValues.Volumes">
<!-- volume-line1 -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<!-- container-path -->
<div class="input-group input-group-sm col-sm-6">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.containerPath" placeholder="e.g. /path/in/container">
</div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.name = ''">Bind</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
<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-5 input-group-sm">
<select class="selectpicker form-control" ng-model="volume.name">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:50}}</option>
</select>
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.containerPath" placeholder="e.g. /path/in/container">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeVolume($index)">
<i class="fa fa-minus" aria-hidden="true"></i>
</button>
</div>
<!-- !volume-type -->
</span>
</div>
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'volume'">
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.name">
<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>
</div>
<!-- !volume -->
<!-- bind -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.type === 'bind'">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.name" placeholder="e.g. /path/on/host">
</div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.readOnly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.readOnly" uib-btn-radio="true">Read-only</label>
</div>
</div>
<!-- !read-only -->
</div>
<!-- !volume-line2 -->
</div>
</div>
<!-- !volumes-input-list -->
@@ -305,38 +252,25 @@
<!-- tab-network -->
<div class="tab-pane" id="network">
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group" ng-if="globalNetworkCount === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE'">
<div class="form-group" ng-if="globalNetworkCount === 0">
<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>
</div>
<!-- network-input -->
<div class="form-group">
<label for="container_network" class="col-sm-2 col-lg-1 control-label text-left">Network</label>
<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="config.HostConfig.NetworkMode" id="container_network">
<select class="selectpicker form-control" ng-model="config.HostConfig.NetworkMode">
<option selected disabled hidden value="">Select a network</option>
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
</select>
</div>
</div>
<!-- !network-input -->
<!-- container-name-input -->
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
<label for="container_network_container" class="col-sm-2 col-lg-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-2 col-lg-1 control-label text-left">Hostname</label>
<label for="container_hostname" class="col-sm-1 control-label text-left">Hostname</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Hostname" id="container_hostname" placeholder="e.g. web01">
</div>
@@ -344,151 +278,44 @@
<!-- !hostname-input -->
<!-- domainname-input -->
<div class="form-group">
<label for="container_domainname" class="col-sm-2 col-lg-1 control-label text-left">Domain Name</label>
<label for="container_domainname" class="col-sm-1 control-label text-left">Domain Name</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Domainname" id="container_domainname" placeholder="e.g. example.com">
</div>
</div>
<!-- !domainname -->
<!-- extra-hosts-variables -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Hosts file entries</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addExtraHost()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add additional entry
</span>
</div>
<!-- extra-hosts-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in formValues.ExtraHosts" style="margin-top: 2px;">
<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. host:IP">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeExtraHost($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !extra-hosts-input-list -->
</div>
<!-- !extra-hosts-variables -->
</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">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Labels</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-12 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">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels-->
</form>
</div>
<!-- !tab-labels -->
<!-- tab-env -->
<div class="tab-pane" id="env">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- environment-variables -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Environment variables</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="variable in config.Env" style="margin-top: 2px;">
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="variable.name" placeholder="e.g. FOO">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="variable.value" placeholder="e.g. bar">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !environment-variable-input-list -->
</div>
<!-- !environment-variables -->
</form>
</div>
<!-- !tab-labels -->
<!-- tab-restart-policy -->
<div class="tab-pane" id="restart-policy">
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Restart policy
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-primary" ng-model="config.HostConfig.RestartPolicy.Name" uib-btn-radio="'no'">
Never
</label>
<label class="btn btn-primary" ng-model="config.HostConfig.RestartPolicy.Name" uib-btn-radio="'always'">
Always
</label>
<label class="btn btn-primary" ng-model="config.HostConfig.RestartPolicy.Name" uib-btn-radio="'on-failure'">
On failure
</label>
<label class="btn btn-primary" ng-model="config.HostConfig.RestartPolicy.Name" uib-btn-radio="'unless-stopped'">
Unless stopped
</label>
</div>
</div>
</div>
</form>
</div>
<!-- !tab-restart-policy -->
<!-- tab-runtime -->
<div class="tab-pane" id="runtime">
<!-- tab-security -->
<div class="tab-pane" id="security">
<form class="form-horizontal" style="margin-top: 15px;">
<!-- privileged-mode -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Privileged mode
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="config.HostConfig.Privileged"><i></i>
</label>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="config.HostConfig.Privileged"> Privileged mode
</label>
</div>
</div>
</div>
<!-- !privileged-mode -->
</form>
</div>
<!-- !tab-runtime -->
<!-- !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="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-click="create()">Create</button>
<a type="button" class="btn btn-default btn-lg" ui-sref="containers">Cancel</a>
</div>
</div>
@@ -1,98 +0,0 @@
angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Messages', 'Network',
function ($scope, $state, Messages, Network) {
$scope.formValues = {
DriverOptions: [],
Subnet: '',
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() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$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) {
if (d.message) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', {}, d.message);
} else {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error("Failure", e, 'Unable to create network');
});
}
function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) {
var ipamConfig = {};
ipamConfig.Subnet = $scope.formValues.Subnet;
if ($scope.formValues.Gateway) {
ipamConfig.Gateway = $scope.formValues.Gateway ;
}
config.IPAM.Config.push(ipamConfig);
}
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
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;
}
$scope.create = function () {
var config = prepareConfiguration();
createNetwork(config);
};
}]);
@@ -1,135 +0,0 @@
<rd-header>
<rd-header-title title="Create network"></rd-header-title>
<rd-header-content>
<a ui-sref="networks">Networks</a> > Add network
</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="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 -->
<div class="col-sm-12 form-section-title">
Network configuration
</div>
<!-- subnet-gateway-inputs -->
<div class="form-group">
<label for="network_subnet" class="col-sm-2 col-lg-1 control-label text-left">Subnet</label>
<div class="col-sm-4 col-lg-5">
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
</div>
<label for="network_gateway" class="col-sm-2 col-lg-1 control-label text-left">Gateway</label>
<div class="col-sm-4 col-lg-5">
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
</div>
</div>
<!-- !subnet-gateway-inputs -->
<div class="col-sm-12 form-section-title">
Driver configuration
</div>
<!-- driver-input -->
<div class="form-group">
<label for="network_driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName">
</div>
</div>
<!-- !driver-input -->
<!-- driver-options -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">
Driver options
<portainer-tooltip position="bottom" message="Driver options are specific to the selected driver. Please refer to the selected driver documentation."></portainer-tooltip>
</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add driver option
</span>
</div>
<!-- driver-options-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" 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="option.name" placeholder="e.g. com.docker.network.bridge.enable_icc">
</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="option.value" placeholder="e.g. true">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
<div class="col-sm-12 form-section-title">
Advanced configuration
</div>
<!-- labels -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Labels</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-12 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">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels-->
<!-- internal -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Restrict external access to the network
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
</label>
</div>
</div>
<!-- !internal -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="create()">Create network</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="networks">Cancel</a>
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
@@ -1,254 +0,0 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference.
angular.module('createService', [])
.controller('CreateServiceController', ['$scope', '$state', 'Service', 'Volume', 'Network', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Messages',
function ($scope, $state, Service, Volume, Network, ImageHelper, Authentication, ResourceControlService, Messages) {
$scope.formValues = {
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
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({ Source: '', Target: '', ReadOnly: false, Type: 'volume' });
};
$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) {
if ($scope.formValues.Ownership === 'private') {
ResourceControlService.setServiceResourceControl(Authentication.getUserDetails().ID, d.ID)
.then(function success() {
$('#createServiceSpinner').hide();
Messages.send('Service created', d.ID);
$state.go('services', {}, {reload: true});
})
.catch(function error(err) {
$('#createContainerSpinner').hide();
Messages.error("Failure", err, 'Unable to apply resource control on service');
});
} else {
$('#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");
});
}]);
@@ -1,443 +0,0 @@
<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 -->
<div class="col-sm-12 form-section-title">
Image configuration
</div>
<!-- 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-11 col-md-6">
<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-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="formValues.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
</div>
</div>
<!-- !image-and-registry-inputs -->
<div class="col-sm-12 form-section-title">
Scheduling
</div>
<!-- scheduling-mode -->
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Scheduling mode
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-primary" ng-model="formValues.Mode" uib-btn-radio="'global'">Global</label>
<label class="btn btn-primary" ng-model="formValues.Mode" uib-btn-radio="'replicated'">Replicated</label>
</div>
</div>
</div>
<div class="form-group form-inline" ng-if="formValues.Mode === 'replicated'">
<div class="col-sm-12">
<label class="control-label text-left">
Replicas
</label>
<input type="number" class="form-control" ng-model="formValues.Replicas" id="replicas" placeholder="e.g. 3" style="margin-left: 20px;">
</div>
</div>
<!-- !scheduling-mode -->
<div class="col-sm-12 form-section-title">
Ports configuration
</div>
<!-- port-mapping -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Port mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPortBinding()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional port
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="portBinding in formValues.Ports" style="margin-top: 2px;">
<!-- host-port -->
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 80 or 1.2.3.4:80 (optional)">
</div>
<!-- !host-port -->
<span style="margin: 0 10px 0 10px;">
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
</span>
<!-- container-port -->
<div class="input-group col-sm-4 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>
<!-- !container-port -->
<!-- protocol-actions -->
<div class="input-group col-sm-3 input-group-sm">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="portBinding.Protocol" uib-btn-radio="'tcp'">TCP</label>
<label class="btn btn-primary" ng-model="portBinding.Protocol" uib-btn-radio="'udp'">UDP</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortBinding($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !protocol-actions -->
</div>
</div>
<!-- !port-mapping-input-list -->
</div>
<!-- !port-mapping -->
<div class="col-sm-12 form-section-title" ng-if="applicationState.application.authentication">
Access control
</div>
<!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Ownership
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
Private
</label>
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i>
Public
</label>
</div>
</div>
</div>
<!-- !ownership -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Image" ng-click="create()">Create service</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="services">Cancel</a>
<i id="createServiceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</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-pills nav-justified">
<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-2 col-lg-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-2 col-lg-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-2 col-lg-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">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Environment variables</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addEnvironmentVariable()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add environment variable
</span>
</div>
<!-- environment-variable-input-list -->
<div class="col-sm-12 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">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</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">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Volume mapping</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addVolume()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> map additional volume
</span>
</div>
<!-- volumes-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="volume in formValues.Volumes">
<div class="col-sm-12" style="margin-top: 10px;">
<!-- volume-line1 -->
<div class="col-sm-12 form-inline">
<!-- container-path -->
<div class="input-group input-group-sm col-sm-6">
<span class="input-group-addon">container</span>
<input type="text" class="form-control" ng-model="volume.Source" placeholder="e.g. /path/in/container">
</div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Name = ''">Bind</label>
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeVolume($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
<!-- !volume-type -->
</div>
<!-- !volume-line1 -->
<!-- volume-line2 -->
<div class="col-sm-12 form-inline" style="margin-top: 5px;">
<i class="fa fa-long-arrow-right" aria-hidden="true"></i>
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.Target">
<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>
</div>
<!-- !volume -->
<!-- bind -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'bind'">
<span class="input-group-addon">host</span>
<input type="text" class="form-control" ng-model="volume.Target" placeholder="e.g. /path/on/host">
</div>
<!-- !bind -->
<!-- read-only -->
<div class="input-group input-group-sm col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm">
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="false">Writable</label>
<label class="btn btn-primary" ng-model="volume.ReadOnly" uib-btn-radio="true">Read-only</label>
</div>
</div>
<!-- !read-only -->
</div>
<!-- !volume-line2 -->
</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-2 col-lg-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">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Extra networks</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addExtraNetwork()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add extra network
</span>
</div>
<!-- network-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="network in formValues.ExtraNetworks" style="margin-top: 2px;">
<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>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeExtraNetwork($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</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">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Service labels</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add service label
</span>
</div>
<!-- labels-input-list -->
<div class="col-sm-12 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">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !labels-input-list -->
</div>
<!-- !labels-->
<!-- container-labels -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Container labels</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addContainerLabel()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add container label
</span>
</div>
<!-- container-labels-input-list -->
<div class="col-sm-12 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">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeContainerLabel($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</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-2 col-lg-1 control-label text-left">Parallelism</label>
<div class="col-sm-2">
<input type="number" class="form-control" ng-model="formValues.Parallelism" id="parallelism" placeholder="e.g. 1">
</div>
<div class="col-sm-8">
<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-2 col-lg-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-8">
<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">
<div class="col-sm-12">
<label class="control-label text-left">Failure action</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-primary" ng-model="formValues.FailureAction" uib-btn-radio="'continue'">Continue</label>
<label class="btn btn-primary" ng-model="formValues.FailureAction" uib-btn-radio="'pause'">Pause</label>
</div>
</div>
</div>
<!-- !failureAction-input -->
</form>
</div>
<!-- !tab-update-config -->
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
@@ -1,70 +0,0 @@
angular.module('createVolume', [])
.controller('CreateVolumeController', ['$scope', '$state', 'Volume', 'ResourceControlService', 'Authentication', 'Messages',
function ($scope, $state, Volume, ResourceControlService, Authentication, Messages) {
$scope.formValues = {
Ownership: $scope.applicationState.application.authentication ? 'private' : '',
DriverOptions: []
};
$scope.config = {
Driver: 'local'
};
$scope.addDriverOption = function() {
$scope.formValues.DriverOptions.push({ name: '', value: '' });
};
$scope.removeDriverOption = function(index) {
$scope.formValues.DriverOptions.splice(index, 1);
};
function createVolume(config) {
$('#createVolumeSpinner').show();
Volume.create(config, function (d) {
if (d.message) {
$('#createVolumeSpinner').hide();
Messages.error('Unable to create volume', {}, d.message);
} else {
if ($scope.formValues.Ownership === 'private') {
ResourceControlService.setVolumeResourceControl(Authentication.getUserDetails().ID, d.Name)
.then(function success() {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
})
.catch(function error(err) {
$('#createVolumeSpinner').hide();
Messages.error("Failure", err, 'Unable to apply resource control on volume');
});
} else {
Messages.send("Volume created", d.Name);
$('#createVolumeSpinner').hide();
$state.go('volumes', {}, {reload: true});
}
}
}, function (e) {
$('#createVolumeSpinner').hide();
Messages.error("Failure", e, 'Unable to create volume');
});
}
function prepareDriverOptions(config) {
var options = {};
$scope.formValues.DriverOptions.forEach(function (option) {
options[option.name] = option.value;
});
config.DriverOpts = options;
}
function prepareConfiguration() {
var config = angular.copy($scope.config);
prepareDriverOptions(config);
return config;
}
$scope.create = function () {
var config = prepareConfiguration();
createVolume(config);
};
}]);
@@ -1,101 +0,0 @@
<rd-header>
<rd-header-title title="Create volume"></rd-header-title>
<rd-header-content>
<a ui-sref="volumes">Volumes</a> > Add volume
</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="volume_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="volume_name" placeholder="e.g. myVolume">
</div>
</div>
<!-- !name-input -->
<div class="col-sm-12 form-section-title">
Driver configuration
</div>
<!-- driver-input -->
<div class="form-group">
<label for="volume_driver" class="col-sm-1 control-label text-left">Driver</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="volume_driver" placeholder="e.g. driverName">
</div>
</div>
<!-- !driver-input -->
<!-- driver-options -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">
Driver options
<portainer-tooltip position="bottom" message="Driver options are specific to the selected driver. Please refer to the selected driver documentation."></portainer-tooltip>
</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addDriverOption()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add driver option
</span>
</div>
<!-- driver-options-input-list -->
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="option in formValues.DriverOptions" 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="option.name" placeholder="e.g. mountpoint">
</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="option.value" placeholder="e.g. /path/on/host">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removeDriverOption($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
<!-- !driver-options-input-list -->
</div>
<!-- !driver-options -->
<div class="col-sm-12 form-section-title" ng-if="applicationState.application.authentication">
Access control
</div>
<!-- ownership -->
<div class="form-group" ng-if="applicationState.application.authentication">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Ownership
<portainer-tooltip position="bottom" message="When setting the ownership value to private, only you and the administrators will be able to see and manage this object. When choosing public, everybody will be able to access it."></portainer-tooltip>
</label>
<div class="btn-group btn-group-sm" style="margin-left: 20px;">
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'private'">
<i class="fa fa-eye-slash" aria-hidden="true"></i>
Private
</label>
<label class="btn btn-primary" ng-model="formValues.Ownership" uib-btn-radio="'public'">
<i class="fa fa-eye" aria-hidden="true"></i>
Public
</label>
</div>
</div>
</div>
<!-- !ownership -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="create()">Create volume</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="volumes">Cancel</a>
<i id="createVolumeSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
+13 -35
View File
@@ -6,7 +6,7 @@
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="!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="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">
<div class="col-lg-12 col-md-12 col-xs-12" ng-if="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[0][1] == 'primary' ? infoData.SystemStatus[3][1] : infoData.SystemStatus[4][1] }}</td>
<td>{{ infoData.SystemStatus[3][1] }}</td>
</tr>
<tr>
<td>Swarm version</td>
@@ -53,29 +53,7 @@
</tr>
<tr>
<td>Total memory</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>
<td>{{ infoData.MemTotal|humansize }}</td>
</tr>
</tbody>
</table>
@@ -85,16 +63,16 @@
</div>
<div class="row">
<div class="col-xs-12 col-md-6">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="containers">
<rd-widget>
<rd-widget-body>
<div class="widget-icon blue pull-left">
<i class="fa fa-server"></i>
<i class="fa fa-tasks"></i>
</div>
<div class="pull-right">
<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><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>
<div class="title">{{ containerData.total }}</div>
<div class="comment">Containers</div>
@@ -102,7 +80,7 @@
</rd-widget>
</a>
</div>
<div class="col-xs-12 col-md-6">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="images">
<rd-widget>
<rd-widget-body>
@@ -110,7 +88,7 @@
<i class="fa fa-clone"></i>
</div>
<div class="pull-right">
<div><i class="fa fa-pie-chart space-right"></i>{{ imageData.size|humansize }}</div>
<div><i class="fa fa-pie-chart text-icon"></i>{{ imageData.size|humansize }}</div>
</div>
<div class="title">{{ imageData.total }}</div>
<div class="comment">Images</div>
@@ -118,7 +96,7 @@
</rd-widget>
</a>
</div>
<div class="col-xs-12 col-md-6">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="volumes">
<rd-widget>
<rd-widget-body>
@@ -126,7 +104,7 @@
<i class="fa fa-cubes"></i>
</div>
<div class="pull-right" ng-if="infoData.Driver">
<div><i class="fa fa-hdd-o space-right"></i>{{ infoData.Driver }} driver</div>
<div><i class="fa fa-hdd-o text-icon"></i>{{ infoData.Driver }} driver</div>
</div>
<div class="title">{{ volumeData.total }}</div>
<div class="comment">Volumes</div>
@@ -134,7 +112,7 @@
</rd-widget>
</a>
</div>
<div class="col-xs-12 col-md-6">
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<a ui-sref="networks">
<rd-widget>
<rd-widget-body>
+25 -11
View File
@@ -1,6 +1,6 @@
angular.module('dashboard', [])
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Messages',
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info, Messages) {
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'Image', 'Network', 'Volume', 'Info',
function ($scope, $q, Config, Container, Image, Network, Volume, Info) {
$scope.containerData = {
total: 0
@@ -15,13 +15,13 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
total: 0
};
function prepareContainerData(d, containersToHideLabels) {
function prepareContainerData(d) {
var running = 0;
var stopped = 0;
var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
if (hiddenLabels) {
containers = hideContainers(d);
}
for (var i = 0; i < containers.length; i++) {
@@ -65,7 +65,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
$scope.infoData = info;
}
function fetchDashboardData(containersToHideLabels) {
function fetchDashboardData() {
$('#loadingViewSpinner').show();
$q.all([
Container.query({all: 1}).$promise,
@@ -74,19 +74,33 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
Network.query({}).$promise,
Info.get({}).$promise
]).then(function (d) {
prepareContainerData(d[0], containersToHideLabels);
prepareContainerData(d[0]);
prepareImageData(d[1]);
prepareVolumeData(d[2]);
prepareNetworkData(d[3]);
prepareInfoData(d[4]);
$('#loadingViewSpinner').hide();
}, function(e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to load dashboard data");
});
}
var hideContainers = function (containers) {
return containers.filter(function (container) {
var filterContainer = false;
hiddenLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
Config.$promise.then(function (c) {
fetchDashboardData(c.hiddenLabels);
$scope.swarm = c.swarm;
hiddenLabels = c.hiddenLabels;
fetchDashboardData();
});
}]);
@@ -1,16 +1,15 @@
angular.module('main', [])
.controller('MainController', ['$scope', '$cookieStore', 'StateManager',
function ($scope, $cookieStore, StateManager) {
angular.module('dashboard')
.controller('MasterCtrl', ['$scope', '$cookieStore', 'Settings', 'Config', function ($scope, $cookieStore, Settings, Config) {
/**
* Sidebar Toggle & Cookie Control
*/
var mobileView = 992;
$scope.getWidth = function() {
return window.innerWidth;
};
$scope.applicationState = StateManager.getState();
$scope.config = Config;
$scope.$watch($scope.getWidth, function(newValue, oldValue) {
if (newValue >= mobileView) {
@@ -33,4 +32,6 @@ function ($scope, $cookieStore, StateManager) {
window.onresize = function() {
$scope.$apply();
};
$scope.uiVersion = Settings.uiVersion;
}]);
+103 -77
View File
@@ -3,113 +3,139 @@
<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" ng-if="state.loaded">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row">
<div class="col-lg-3 col-md-6 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-code" title="Engine version"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Version</td>
<td>{{ version.Version }}</td>
</tr>
<tr>
<td>API version</td>
<td>{{ version.ApiVersion }}</td>
</tr>
<tr>
<td>Go version</td>
<td>{{ version.GoVersion }}</td>
</tr>
<tr>
<td>OS type</td>
<td>{{ version.Os }}</td>
</tr>
<tr>
<td>OS</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr>
<td>Architecture</td>
<td>{{ version.Arch }}</td>
</tr>
<tr>
<td>Kernel version</td>
<td>{{ version.KernelVersion }}</td>
</tr>
</tbody>
</table>
<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" ng-if="state.loaded">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row">
<div class="col-lg-12">
<rd-widget>
<rd-widget-header icon="fa-th" title="Engine status"></rd-widget-header>
<rd-widget-header icon="fa-object-group" title="Engine status"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Total CPU</td>
<td>Containers</td>
<td>{{ info.Containers }}</td>
</tr>
<tr>
<td>Images</td>
<td>{{ info.Images }}</td>
</tr>
<tr>
<td>Debug</td>
<td>{{ info.Debug }}</td>
</tr>
<tr>
<td>CPUs</td>
<td>{{ info.NCPU }}</td>
</tr>
<tr>
<td>Total memory</td>
<td>Total Memory</td>
<td>{{ info.MemTotal|humansize }}</td>
</tr>
<tr>
<td>Docker root directory</td>
<td>{{ info.DockerRootDir }}</td>
<td>Operating System</td>
<td>{{ info.OperatingSystem }}</td>
</tr>
<tr>
<td>Storage driver</td>
<td>Kernel Version</td>
<td>{{ info.KernelVersion }}</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>Logging driver</td>
<td>{{ info.LoggingDriver }}</td>
<td>Storage Driver Status</td>
<td>
<p ng-repeat="val in info.DriverStatus">
{{ val[0] }}: {{ val[1] }}
</p>
</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>
<tr>
<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>
<td>IPv4 Forwarding</td>
<td>{{ info.IPv4Forwarding }}</td>
</tr>
<tr ng-if="info.Plugins.Network">
<td>Network</td>
<td>{{ info.Plugins.Network|arraytostr: ', '}}</td>
<tr>
<td>Index Server Address</td>
<td>{{ info.IndexServerAddress }}</td>
</tr>
<tr ng-if="info.Plugins.Authorization">
<td>Authorization</td>
<td>{{ info.Plugins.Authorization|arraytostr: ', '}}</td>
<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>
</tr>
</tbody>
</table>
+15 -20
View File
@@ -1,24 +1,19 @@
angular.module('docker', [])
.controller('DockerController', ['$scope', 'Info', 'Version', 'Messages',
function ($scope, Info, Version, Messages) {
$scope.state = {
loaded: false
};
$scope.info = {};
$scope.version = {};
.controller('DockerController', ['$scope', 'Info', 'Version', 'Settings',
function ($scope, Info, Version, Settings) {
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();
$scope.info = {};
$scope.docker = {};
$scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
Version.get({}, function (d) {
$scope.docker = d;
});
Info.get({}, function (d) {
$scope.info = d;
});
}]);
-107
View File
@@ -1,107 +0,0 @@
<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-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-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-3 col-lg-2 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-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'">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="endpoint.TLS"><i></i>
</label>
</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>
@@ -1,65 +0,0 @@
angular.module('endpoint', [])
.controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Messages',
function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
if (!$scope.applicationState.application.endpointManagement) {
$state.go('endpoints');
}
$scope.state = {
error: '',
uploadInProgress: false
};
$scope.formValues = {
TLSCACert: null,
TLSCert: null,
TLSKey: null
};
$scope.updateEndpoint = function() {
var ID = $scope.endpoint.Id;
var endpointParams = {
name: $scope.endpoint.Name,
URL: $scope.endpoint.URL,
TLS: $scope.endpoint.TLS,
TLSCACert: $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null,
TLSCert: $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null,
TLSKey: $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null,
type: $scope.endpointType
};
EndpointService.updateEndpoint(ID, endpointParams)
.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);
}]);
@@ -1,177 +0,0 @@
<rd-header>
<rd-header-title title="Endpoint access">
<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> > Access management
</rd-header-content>
</rd-header>
<div class="row" ng-if="endpoint">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-plug" title="Endpoint"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>Name</td>
<td>
{{ endpoint.Name }}
</td>
</tr>
<tr>
<td>URL</td>
<td>
{{ endpoint.URL | stripprotocol }}
</td>
</tr>
<tr>
<td colspan="2">
<span class="small text-muted">
You can select which user can access this endpoint by moving them to the authorized users table. Simply click
on a user entry to move it from one table to the other.
</span>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="endpoint">
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="state.pagination_count_users" ng-change="changePaginationCountUsers()">
<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-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="authorizeAllUsers()" ng-disabled="users.length === 0 || filteredUsers.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all users</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="state.filterUsers" 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>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderUsers('Username')">
Name
<span ng-show="sortTypeUsers == 'Username' && !sortReverseUsers" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeUsers == 'Username' && sortReverseUsers" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderUsers('Role')">
Role
<span ng-show="sortTypeUsers == 'Role' && !sortReverseUsers" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeUsers == 'Role' && sortReverseUsers" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="authorizeUser(user)" class="interactive" dir-paginate="user in (state.filteredUsers = (users | filter:state.filterUsers | orderBy:sortTypeUsers:sortReverseUsers | itemsPerPage: state.pagination_count_users))">
<td>{{ user.Username }}</td>
<td>
{{ user.RoleName }}
<i class="fa" ng-class="user.RoleId === 1 ? 'fa-user-circle-o' : 'fa-user'" aria-hidden="true" style="margin-left: 2px;"></i>
</td>
</tr>
<tr ng-if="!users">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="users.length === 0 || state.filteredUsers.length === 0">
<td colspan="2" class="text-center text-muted">No users.</td>
</tr>
</tbody>
</table>
<div ng-if="users" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-sm-6">
<rd-widget>
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users">
<div class="pull-md-right pull-lg-right">
Items per page:
<select ng-model="state.pagination_count_authorizedUsers" ng-change="changePaginationCountAuthorizedUsers()">
<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-sm-12 nopadding">
<div class="col-sm-12 col-md-6 nopadding">
<button class="btn btn-primary btn-sm" ng-click="unauthorizeAllUsers()" ng-disabled="authorizedUsers.length === 0 || filteredAuthorizedUsers.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all users</button>
</div>
<div class="col-sm-12 col-md-6 nopadding">
<input type="text" id="filter" ng-model="state.filterAuthorizedUsers" 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>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAuthorizedUsers('Username')">
Name
<span ng-show="sortTypeAuthorizedUsers == 'Username' && !sortReverseAuthorizedUsers" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeAuthorizedUsers == 'Username' && sortReverseAuthorizedUsers" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="endpoint.access({id: endpoint.Id})" ng-click="orderAuthorizedUsers('Role')">
Role
<span ng-show="sortTypeAuthorizedUsers == 'Role' && !sortReverseAuthorizedUsers" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortTypeAuthorizedUsers == 'Role' && sortReverseAuthorizedUsers" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr ng-click="unauthorizeUser(user)" class="interactive" dir-paginate="user in (state.filteredAuthorizedUsers = (authorizedUsers | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedUsers:sortReverseAuthorizedUsers | itemsPerPage: state.pagination_count_authorizedUsers))">
<td>{{ user.Username }}</td>
<td>
{{ user.RoleName }}
<i class="fa" ng-class="user.RoleId === 1 ? 'fa-user-circle-o' : 'fa-user'" aria-hidden="true" style="margin-left: 2px;"></i>
</td>
</tr>
<tr ng-if="!authorizedUsers">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="authorizedUsers.length === 0 || state.filteredAuthorizedUsers.length === 0">
<td colspan="2" class="text-center text-muted">No authorized users.</td>
</tr>
</tbody>
</table>
<div ng-if="authorizedUsers" class="pull-left pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
@@ -1,148 +0,0 @@
angular.module('endpointAccess', [])
.controller('EndpointAccessController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'UserService', 'Pagination', 'Messages',
function ($q, $scope, $state, $stateParams, $filter, EndpointService, UserService, Pagination, Messages) {
$scope.state = {
pagination_count_users: Pagination.getPaginationCount('endpoint_access_users'),
pagination_count_authorizedUsers: Pagination.getPaginationCount('endpoint_access_authorizedUsers')
};
$scope.sortTypeUsers = 'Username';
$scope.sortReverseUsers = true;
$scope.orderUsers = function(sortType) {
$scope.sortReverseUsers = ($scope.sortTypeUsers === sortType) ? !$scope.sortReverseUsers : false;
$scope.sortTypeUsers = sortType;
};
$scope.changePaginationCountUsers = function() {
Pagination.setPaginationCount('endpoint_access_users', $scope.state.pagination_count_users);
};
$scope.sortTypeAuthorizedUsers = 'Username';
$scope.sortReverseAuthorizedUsers = true;
$scope.orderAuthorizedUsers = function(sortType) {
$scope.sortReverseAuthorizedUsers = ($scope.sortTypeAuthorizedUsers === sortType) ? !$scope.sortReverseAuthorizedUsers : false;
$scope.sortTypeAuthorizedUsers = sortType;
};
$scope.changePaginationCountAuthorizedUsers = function() {
Pagination.setPaginationCount('endpoint_access_authorizedUsers', $scope.state.pagination_count_authorizedUsers);
};
$scope.authorizeAllUsers = function() {
var authorizedUserIDs = [];
angular.forEach($scope.authorizedUsers, function (user) {
authorizedUserIDs.push(user.Id);
});
angular.forEach($scope.users, function (user) {
authorizedUserIDs.push(user.Id);
});
EndpointService.updateAuthorizedUsers($stateParams.id, authorizedUserIDs)
.then(function success(data) {
$scope.authorizedUsers = $scope.authorizedUsers.concat($scope.users);
$scope.users = [];
Messages.send('Access granted for all users');
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to update endpoint permissions");
});
};
$scope.unauthorizeAllUsers = function() {
EndpointService.updateAuthorizedUsers($stateParams.id, [])
.then(function success(data) {
$scope.users = $scope.users.concat($scope.authorizedUsers);
$scope.authorizedUsers = [];
Messages.send('Access removed for all users');
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to update endpoint permissions");
});
};
$scope.authorizeUser = function(user) {
var authorizedUserIDs = [];
angular.forEach($scope.authorizedUsers, function (u) {
authorizedUserIDs.push(u.Id);
});
authorizedUserIDs.push(user.Id);
EndpointService.updateAuthorizedUsers($stateParams.id, authorizedUserIDs)
.then(function success(data) {
removeUserFromArray(user.Id, $scope.users);
$scope.authorizedUsers.push(user);
Messages.send('Access granted for user', user.Username);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to update endpoint permissions");
});
};
$scope.unauthorizeUser = function(user) {
var authorizedUserIDs = $scope.authorizedUsers.filter(function (u) {
if (u.Id !== user.Id) {
return u;
}
}).map(function (u) {
return u.Id;
});
EndpointService.updateAuthorizedUsers($stateParams.id, authorizedUserIDs)
.then(function success(data) {
removeUserFromArray(user.Id, $scope.authorizedUsers);
$scope.users.push(user);
Messages.send('Access removed for user', user.Username);
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to update endpoint permissions");
});
};
function getEndpointAndUsers(endpointID) {
$('#loadingViewSpinner').show();
$q.all({
endpoint: EndpointService.endpoint($stateParams.id),
users: UserService.users(),
})
.then(function success(data) {
$scope.endpoint = data.endpoint;
$scope.users = data.users.filter(function (user) {
if (user.Role !== 1) {
return user;
}
}).map(function (user) {
return new UserViewModel(user);
});
$scope.authorizedUsers = [];
angular.forEach($scope.endpoint.AuthorizedUsers, function(userID) {
for (var i = 0, l = $scope.users.length; i < l; i++) {
if ($scope.users[i].Id === userID) {
$scope.authorizedUsers.push($scope.users[i]);
$scope.users.splice(i, 1);
return;
}
}
});
})
.catch(function error(err) {
$scope.templates = [];
$scope.users = [];
$scope.authorizedUsers = [];
Messages.error("Failure", err, "Unable to retrieve endpoint details");
})
.finally(function final(){
$('#loadingViewSpinner').hide();
});
}
function removeUserFromArray(id, users) {
for (var i = 0, l = users.length; i < l; i++) {
if (users[i].Id === id) {
users.splice(i, 1);
return;
}
}
}
getEndpointAndUsers($stateParams.id);
}]);
@@ -1,153 +0,0 @@
<div class="page-wrapper">
<!-- simple box -->
<div class="container simple-box">
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
<!-- 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" style="text-align: center;">
<h4>Connect Portainer to a Docker engine or Swarm cluster endpoint</h4>
</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 yet available for native Docker Windows containers.</span>
<div class="small text-primary">On Linux and when using Docker for Mac or Docker for Windows or Docker Toolbox, 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-4 col-lg-3 control-label text-left">Name</label>
<div class="col-sm-8 col-lg-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-4 col-lg-3 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-8 col-lg-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">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLS"><i></i>
</label>
</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>
@@ -1,90 +0,0 @@
angular.module('endpointInit', [])
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Messages',
function ($scope, $state, EndpointService, StateManager, EndpointProvider, 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) {
var endpointID = data.Id;
EndpointProvider.setEndpointID(endpointID);
StateManager.updateEndpointState(false).then(
function success() {
$state.go('dashboard');
},
function error(err) {
EndpointService.deleteEndpoint(endpointID)
.then(function success() {
$scope.state.error = 'Unable to connect to the Docker endpoint';
});
});
},
function error() {
$scope.state.error = 'Unable to create endpoint';
})
.finally(function final() {
$('#initEndpointSpinner').hide();
});
};
$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) {
var endpointID = data.Id;
EndpointProvider.setEndpointID(endpointID);
StateManager.updateEndpointState(false)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
EndpointService.deleteEndpoint(endpointID)
.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;
}
});
};
}]);
-202
View File
@@ -1,202 +0,0 @@
<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" ng-if="!applicationState.application.endpointManagement">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title="Endpoint management is not available">
</rd-widget-header>
<rd-widget-body>
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag. Endpoint management via the UI is disabled. You can still manage endpoint access.</span>
</rd-wigdet-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="applicationState.application.endpointManagement">
<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-3 col-lg-2 control-label text-left">Name</label>
<div class="col-sm-9 col-lg-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-3 col-lg-2 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-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">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLS"><i></i>
</label>
</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" ng-if="applicationState.application.endpointManagement">
<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 ng-if="applicationState.application.endpointManagement">
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</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></th>
</tr>
</thead>
<tbody>
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td ng-if="applicationState.application.endpointManagement"><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>
<span ng-if="applicationState.application.endpointManagement">
<span ng-if="endpoint.Id !== activeEndpointID">
<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 === activeEndpointID">
<i class="fa fa-lock" aria-hidden="true"></i> You cannot edit the active endpoint
</span>
</span>
<span ng-if="applicationState.application.authentication">
<a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
</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>
@@ -1,111 +0,0 @@
angular.module('endpoints', [])
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Messages', 'Pagination',
function ($scope, $state, EndpointService, EndpointProvider, 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.selectItems = function (allSelected) {
angular.forEach($scope.state.filteredEndpoints, function (endpoint) {
if (endpoint.Checked !== allSelected) {
endpoint.Checked = allSelected;
$scope.selectItem(endpoint);
}
});
};
$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;
$scope.activeEndpointID = EndpointProvider.endpointID();
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to retrieve endpoints");
$scope.endpoints = [];
})
.finally(function final() {
$('#loadEndpointsSpinner').hide();
});
}
fetchEndpoints();
}]);
+2 -13
View File
@@ -3,7 +3,6 @@
<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>
@@ -13,14 +12,7 @@
<rd-widget>
<rd-widget-header icon="fa-history" title="Events">
<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>
<i id="loadEventsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
</div>
</rd-widget-header>
<rd-widget-taskbar classes="col-lg-12">
@@ -57,16 +49,13 @@
</tr>
</thead>
<tbody>
<tr dir-paginate="event in (events | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<tr ng-repeat="event in (events | filter:state.filter | orderBy:sortType:sortReverse)">
<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>
+3 -8
View File
@@ -1,8 +1,7 @@
angular.module('events', [])
.controller('EventsController', ['$scope', 'Messages', 'Events', 'Pagination',
function ($scope, Messages, Events, Pagination) {
.controller('EventsController', ['$scope', 'Settings', 'Messages', 'Events',
function ($scope, Settings, Messages, Events) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('events');
$scope.sortType = 'Time';
$scope.sortReverse = true;
@@ -11,10 +10,6 @@ function ($scope, Messages, Events, Pagination) {
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('events', $scope.state.pagination_count);
};
var from = moment().subtract(24, 'hour').unix();
var to = moment().unix();
@@ -26,7 +21,7 @@ function ($scope, Messages, Events, Pagination) {
$('#loadEventsSpinner').hide();
},
function (e) {
Messages.error("Unable to load events", e.data);
$('#loadEventsSpinner').hide();
Messages.error("Failure", e, "Unable to load events");
});
}]);
+40 -54
View File
@@ -3,46 +3,32 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="images">Images</a> > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
Images > <a ui-sref="image({id: image.Id})">{{ image.Id }}</a>
</rd-header-content>
</rd-header>
<div class="row" ng-if="image.RepoTags.length > 0">
<div class="row" ng-if="RepoTags.length > 0">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa fa-tags" title="Image tags"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="row">
<div class="pull-left" ng-repeat="tag in image.RepoTags" style="display:table">
<div class="input-group col-md-1" style="padding:0 15px">
<span class="input-group-addon">{{ tag }}</span>
<span class="input-group-btn">
<a data-toggle="tooltip" class="btn btn-primary interactive" title="Push to registry" ng-click="pushImage(tag)">
<span class="fa fa-upload white-icon" aria-hidden="true"></span>
</a>
<a data-toggle="tooltip" class="btn btn-primary interactive" title="Pull from registry" ng-click="pullImage(tag)">
<span class="fa fa-download white-icon" aria-hidden="true"></span>
</a>
<a data-toggle="tooltip" class="btn btn-primary interactive" title="Remove tag" ng-click="removeTag(tag)">
<span class="fa fa-trash-o white-icon" aria-hidden="true"></span>
</a>
</span>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
Note: you can click on the upload icon <span class="fa fa-upload" aria-hidden="true"></span> to push an image
or on the download icon <span class="fa fa-download" aria-hidden="true"></span> to pull an image
or on the trash icon <span class="fa fa-trash-o" aria-hidden="true"></span> to delete a tag.
</span>
</div>
</div>
</form>
<rd-widget-body classes="no-padding">
<div style="margin: 5px 10px;">
<span ng-repeat="tag in RepoTags" class="label label-primary image-tag">
<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>
{{ tag }}
<a data-toggle="tooltip" class="interactive" title="Remove tag" ng-click="removeImage(tag)">
<i class="fa fa-trash-o white-icon" aria-hidden="true"></i>
</a>
</span>
</div>
<div style="margin: 5px 10px;">
<span class="small text-muted">
Note: you can click on the upload icon to push an image
and on the trash icon to delete a tag
</span>
</div>
</rd-widget-body>
</rd-widget>
</div>
@@ -57,15 +43,15 @@
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6">
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in formValues.AvailableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-and-registry-inputs -->
@@ -78,7 +64,7 @@
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="tagImage()">Tag</button>
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="tagImage()">Tag</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
@@ -99,7 +85,7 @@
<td>ID</td>
<td>
{{ image.Id }}
<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>
<button class="btn btn-xs btn-danger" ng-click="removeImage(image.Id)">Delete this image</button>
</td>
</tr>
<tr ng-if="image.Parent">
@@ -138,35 +124,35 @@
<tbody>
<tr>
<td>CMD</td>
<td><code>{{ image.Command|command }}</code></td>
<td><code>{{ image.ContainerConfig.Cmd|command }}</code></td>
</tr>
<tr ng-if="image.Entrypoint">
<tr ng-if="image.ContainerConfig.Entrypoint">
<td>ENTRYPOINT</td>
<td><code>{{ image.Entrypoint|command }}</code></td>
<td><code>{{ image.ContainerConfig.Entrypoint|command }}</code></td>
</tr>
<tr ng-if="image.ExposedPorts.length > 0">
<tr ng-if="image.ContainerConfig.ExposedPorts">
<td>EXPOSE</td>
<td>
<span class="label label-default space-right" ng-repeat="port in image.ExposedPorts">
<span class="label label-default tag" ng-repeat="port in exposedPorts">
{{ port }}
</span>
</td>
</tr>
<tr ng-if="image.Volumes.length > 0">
<tr ng-if="image.ContainerConfig.Volumes">
<td>VOLUME</td>
<td>
<span class="label label-default space-right" ng-repeat="volume in image.Volumes">
<span class="label label-default tag" ng-repeat="volume in volumes">
{{ volume }}
</span>
</td>
</tr>
<tr ng-if="image.Env.length > 0">
<tr>
<td>ENV</td>
<td>
<table class="table table-bordered table-condensed">
<tr ng-repeat="var in image.Env">
<td>{{ var|key: '=' }}</td>
<td>{{ var|value: '=' }}</td>
<tr ng-repeat="var in image.ContainerConfig.Env">
<td>{{ var|key }}</td>
<td>{{ var|value }}</td>
</tr>
</table>
</td>
+104 -98
View File
@@ -1,109 +1,115 @@
angular.module('image', [])
.controller('ImageController', ['$scope', '$stateParams', '$state', 'ImageService', 'Messages',
function ($scope, $stateParams, $state, ImageService, Messages) {
$scope.config = {
Image: '',
Registry: ''
};
.controller('ImageController', ['$scope', '$stateParams', '$state', 'Config', 'Image', 'Messages',
function ($scope, $stateParams, $state, Config, Image, Messages) {
$scope.RepoTags = [];
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
$scope.config = {
Image: '',
Registry: ''
};
ImageService.tagImage($stateParams.id, image, registry)
.then(function success(data) {
Messages.send('Image successfully tagged');
$state.go('image', {id: $stateParams.id}, {reload: true});
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to tag image");
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
$scope.formValues = {
AvailableRegistries: []
};
$scope.pushImage = function(tag) {
$('#loadingViewSpinner').show();
ImageService.pushImage(tag)
.then(function success() {
Messages.send('Image successfully pushed');
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to push image tag");
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
// Get RepoTags from the /images/query endpoint instead of /image/json,
// for backwards compatibility with Docker API versions older than 1.21
function getRepoTags(imageId) {
Image.query({}, function (d) {
d.forEach(function(image) {
if (image.Id === imageId && image.RepoTags[0] !== '<none>:<none>') {
$scope.RepoTags = image.RepoTags;
}
});
});
}
$scope.pullImage = function(tag) {
$('#loadingViewSpinner').show();
var image = $scope.config.Image;
var registry = $scope.config.Registry;
function createImageConfig(imageName, registry) {
var imageNameAndTag = imageName.split(':');
var image = imageNameAndTag[0];
if (registry) {
image = registry + '/' + imageNameAndTag[0];
}
var imageConfig = {
repo: image,
tag: imageNameAndTag[1] ? imageNameAndTag[1] : 'latest'
};
return imageConfig;
}
ImageService.pullImage(image, registry)
.then(function success(data) {
Messages.send('Image successfully pulled', image);
})
.catch(function error(err){
Messages.error("Failure", err, "Unable to pull image");
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
$scope.tagImage = function() {
$('#loadingViewSpinner').show();
var image = _.toLower($scope.config.Image);
var registry = _.toLower($scope.config.Registry);
var imageConfig = createImageConfig(image, registry);
Image.tag({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
Messages.send('Image successfully tagged');
$('#loadingViewSpinner').hide();
$state.go('image', {id: $stateParams.id}, {reload: true});
}, function(e) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to tag image", e.data);
});
};
$scope.removeTag = function(id) {
$('#loadingViewSpinner').show();
ImageService.deleteImage(id, false)
.then(function success() {
if ($scope.image.RepoTags.length === 1) {
Messages.send('Image successfully deleted', id);
$state.go('images', {}, {reload: true});
} else {
Messages.send('Tag successfully deleted', id);
$state.go('image', {id: $stateParams.id}, {reload: true});
}
})
.catch(function error(err) {
Messages.error("Failure", err, 'Unable to remove image');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
$scope.pushImage = function(tag) {
$('#loadingViewSpinner').show();
Image.push({tag: tag}, function (d) {
if (d[d.length-1].error) {
Messages.error("Unable to push image", d[d.length-1].error);
} else {
Messages.send('Image successfully pushed');
}
$('#loadingViewSpinner').hide();
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to push image", e.data);
});
};
$scope.removeImage = function (id) {
$('#loadingViewSpinner').show();
ImageService.deleteImage(id, false)
.then(function success() {
Messages.send('Image successfully deleted', id);
$state.go('images', {}, {reload: true});
})
.catch(function error(err) {
Messages.error("Failure", err, 'Unable to remove image');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
};
$scope.removeImage = function (id) {
$('#loadingViewSpinner').show();
Image.remove({id: id}, function (d) {
if (d[0].message) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", d[0].message);
} else {
// If last message key is 'Deleted' or if it's 'Untagged' and there is only one tag associated to the image
// then assume the image is gone and send to images page
if (d[d.length-1].Deleted || (d[d.length-1].Untagged && $scope.RepoTags.length === 1)) {
Messages.send('Image successfully deleted');
$state.go('images', {}, {reload: true});
} else {
Messages.send('Tag successfully deleted');
$state.go('image', {id: $stateParams.id}, {reload: true});
}
}
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", e.data);
});
};
function retrieveImageDetails() {
$('#loadingViewSpinner').show();
ImageService.image($stateParams.id)
.then(function success(data) {
$scope.image = data;
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to retrieve image details");
$state.go('images');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
Config.$promise.then(function (c) {
$scope.formValues.AvailableRegistries = c.registries;
retrieveImageDetails();
$('#loadingViewSpinner').show();
Image.get({id: $stateParams.id}, function (d) {
$scope.image = d;
if (d.RepoTags) {
$scope.RepoTags = d.RepoTags;
} else {
getRepoTags(d.Id);
}
$('#loadingViewSpinner').hide();
$scope.exposedPorts = d.ContainerConfig.ExposedPorts ? Object.keys(d.ContainerConfig.ExposedPorts) : [];
$scope.volumes = d.ContainerConfig.Volumes ? Object.keys(d.ContainerConfig.Volumes) : [];
}, function (e) {
if (e.status === 404) {
Messages.error("Unable to find image", $stateParams.id);
} else {
Messages.error("Unable to retrieve image info", e.data);
}
});
});
}]);
+15 -51
View File
@@ -3,7 +3,6 @@
<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>
@@ -18,15 +17,15 @@
<!-- name-and-registry-inputs -->
<div class="form-group">
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-6">
<div class="col-sm-7">
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. ubuntu:trusty">
</div>
<label for="image_registry" class="col-sm-2 margin-sm-top control-label text-left">
Registry
<portainer-tooltip position="bottom" message="A registry to pull the image from. Leave empty to use the official Docker registry."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-3 margin-sm-top">
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="e.g. myregistry.mydomain">
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
<div class="col-sm-3">
<select class="selectpicker form-control" ng-model="config.Registry">
<option value="">Docker Hub</option>
<option ng-repeat="registry in availableRegistries" ng-value="registry.value">{{ registry.name }}</option>
</select>
</div>
</div>
<!-- !name-and-registry-inputs -->
@@ -39,7 +38,7 @@
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="pullImage()">Pull</button>
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="pullImage()">Pull</button>
<i id="pullImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
@@ -54,28 +53,12 @@
<rd-widget>
<rd-widget-header icon="fa-clone" title="Images">
<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>
<i id="loadImagesSpinner" 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">
<div class="btn-group">
<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>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" ng-disabled="!state.selectedItemCount" >
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="confirmRemovalAction(true)" ng-disabled="!state.selectedItemCount" >Force Remove</a></li>
</ul>
</div>
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount">Remove</button>
</div>
<div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@@ -86,9 +69,7 @@
<table class="table table-hover">
<thead>
<tr>
<th>
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th></th>
<th>
<a ui-sref="images" ng-click="order('Id')">
Id
@@ -110,38 +91,21 @@
<span ng-show="sortType == 'VirtualSize' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="images" ng-click="order('Created')">
Created
<span ng-show="sortType == 'Created' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Created' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<tr ng-repeat="image in (state.filteredImages = (images | filter:state.filter | orderBy:sortType:sortReverse))">
<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>
<span class="label label-primary image-tag" ng-repeat="tag in (image|repotags)">{{ tag }}</span>
</td>
<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>
</div>
</div>
<rd-widget>
</div>
</div>
+54 -55
View File
@@ -1,8 +1,7 @@
angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Config', 'ImageService', 'Messages', 'Pagination', 'ModalService',
function ($scope, $state, Config, ImageService, Messages, Pagination, ModalService) {
.controller('ImagesController', ['$scope', '$state', 'Config', 'Image', 'Messages',
function ($scope, $state, Config, Image, Messages) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('images');
$scope.sortType = 'RepoTags';
$scope.sortReverse = true;
$scope.state.selectedItemCount = 0;
@@ -17,19 +16,6 @@ function ($scope, $state, Config, ImageService, Messages, Pagination, ModalServi
$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++;
@@ -38,31 +24,41 @@ function ($scope, $state, Config, ImageService, Messages, Pagination, ModalServi
}
};
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 = $scope.config.Image;
var image = _.toLower($scope.config.Image);
var registry = $scope.config.Registry;
ImageService.pullImage(image, registry)
.then(function success(data) {
$state.reload();
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to pull image");
})
.finally(function final() {
var imageConfig = createImageConfig(image, registry);
Image.create(imageConfig, function (data) {
var err = data.length > 0 && data[data.length - 1].hasOwnProperty('error');
if (err) {
var detail = data[data.length - 1];
$('#pullImageSpinner').hide();
Messages.error('Error', detail.error);
} else {
$('#pullImageSpinner').hide();
$state.go('images', {}, {reload: true});
}
}, function (e) {
$('#pullImageSpinner').hide();
Messages.error('Error', 'Unable to pull image ' + image);
});
};
$scope.confirmRemovalAction = function (force) {
ModalService.confirmImageForceRemoval(function (confirmed) {
if(!confirmed) { return; }
$scope.removeAction(force);
});
};
$scope.removeAction = function (force) {
force = !!force;
$scope.removeAction = function () {
$('#loadImagesSpinner').show();
var counter = 0;
var complete = function () {
@@ -74,16 +70,18 @@ function ($scope, $state, Config, ImageService, Messages, Pagination, ModalServi
angular.forEach($scope.images, function (i) {
if (i.Checked) {
counter = counter + 1;
ImageService.deleteImage(i.Id, force)
.then(function success(data) {
Messages.send("Image deleted", i.Id);
var index = $scope.images.indexOf(i);
$scope.images.splice(index, 1);
})
.catch(function error(err) {
Messages.error("Failure", err, 'Unable to remove image');
})
.finally(function final() {
Image.remove({id: i.Id}, function (d) {
if (d[0].message) {
$('#loadingViewSpinner').hide();
Messages.error("Unable to remove image", d[0].message);
} else {
Messages.send("Image deleted", i.Id);
var index = $scope.images.indexOf(i);
$scope.images.splice(index, 1);
}
complete();
}, function (e) {
Messages.error("Unable to remove image", e.data);
complete();
});
}
@@ -91,19 +89,20 @@ function ($scope, $state, Config, ImageService, Messages, Pagination, ModalServi
};
function fetchImages() {
$('#loadImagesSpinner').show();
ImageService.images()
.then(function success(data) {
$scope.images = data;
})
.catch(function error(err) {
Messages.error("Failure", err, "Unable to retrieve images");
$scope.images = [];
})
.finally(function final() {
Image.query({}, function (d) {
$scope.images = d.map(function (item) {
return new ImageViewModel(item);
});
$('#loadImagesSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadImagesSpinner').hide();
});
}
fetchImages();
Config.$promise.then(function (c) {
$scope.availableRegistries = c.registries;
fetchImages();
});
}]);
+97 -66
View File
@@ -3,91 +3,122 @@
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="networks">Networks</a> > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
Networks > <a ui-sref="network({id: network.Id})">{{ network.Name }}</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<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">
<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>Name</td>
<td>{{ network.Name }}</td>
<td>Id</td>
<td>{{ network.Id }}</td>
</tr>
<tr>
<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>
<td>Scope</td>
<td>{{ network.Scope }}</td>
</tr>
<tr>
<td>Driver</td>
<td>{{ network.Driver }}</td>
</tr>
<tr>
<td>Scope</td>
<td>{{ network.Scope }}</td>
</tr>
<tr ng-if="network.IPAM.Config[0].Subnet">
<td>Subnet</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
</tr>
<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>IPAM</td>
<td>
<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>
<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>
</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>
<tr>
<td>Options</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>
</td>
</tr>
</tbody>
+26 -89
View File
@@ -1,100 +1,37 @@
angular.module('network', [])
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Messages',
function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Messages) {
.controller('NetworkController', ['$scope', 'Network', 'Messages', '$state', '$stateParams', 'errorMsgFilter',
function ($scope, Network, Messages, $state, $stateParams, errorMsgFilter) {
$scope.removeNetwork = function removeNetwork(networkId) {
$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.data);
});
};
$scope.remove = function remove(networkId) {
$('#loadingViewSpinner').show();
Network.remove({id: $stateParams.id}, function (d) {
if (d.message) {
$('#loadingViewSpinner').hide();
Messages.send("Error", {}, d.message);
} else {
$('#loadingViewSpinner').hide();
Messages.send("Network removed", $stateParams.id);
$state.go('networks', {});
}
$('#loadingViewSpinner').hide();
Messages.send("Network removed", "");
$state.go('networks', {});
}, function (e) {
$('#loadingViewSpinner').hide();
Messages.error("Failure", e, "Unable to remove network");
Messages.error("Failure", e.data);
});
};
$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();
$('#loadingViewSpinner').show();
Network.get({id: $stateParams.id}, function (d) {
$scope.network = d;
$('#loadingViewSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadingViewSpinner').hide();
});
}]);
+90 -117
View File
@@ -3,15 +3,15 @@
<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="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 icon="fa-plus" title="Add a shared network">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
@@ -23,22 +23,39 @@
</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'">
<!-- advanced-settings-input -->
<div class="form-group">
<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 class="checkbox">
<label>
<input type="checkbox" ng-model="state.advancedSettings"> Show advanced settings
</label>
</div>
</div>
</div>
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
<!-- !advanced-settings-input -->
<!-- subnet-gateway-inputs -->
<div class="form-group" ng-if="state.advancedSettings">
<label for="network_subnet" class="col-sm-1 control-label text-left">Subnet</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Subnet" id="network_subnet" placeholder="e.g. 172.20.0.0/16">
</div>
<label for="network_gateway" class="col-sm-1 control-label text-left">Gateway</label>
<div class="col-sm-5">
<input type="text" class="form-control" ng-model="formValues.Gateway" id="network_gateway" placeholder="e.g. 172.20.10.11">
</div>
</div>
<!-- !subnet-gateway-inputs -->
<!-- tag-note -->
<div class="form-group" ng-if="swarm">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
<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>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
<button type="button" class="btn btn-primary btn-sm" ui-sref="actions.create.network">Advanced settings...</button>
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
@@ -48,112 +65,68 @@
</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 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>
</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('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('IPAM.Config[0].Subnet')">
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')">
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.Scope }}</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>
+57 -54
View File
@@ -1,49 +1,22 @@
angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'Pagination',
function ($scope, $state, Network, Config, Messages, Pagination) {
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Messages', 'errorMsgFilter',
function ($scope, $state, Network, Config, Messages, errorMsgFilter) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
$scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false;
$scope.sortType = 'Name';
$scope.sortType = 'Scope';
$scope.sortReverse = false;
$scope.formValues = {
Subnet: '',
Gateway: ''
};
$scope.config = {
Name: ''
};
$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'
};
Name: '',
IPAM: {
Config: []
}
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) {
@@ -51,15 +24,6 @@ function ($scope, $state, Network, Config, Messages, Pagination) {
$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++;
@@ -68,6 +32,44 @@ function ($scope, $state, Network, Config, Messages, Pagination) {
}
};
function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) {
var ipamConfig = {};
ipamConfig.Subnet = $scope.formValues.Subnet;
if ($scope.formValues.Gateway) {
ipamConfig.Gateway = $scope.formValues.Gateway ;
}
config.IPAM.Config.push(ipamConfig);
}
}
function prepareNetworkConfiguration() {
var config = angular.copy($scope.config);
prepareIPAMConfiguration(config);
if ($scope.swarm) {
config.Driver = 'overlay';
}
return config;
}
$scope.createNetwork = function() {
$('#createNetworkSpinner').show();
var config = prepareNetworkConfiguration();
Network.create(config, function (d) {
if (d.Id) {
Messages.send("Network created", d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
} else {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', errorMsgFilter(d));
}
}, function (e) {
$('#createNetworkSpinner').hide();
Messages.error('Unable to create network', e.data);
});
};
$scope.removeAction = function () {
$('#loadNetworksSpinner').show();
var counter = 0;
@@ -81,16 +83,17 @@ function ($scope, $state, Network, Config, Messages, Pagination) {
if (network.Checked) {
counter = counter + 1;
Network.remove({id: network.Id}, function (d) {
if (d.message) {
Messages.send("Error", d.message);
var error = errorMsgFilter(d);
if (error) {
Messages.send("Error", "Unable to remove network with active endpoints");
} else {
Messages.send("Network removed", network.Id);
Messages.send("Network deleted", network.Id);
var index = $scope.networks.indexOf(network);
$scope.networks.splice(index, 1);
}
complete();
}, function (e) {
Messages.error("Failure", e, 'Unable to remove network');
Messages.error("Failure", e.data);
complete();
});
}
@@ -103,13 +106,13 @@ function ($scope, $state, Network, Config, Messages, Pagination) {
$scope.networks = d;
$('#loadNetworksSpinner').hide();
}, function (e) {
Messages.error("Failure", e.data);
$('#loadNetworksSpinner').hide();
Messages.error("Failure", e, "Unable to retrieve networks");
$scope.networks = [];
});
}
Config.$promise.then(function (c) {
$scope.swarm = c.swarm;
fetchNetworks();
});
}]);
-273
View File
@@ -1,273 +0,0 @@
<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-sm btn-danger" type="button" ng-click="removeLabel(node, $index)">
<i class="fa fa-trash" 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
View File
@@ -1,112 +0,0 @@
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();
}]);
@@ -1,66 +0,0 @@
<div ng-if="service.ServiceConstraints">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Placement constraints">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating || addPlacementConstraint(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.ServiceConstraints.length === 0">
<p>There are no placement constraints for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.ServiceConstraints.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Name</th>
<th>Operator</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="constraint in service.ServiceConstraints">
<td>
<div class="input-group input-group-sm">
<input type="text" class="form-control" ng-model="constraint.key" placeholder="e.g. node.role" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<select name="constraintOperator" class="form-control" ng-model="constraint.operator" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
<option value="==">==</option>
<option value="!=">!=</option>
</select>
</div>
</td>
<td>
<div class="input-group input-group-sm">
<input type="text" class="form-control" ng-model="constraint.value" placeholder="e.g. manager" ng-change="updatePlacementConstraint(service, constraint)" ng-disabled="isUpdating">
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementConstraint(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" 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(service, ['ServiceConstraints'])" ng-click="updateService(service)">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(service, ['ServiceConstraints'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>
@@ -1,56 +0,0 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-list-alt" title="Container spec"></rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table">
<tbody>
<tr>
<td>CMD</td>
<td><code ng-if="service.Command">{{ service.Command|command }}</code></td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Command to execute.
</p>
</td>
</tr>
<tr>
<td>Args</td>
<td><code ng-if="service.Arguments">{{ service.Arguments }}</code></td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Arguments passed to command in container.
</p>
</td>
</tr>
<tr>
<td>User</td>
<td>{{ service.User }}</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Username or UID.
</p>
</td>
</tr>
<tr>
<td>Working directory</td>
<td>{{ service.Dir }}</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Working directory inside the container.
</p>
</td>
</tr>
<tr>
<td>Stop grace period</td>
<td>{{ service.StopGracePeriod }}</td>
<td>
<p class="small text-muted" style="margin-top: 10px;">
Time to wait before force killing a container (default none).
</p>
</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
@@ -1,59 +0,0 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Container labels">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addContainerLabel(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> container label
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.ServiceContainerLabels.length === 0">
<p>There are no container labels for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.ServiceContainerLabels.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Label</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="label in service.ServiceContainerLabels">
<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="updateContainerLabel(service, label)" ng-disabled="isUpdating">
</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="updateContainerLabel(service, label)" ng-disabled="isUpdating">
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeContainerLabel(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" 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(service, ['ServiceContainerLabels'])" ng-click="updateService(service)">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(service, ['ServiceContainerLabels'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>
@@ -1,59 +0,0 @@
<div ng-if="service.EnvironmentVariables">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Environment variables">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addEnvironmentVariable(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> environment variable
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.EnvironmentVariables.length === 0">
<p>There are no environment variables for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.EnvironmentVariables.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="var in service.EnvironmentVariables">
<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="var.key" ng-disabled="var.added || isUpdating" placeholder="e.g. FOO">
</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="var.value" ng-change="updateEnvironmentVariable(service, var)" placeholder="e.g. bar" ng-disabled="isUpdating">
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeEnvironmentVariable(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" 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(service, ['EnvironmentVariables'])" ng-click="updateService(service)">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(service, ['EnvironmentVariables'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>
@@ -1,67 +0,0 @@
<div ng-if="service.ServiceMounts">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Mounts">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addMount(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> mount
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="service.ServiceMounts.length === 0">
<p>There are no mounts for this service.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.ServiceMounts.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Type</th>
<th>Source</th>
<th>Target</th>
<th>Read only</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="mount in service.ServiceMounts">
<td>
<select name="mountType" class="form-control" ng-model="mount.Type" ng-disabled="isUpdating">
<option value="volume">Volume</option>
<option value="bind">Bind</option>
</select>
</td>
<td>
<input type="text" class="form-control" ng-model="mount.Source" placeholder="e.g. /tmp/portainer/data" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
</td>
<td>
<input type="text" class="form-control" ng-model="mount.Target" placeholder="e.g. /tmp/portainer/data" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
</td>
<td>
<input type="checkbox" class="form-control" ng-model="mount.ReadOnly" ng-change="updateMount(service, mount)" ng-disabled="isUpdating">
</td>
<td>
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removeMount(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</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(service, ['ServiceMounts'])" ng-click="updateService(service)">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(service, ['ServiceMounts'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>
@@ -1,26 +0,0 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Networks"></rd-widget-header>
<rd-widget-body ng-if="!service.VirtualIPs || service.VirtualIPs.length === 0">
<p>This service is not connected to any networks.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.VirtualIPs && service.VirtualIPs.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>ID</th>
<th>IP address</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="network in service.VirtualIPs">
<td>
<a ui-sref="network({id: network.NetworkID})">{{ network.NetworkID }}</a>
</td>
<td>{{ network.Addr }}</td>
</tr>
</tbody>
</table>
</rd-widget-body>
</rd-widget>
</div>
@@ -1,71 +0,0 @@
<div>
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Published ports">
<div class="nopadding">
<a class="btn btn-default btn-sm pull-right" ng-click="isUpdating ||addPublishedPort(service)" ng-disabled="isUpdating">
<i class="fa fa-plus-circle" aria-hidden="true"></i> port mapping
</a>
</div>
</rd-widget-header>
<rd-widget-body ng-if="!service.Ports || service.Ports.length === 0">
<p>This service has no ports published.</p>
</rd-widget-body>
<rd-widget-body ng-if="service.Ports && service.Ports.length > 0" classes="no-padding">
<table class="table" >
<thead>
<tr>
<th>Host port</th>
<th>Container port</th>
<th>Protocol</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="portBinding in service.Ports">
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon">host</span>
<input type="number" class="form-control" ng-model="portBinding.PublishedPort" placeholder="e.g. 8080" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<span class="input-group-addon">container</span>
<input type="number" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
</div>
</td>
<td>
<div class="input-group input-group-sm">
<select class="selectpicker form-control" ng-model="portBinding.Protocol" ng-change="updatePublishedPort(service, mapping)" ng-disabled="isUpdating">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</div>
</td>
<td>
<span class="input-group-btn">
<button class="btn btn-sm btn-danger" type="button" ng-click="removePortPublishedBinding(service, $index)" ng-disabled="isUpdating">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</span>
</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(service, ['Ports'])" ng-click="updateService(service)">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(service, ['Ports'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget>
</div>

Some files were not shown because too many files have changed in this diff Show More