From 57fde5ae7c8e9764e759e85aa45a42f9b9663d07 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 18 Jul 2017 12:17:31 +0200 Subject: [PATCH 01/30] feat(Dockerfile): use portainer/base image (#1045) --- build/linux/Dockerfile | 2 +- gruntfile.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/linux/Dockerfile b/build/linux/Dockerfile index 299b9bd23..e16144acc 100644 --- a/build/linux/Dockerfile +++ b/build/linux/Dockerfile @@ -1,4 +1,4 @@ -FROM centurylink/ca-certs +FROM portainer/base COPY dist / diff --git a/gruntfile.js b/gruntfile.js index 844683369..95ee9184e 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -4,7 +4,7 @@ var loadGruntTasks = require('load-grunt-tasks'); module.exports = function (grunt) { - loadGruntTasks(grunt); + loadGruntTasks(grunt); grunt.registerTask('default', ['eslint', 'build']); grunt.registerTask('before-copy', [ @@ -180,7 +180,7 @@ module.exports = function (grunt) { run: { command: [ 'docker rm -f portainer', - 'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer centurylink/ca-certs /app/portainer-linux-amd64 --no-analytics -a /app' + 'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer-linux-amd64 --no-analytics -a /app' ].join(';') } }, From 29d66bfd978f8a46f7033b7d2c3c5a5f03a2b6ea Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 19 Jul 2017 16:34:11 +0200 Subject: [PATCH 02/30] fix(containers): add support for the 'dead' status (#1048) --- app/filters/filters.js | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/app/filters/filters.js b/app/filters/filters.js index b5573bcd5..b4cac3fe0 100644 --- a/app/filters/filters.js +++ b/app/filters/filters.js @@ -1,3 +1,9 @@ +function includeString(text, values) { + return values.some(function(val){ + return text.indexOf(val) !== -1; + }); +} + angular.module('portainer.filters', []) .filter('truncate', function () { 'use strict'; @@ -35,15 +41,13 @@ angular.module('portainer.filters', []) 'use strict'; return function (text) { var status = _.toLower(text); - if (status.indexOf('new') !== -1 || status.indexOf('allocated') !== -1 || - status.indexOf('assigned') !== -1 || status.indexOf('accepted') !== -1) { + if (includeString(status, ['new', 'allocated', 'assigned', 'accepted'])) { return 'info'; - } else if (status.indexOf('pending') !== -1) { + } else if (includeString(status, ['pending'])) { return 'warning'; - } else if (status.indexOf('shutdown') !== -1 || status.indexOf('failed') !== -1 || - status.indexOf('rejected') !== -1) { + } else if (includeString(status, ['shutdown', 'failed', 'rejected'])) { return 'danger'; - } else if (status.indexOf('complete') !== -1) { + } else if (includeString(status, ['complete'])) { return 'primary'; } return 'success'; @@ -53,11 +57,11 @@ angular.module('portainer.filters', []) 'use strict'; return function (text) { var status = _.toLower(text); - if (status.indexOf('paused') !== -1 || status.indexOf('starting') !== -1) { + if (includeString(status, ['paused', 'starting'])) { return 'warning'; - } else if (status.indexOf('created') !== -1) { + } else if (includeString(status, ['created'])) { return 'info'; - } else if (status.indexOf('stopped') !== -1 || status.indexOf('unhealthy') !== -1) { + } else if (includeString(status, ['stopped', 'unhealthy', 'dead'])) { return 'danger'; } return 'success'; @@ -67,17 +71,19 @@ angular.module('portainer.filters', []) 'use strict'; return function (text) { var status = _.toLower(text); - if (status.indexOf('paused') !== -1) { + if (includeString(status, ['paused'])) { return 'paused'; - } else if (status.indexOf('created') !== -1) { + } else if (includeString(status, ['dead'])) { + return 'dead'; + } else if (includeString(status, ['created'])) { return 'created'; - } else if (status.indexOf('exited') !== -1) { + } else if (includeString(status, ['exited'])) { return 'stopped'; - } else if (status.indexOf('(healthy)') !== -1) { + } else if (includeString(status, ['(healthy)'])) { return 'healthy'; - } else if (status.indexOf('(unhealthy)') !== -1) { + } else if (includeString(status, ['(unhealthy)'])) { return 'unhealthy'; - } else if (status.indexOf('(health: starting)') !== -1) { + } else if (includeString(status, ['(health: starting)'])) { return 'starting'; } return 'running'; @@ -113,6 +119,9 @@ angular.module('portainer.filters', []) if (state === undefined) { return ''; } + if (state.Dead) { + return 'Dead'; + } if (state.Ghost && state.Running) { return 'Ghost'; } From 12eb9671de35cc5bd1ab1ce819b2b5bb41e36c1b Mon Sep 17 00:00:00 2001 From: 1138-4EB <1138-4EB@users.noreply.github.com> Date: Thu, 20 Jul 2017 08:47:11 +0200 Subject: [PATCH 03/30] style(volumes): replace label 'Dangling' with 'Unused' (#1052) --- app/components/volumes/volumes.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/volumes/volumes.html b/app/components/volumes/volumes.html index cfbb11252..c90e40945 100644 --- a/app/components/volumes/volumes.html +++ b/app/components/volumes/volumes.html @@ -38,7 +38,7 @@ Attached @@ -85,7 +85,7 @@ {{ volume.Id|truncate:25 }} - Dangling + Unused {{ volume.Driver }} {{ volume.Mountpoint | truncatelr }} From 53583741babf0e5830c01d06f86557b0e6bd801d Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 20 Jul 2017 15:48:05 +0200 Subject: [PATCH 04/30] fix(UAC): fix the ability to update the ownership of a resource from public to another type (#1054) --- app/components/container/container.html | 1 + app/components/service/service.html | 1 + app/components/volume/volume.html | 1 + app/directives/accessControlPanel/por-access-control-panel.js | 2 ++ .../accessControlPanel/porAccessControlPanelController.js | 2 +- 5 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/components/container/container.html b/app/components/container/container.html index 11b831635..ff270be5c 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -94,6 +94,7 @@ diff --git a/app/components/service/service.html b/app/components/service/service.html index cec143309..7cbe03a89 100644 --- a/app/components/service/service.html +++ b/app/components/service/service.html @@ -128,6 +128,7 @@ diff --git a/app/components/volume/volume.html b/app/components/volume/volume.html index fcdc8f52b..15ebfc41e 100644 --- a/app/components/volume/volume.html +++ b/app/components/volume/volume.html @@ -50,6 +50,7 @@ diff --git a/app/directives/accessControlPanel/por-access-control-panel.js b/app/directives/accessControlPanel/por-access-control-panel.js index 6bde5f128..afed037c2 100644 --- a/app/directives/accessControlPanel/por-access-control-panel.js +++ b/app/directives/accessControlPanel/por-access-control-panel.js @@ -2,6 +2,8 @@ angular.module('portainer').component('porAccessControlPanel', { templateUrl: 'app/directives/accessControlPanel/porAccessControlPanel.html', controller: 'porAccessControlPanelController', bindings: { + // The component will use this identifier when updating the resource control object. + resourceId: '<', // The component will display information about this resource control object. resourceControl: '=', // This component is usually displayed inside a resource-details view. diff --git a/app/directives/accessControlPanel/porAccessControlPanelController.js b/app/directives/accessControlPanel/porAccessControlPanelController.js index 13914606a..32c3f8635 100644 --- a/app/directives/accessControlPanel/porAccessControlPanelController.js +++ b/app/directives/accessControlPanel/porAccessControlPanelController.js @@ -73,7 +73,7 @@ function ($q, $state, UserService, ResourceControlService, Notifications, Authen function updateOwnership() { $('#loadingViewSpinner').show(); - var resourceId = ctrl.resourceControl.ResourceId; + var resourceId = ctrl.resourceId; var ownershipParameters = processOwnershipFormValues(); ResourceControlService.applyResourceControlChange(ctrl.resourceType, resourceId, From 02203e7ce528e3db16bd9fcb1e1b9b4ef913edc8 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 20 Jul 2017 16:22:27 +0200 Subject: [PATCH 05/30] refactor(api): relocate /docker API endpoint under /endpoints (#1053) --- api/http/handler/docker.go | 4 ++-- api/http/handler/handler.go | 8 +++++--- app/app.js | 24 +++++++++++------------- app/rest/api/auth.js | 4 ++-- app/rest/api/dockerhub.js | 4 ++-- app/rest/api/endpoint.js | 4 ++-- app/rest/api/registry.js | 4 ++-- app/rest/api/resourceControl.js | 4 ++-- app/rest/api/settings.js | 4 ++-- app/rest/api/status.js | 4 ++-- app/rest/api/team.js | 4 ++-- app/rest/api/teamMembership.js | 4 ++-- app/rest/api/template.js | 4 ++-- app/rest/api/user.js | 4 ++-- app/rest/docker/container.js | 4 ++-- app/rest/docker/containerCommit.js | 4 ++-- app/rest/docker/containerLogs.js | 4 ++-- app/rest/docker/containerTop.js | 4 ++-- app/rest/docker/exec.js | 4 ++-- app/rest/docker/image.js | 4 ++-- app/rest/docker/network.js | 4 ++-- app/rest/docker/node.js | 4 ++-- app/rest/docker/secret.js | 4 ++-- app/rest/docker/service.js | 4 ++-- app/rest/docker/serviceLogs.js | 4 ++-- app/rest/docker/swarm.js | 4 ++-- app/rest/docker/system.js | 4 ++-- app/rest/docker/task.js | 4 ++-- app/rest/docker/volume.js | 4 ++-- 29 files changed, 70 insertions(+), 70 deletions(-) diff --git a/api/http/handler/docker.go b/api/http/handler/docker.go index 6c4e23636..c823b3d20 100644 --- a/api/http/handler/docker.go +++ b/api/http/handler/docker.go @@ -30,7 +30,7 @@ func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler { Router: mux.NewRouter(), Logger: log.New(os.Stderr, "", log.LstdFlags), } - h.PathPrefix("/{id}/").Handler( + h.PathPrefix("/{id}/docker").Handler( bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToDockerAPI))) return h } @@ -90,5 +90,5 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r } } - http.StripPrefix("/"+id, proxy).ServeHTTP(w, r) + http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r) } diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 7fcb58c56..9c3eb45ea 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -51,7 +51,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } else if strings.HasPrefix(r.URL.Path, "/api/team_memberships") { http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r) } else if strings.HasPrefix(r.URL.Path, "/api/endpoints") { - http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) + if strings.Contains(r.URL.Path, "/docker") { + http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r) + } else { + http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) + } } else if strings.HasPrefix(r.URL.Path, "/api/registries") { http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r) } else if strings.HasPrefix(r.URL.Path, "/api/dockerhub") { @@ -68,8 +72,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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) } diff --git a/app/app.js b/app/app.js index ab07489e5..7598aff3b 100644 --- a/app/app.js +++ b/app/app.js @@ -744,18 +744,16 @@ angular.module('portainer', [ // 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/old_settings') - .constant('SETTINGS_ENDPOINT', 'api/settings') - .constant('STATUS_ENDPOINT', 'api/status') - .constant('AUTH_ENDPOINT', 'api/auth') - .constant('USERS_ENDPOINT', 'api/users') - .constant('TEAMS_ENDPOINT', 'api/teams') - .constant('TEAM_MEMBERSHIPS_ENDPOINT', 'api/team_memberships') - .constant('RESOURCE_CONTROL_ENDPOINT', 'api/resource_controls') - .constant('ENDPOINTS_ENDPOINT', 'api/endpoints') - .constant('DOCKERHUB_ENDPOINT', 'api/dockerhub') - .constant('REGISTRIES_ENDPOINT', 'api/registries') - .constant('TEMPLATES_ENDPOINT', 'api/templates') + .constant('API_ENDPOINT_AUTH', 'api/auth') + .constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub') + .constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints') + .constant('API_ENDPOINT_REGISTRIES', 'api/registries') + .constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls') + .constant('API_ENDPOINT_SETTINGS', 'api/settings') + .constant('API_ENDPOINT_STATUS', 'api/status') + .constant('API_ENDPOINT_USERS', 'api/users') + .constant('API_ENDPOINT_TEAMS', 'api/teams') + .constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships') + .constant('API_ENDPOINT_TEMPLATES', 'api/templates') .constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json') .constant('PAGINATION_MAX_ITEMS', 10); diff --git a/app/rest/api/auth.js b/app/rest/api/auth.js index c7ed49447..17d4dc22d 100644 --- a/app/rest/api/auth.js +++ b/app/rest/api/auth.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Auth', ['$resource', 'AUTH_ENDPOINT', function AuthFactory($resource, AUTH_ENDPOINT) { +.factory('Auth', ['$resource', 'API_ENDPOINT_AUTH', function AuthFactory($resource, API_ENDPOINT_AUTH) { 'use strict'; - return $resource(AUTH_ENDPOINT, {}, { + return $resource(API_ENDPOINT_AUTH, {}, { login: { method: 'POST' } diff --git a/app/rest/api/dockerhub.js b/app/rest/api/dockerhub.js index 3a07d4aa2..5572be7f7 100644 --- a/app/rest/api/dockerhub.js +++ b/app/rest/api/dockerhub.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('DockerHub', ['$resource', 'DOCKERHUB_ENDPOINT', function DockerHubFactory($resource, DOCKERHUB_ENDPOINT) { +.factory('DockerHub', ['$resource', 'API_ENDPOINT_DOCKERHUB', function DockerHubFactory($resource, API_ENDPOINT_DOCKERHUB) { 'use strict'; - return $resource(DOCKERHUB_ENDPOINT, {}, { + return $resource(API_ENDPOINT_DOCKERHUB, {}, { get: { method: 'GET' }, update: { method: 'PUT' } }); diff --git a/app/rest/api/endpoint.js b/app/rest/api/endpoint.js index c2cf17fdf..bef6f960b 100644 --- a/app/rest/api/endpoint.js +++ b/app/rest/api/endpoint.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Endpoints', ['$resource', 'ENDPOINTS_ENDPOINT', function EndpointsFactory($resource, ENDPOINTS_ENDPOINT) { +.factory('Endpoints', ['$resource', 'API_ENDPOINT_ENDPOINTS', function EndpointsFactory($resource, API_ENDPOINT_ENDPOINTS) { 'use strict'; - return $resource(ENDPOINTS_ENDPOINT + '/:id/:action', {}, { + return $resource(API_ENDPOINT_ENDPOINTS + '/:id/:action', {}, { create: { method: 'POST' }, query: { method: 'GET', isArray: true }, get: { method: 'GET', params: { id: '@id' } }, diff --git a/app/rest/api/registry.js b/app/rest/api/registry.js index 9ba68ee46..819e19061 100644 --- a/app/rest/api/registry.js +++ b/app/rest/api/registry.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Registries', ['$resource', 'REGISTRIES_ENDPOINT', function RegistriesFactory($resource, REGISTRIES_ENDPOINT) { +.factory('Registries', ['$resource', 'API_ENDPOINT_REGISTRIES', function RegistriesFactory($resource, API_ENDPOINT_REGISTRIES) { 'use strict'; - return $resource(REGISTRIES_ENDPOINT + '/:id/:action', {}, { + return $resource(API_ENDPOINT_REGISTRIES + '/:id/:action', {}, { create: { method: 'POST' }, query: { method: 'GET', isArray: true }, get: { method: 'GET', params: { id: '@id' } }, diff --git a/app/rest/api/resourceControl.js b/app/rest/api/resourceControl.js index bcdebde65..5503ce0db 100644 --- a/app/rest/api/resourceControl.js +++ b/app/rest/api/resourceControl.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('ResourceControl', ['$resource', 'RESOURCE_CONTROL_ENDPOINT', function ResourceControlFactory($resource, RESOURCE_CONTROL_ENDPOINT) { +.factory('ResourceControl', ['$resource', 'API_ENDPOINT_RESOURCE_CONTROLS', function ResourceControlFactory($resource, API_ENDPOINT_RESOURCE_CONTROLS) { 'use strict'; - return $resource(RESOURCE_CONTROL_ENDPOINT + '/:id', {}, { + return $resource(API_ENDPOINT_RESOURCE_CONTROLS + '/:id', {}, { create: { method: 'POST' }, get: { method: 'GET', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } }, diff --git a/app/rest/api/settings.js b/app/rest/api/settings.js index 5e7471ad9..46ee8f336 100644 --- a/app/rest/api/settings.js +++ b/app/rest/api/settings.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Settings', ['$resource', 'SETTINGS_ENDPOINT', function SettingsFactory($resource, SETTINGS_ENDPOINT) { +.factory('Settings', ['$resource', 'API_ENDPOINT_SETTINGS', function SettingsFactory($resource, API_ENDPOINT_SETTINGS) { 'use strict'; - return $resource(SETTINGS_ENDPOINT, {}, { + return $resource(API_ENDPOINT_SETTINGS, {}, { get: { method: 'GET' }, update: { method: 'PUT' } }); diff --git a/app/rest/api/status.js b/app/rest/api/status.js index b636ed283..285c67ef5 100644 --- a/app/rest/api/status.js +++ b/app/rest/api/status.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Status', ['$resource', 'STATUS_ENDPOINT', function StatusFactory($resource, STATUS_ENDPOINT) { +.factory('Status', ['$resource', 'API_ENDPOINT_STATUS', function StatusFactory($resource, API_ENDPOINT_STATUS) { 'use strict'; - return $resource(STATUS_ENDPOINT, {}, { + return $resource(API_ENDPOINT_STATUS, {}, { get: { method: 'GET' } }); }]); diff --git a/app/rest/api/team.js b/app/rest/api/team.js index fd55e95b3..0c98e8742 100644 --- a/app/rest/api/team.js +++ b/app/rest/api/team.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Teams', ['$resource', 'TEAMS_ENDPOINT', function TeamsFactory($resource, TEAMS_ENDPOINT) { +.factory('Teams', ['$resource', 'API_ENDPOINT_TEAMS', function TeamsFactory($resource, API_ENDPOINT_TEAMS) { 'use strict'; - return $resource(TEAMS_ENDPOINT + '/:id/:entity/:entityId', {}, { + return $resource(API_ENDPOINT_TEAMS + '/:id/:entity/:entityId', {}, { create: { method: 'POST' }, query: { method: 'GET', isArray: true }, get: { method: 'GET', params: { id: '@id' } }, diff --git a/app/rest/api/teamMembership.js b/app/rest/api/teamMembership.js index 39b49134c..51d503265 100644 --- a/app/rest/api/teamMembership.js +++ b/app/rest/api/teamMembership.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('TeamMemberships', ['$resource', 'TEAM_MEMBERSHIPS_ENDPOINT', function TeamMembershipsFactory($resource, TEAM_MEMBERSHIPS_ENDPOINT) { +.factory('TeamMemberships', ['$resource', 'API_ENDPOINT_TEAM_MEMBERSHIPS', function TeamMembershipsFactory($resource, API_ENDPOINT_TEAM_MEMBERSHIPS) { 'use strict'; - return $resource(TEAM_MEMBERSHIPS_ENDPOINT + '/:id/:action', {}, { + return $resource(API_ENDPOINT_TEAM_MEMBERSHIPS + '/:id/:action', {}, { create: { method: 'POST' }, query: { method: 'GET', isArray: true }, update: { method: 'PUT', params: { id: '@id' } }, diff --git a/app/rest/api/template.js b/app/rest/api/template.js index ea02b7ade..b01a95575 100644 --- a/app/rest/api/template.js +++ b/app/rest/api/template.js @@ -1,6 +1,6 @@ angular.module('portainer.rest') -.factory('Template', ['$resource', 'TEMPLATES_ENDPOINT', function TemplateFactory($resource, TEMPLATES_ENDPOINT) { - return $resource(TEMPLATES_ENDPOINT, {}, { +.factory('Template', ['$resource', 'API_ENDPOINT_TEMPLATES', function TemplateFactory($resource, API_ENDPOINT_TEMPLATES) { + return $resource(API_ENDPOINT_TEMPLATES, {}, { get: {method: 'GET', isArray: true} }); }]); diff --git a/app/rest/api/user.js b/app/rest/api/user.js index 6189130cc..f5b59873d 100644 --- a/app/rest/api/user.js +++ b/app/rest/api/user.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Users', ['$resource', 'USERS_ENDPOINT', function UsersFactory($resource, USERS_ENDPOINT) { +.factory('Users', ['$resource', 'API_ENDPOINT_USERS', function UsersFactory($resource, API_ENDPOINT_USERS) { 'use strict'; - return $resource(USERS_ENDPOINT + '/:id/:entity/:entityId', {}, { + return $resource(API_ENDPOINT_USERS + '/:id/:entity/:entityId', {}, { create: { method: 'POST' }, query: { method: 'GET', isArray: true }, get: { method: 'GET', params: { id: '@id' } }, diff --git a/app/rest/docker/container.js b/app/rest/docker/container.js index 511e61d5f..1bad5758f 100644 --- a/app/rest/docker/container.js +++ b/app/rest/docker/container.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Container', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ContainerFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/containers/:id/:action', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/containers/:id/:action', { name: '@name', endpointId: EndpointProvider.endpointID }, diff --git a/app/rest/docker/containerCommit.js b/app/rest/docker/containerCommit.js index 5fff5912b..c8007f47d 100644 --- a/app/rest/docker/containerCommit.js +++ b/app/rest/docker/containerCommit.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('ContainerCommit', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ContainerCommitFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('ContainerCommit', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerCommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/commit', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', { endpointId: EndpointProvider.endpointID }, { diff --git a/app/rest/docker/containerLogs.js b/app/rest/docker/containerLogs.js index a7dbf85b8..c7798066f 100644 --- a/app/rest/docker/containerLogs.js +++ b/app/rest/docker/containerLogs.js @@ -1,11 +1,11 @@ angular.module('portainer.rest') -.factory('ContainerLogs', ['$http', 'DOCKER_ENDPOINT', 'EndpointProvider', function ContainerLogsFactory($http, DOCKER_ENDPOINT, EndpointProvider) { +.factory('ContainerLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; return { get: function (id, params, callback) { $http({ method: 'GET', - url: DOCKER_ENDPOINT + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/logs', + url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/logs', params: { 'stdout': params.stdout || 0, 'stderr': params.stderr || 0, diff --git a/app/rest/docker/containerTop.js b/app/rest/docker/containerTop.js index edefb3e1d..5df3020fa 100644 --- a/app/rest/docker/containerTop.js +++ b/app/rest/docker/containerTop.js @@ -1,11 +1,11 @@ angular.module('portainer.rest') -.factory('ContainerTop', ['$http', 'DOCKER_ENDPOINT', 'EndpointProvider', function ($http, DOCKER_ENDPOINT, EndpointProvider) { +.factory('ContainerTop', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; return { get: function (id, params, callback, errorCallback) { $http({ method: 'GET', - url: DOCKER_ENDPOINT + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/top', + url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/top', params: { ps_args: params.ps_args } diff --git a/app/rest/docker/exec.js b/app/rest/docker/exec.js index c29036197..530975678 100644 --- a/app/rest/docker/exec.js +++ b/app/rest/docker/exec.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Exec', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ExecFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Exec', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ExecFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/exec/:id/:action', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/exec/:id/:action', { endpointId: EndpointProvider.endpointID }, { diff --git a/app/rest/docker/image.js b/app/rest/docker/image.js index c9ddd14bf..439a10861 100644 --- a/app/rest/docker/image.js +++ b/app/rest/docker/image.js @@ -1,8 +1,8 @@ angular.module('portainer.rest') -.factory('Image', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', 'HttpRequestHelper', function ImageFactory($resource, DOCKER_ENDPOINT, EndpointProvider, HttpRequestHelper) { +.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/images/:id/:action', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/images/:id/:action', { endpointId: EndpointProvider.endpointID }, { diff --git a/app/rest/docker/network.js b/app/rest/docker/network.js index 39e10b4b2..561b00a9d 100644 --- a/app/rest/docker/network.js +++ b/app/rest/docker/network.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Network', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function NetworkFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/networks/:id/:action', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/networks/:id/:action', { id: '@id', endpointId: EndpointProvider.endpointID }, diff --git a/app/rest/docker/node.js b/app/rest/docker/node.js index 1dc13f588..22f7b3543 100644 --- a/app/rest/docker/node.js +++ b/app/rest/docker/node.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Node', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function NodeFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Node', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function NodeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/nodes/:id/:action', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/nodes/:id/:action', { endpointId: EndpointProvider.endpointID }, { diff --git a/app/rest/docker/secret.js b/app/rest/docker/secret.js index 976b8053d..38a593248 100644 --- a/app/rest/docker/secret.js +++ b/app/rest/docker/secret.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Secret', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function SecretFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Secret', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function SecretFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/secrets/:id/:action', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/secrets/:id/:action', { endpointId: EndpointProvider.endpointID }, { get: { method: 'GET', params: {id: '@id'} }, diff --git a/app/rest/docker/service.js b/app/rest/docker/service.js index 721b55a9c..e8ca55962 100644 --- a/app/rest/docker/service.js +++ b/app/rest/docker/service.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Service', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', 'HttpRequestHelper' ,function ServiceFactory($resource, DOCKER_ENDPOINT, EndpointProvider, HttpRequestHelper) { +.factory('Service', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper' ,function ServiceFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpRequestHelper) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/services/:id/:action', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/services/:id/:action', { endpointId: EndpointProvider.endpointID }, { diff --git a/app/rest/docker/serviceLogs.js b/app/rest/docker/serviceLogs.js index b165b2542..cefb57c6e 100644 --- a/app/rest/docker/serviceLogs.js +++ b/app/rest/docker/serviceLogs.js @@ -1,11 +1,11 @@ angular.module('portainer.rest') -.factory('ServiceLogs', ['$http', 'DOCKER_ENDPOINT', 'EndpointProvider', function ServiceLogsFactory($http, DOCKER_ENDPOINT, EndpointProvider) { +.factory('ServiceLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ServiceLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; return { get: function (id, params, callback) { $http({ method: 'GET', - url: DOCKER_ENDPOINT + '/' + EndpointProvider.endpointID() + '/services/' + id + '/logs', + url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/services/' + id + '/logs', params: { 'stdout': params.stdout || 0, 'stderr': params.stderr || 0, diff --git a/app/rest/docker/swarm.js b/app/rest/docker/swarm.js index cec81e85f..d365ea5d1 100644 --- a/app/rest/docker/swarm.js +++ b/app/rest/docker/swarm.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Swarm', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function SwarmFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Swarm', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function SwarmFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/swarm', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/swarm', { endpointId: EndpointProvider.endpointID }, { diff --git a/app/rest/docker/system.js b/app/rest/docker/system.js index c0285070a..8636ef348 100644 --- a/app/rest/docker/system.js +++ b/app/rest/docker/system.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('System', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function SystemFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('System', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function SystemFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/:action/:subAction', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/:action/:subAction', { name: '@name', endpointId: EndpointProvider.endpointID }, diff --git a/app/rest/docker/task.js b/app/rest/docker/task.js index 2ce993cef..2806b0852 100644 --- a/app/rest/docker/task.js +++ b/app/rest/docker/task.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Task', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function TaskFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Task', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/tasks/:id', { + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id', { endpointId: EndpointProvider.endpointID }, { diff --git a/app/rest/docker/volume.js b/app/rest/docker/volume.js index 3bb900df7..1ae1264f9 100644 --- a/app/rest/docker/volume.js +++ b/app/rest/docker/volume.js @@ -1,7 +1,7 @@ angular.module('portainer.rest') -.factory('Volume', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function VolumeFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Volume', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function VolumeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/volumes/:id/:action', + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/volumes/:id/:action', { endpointId: EndpointProvider.endpointID }, From 7c40d2caa9fb911c0d1d94c75a9e3d67788bd85b Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 24 Jul 2017 11:59:09 +0200 Subject: [PATCH 06/30] fix(services): use secrets with services only if endpoint API version >= 1.25 --- .../createService/createServiceController.js | 3 +- .../createService/createservice.html | 2 +- .../includes/placementPreferences.html | 2 +- app/components/service/service.html | 8 +++--- app/components/service/serviceController.js | 28 ++++--------------- 5 files changed, 13 insertions(+), 30 deletions(-) diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index 3ede157b1..dc9831f92 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -302,10 +302,11 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic function initView() { $('#loadingViewSpinner').show(); + var apiVersion = $scope.applicationState.endpoint.apiVersion; $q.all({ volumes: VolumeService.volumes(), networks: NetworkService.retrieveSwarmNetworks(), - secrets: SecretService.secrets() + secrets: apiVersion >= 1.25 ? SecretService.secrets() : [] }) .then(function success(data) { $scope.availableVolumes = data.volumes; diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index 49e796069..5c8ab23d9 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -132,7 +132,7 @@
  • Network
  • Labels
  • Update config
  • -
  • Secrets
  • +
  • Secrets
  • Placement
  • diff --git a/app/components/service/includes/placementPreferences.html b/app/components/service/includes/placementPreferences.html index 210556e72..d92fae5d9 100644 --- a/app/components/service/includes/placementPreferences.html +++ b/app/components/service/includes/placementPreferences.html @@ -1,4 +1,4 @@ -
    +
    diff --git a/app/components/service/service.html b/app/components/service/service.html index 7cbe03a89..e11585c62 100644 --- a/app/components/service/service.html +++ b/app/components/service/service.html @@ -113,11 +113,11 @@
  • Network & published ports
  • Resource limits & reservations
  • Placement constraints
  • -
  • Placement preferences
  • +
  • Placement preferences
  • Restart policy
  • Update configuration
  • Service labels
  • -
  • Secrets
  • +
  • Secrets
  • Tasks
  • @@ -160,11 +160,11 @@

    Service specification

    -
    +
    -
    +
    diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index b61dbb256..abfe6140d 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -1,6 +1,6 @@ angular.module('service', []) -.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', -function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) { +.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'SecretService', 'SecretHelper', 'Service', 'ServiceHelper', 'LabelHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', +function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, SecretService, SecretHelper, Service, ServiceHelper, LabelHelper, TaskService, NodeService, Notifications, Pagination, ModalService) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); @@ -288,7 +288,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, function initView() { $('#loadingViewSpinner').show(); - + var apiVersion = $scope.applicationState.endpoint.apiVersion; ServiceService.service($stateParams.id) .then(function success(data) { var service = data; @@ -304,21 +304,17 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, return $q.all({ tasks: TaskService.serviceTasks(service.Name), nodes: NodeService.nodes(), - secrets: Secret.query({}).$promise + secrets: apiVersion >= 1.25 ? SecretService.secrets() : [] }); }) .then(function success(data) { $scope.tasks = data.tasks; $scope.nodes = data.nodes; - - $scope.secrets = data.secrets.map(function (secret) { - return new SecretViewModel(secret); - }); + $scope.secrets = data.secrets; $timeout(function() { $anchorScroll(); }); - }) .catch(function error(err) { $scope.secrets = []; @@ -329,20 +325,6 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, }); } - function fetchSecrets() { - $('#loadSecretsSpinner').show(); - Secret.query({}, function (d) { - $scope.secrets = d.map(function (secret) { - return new SecretViewModel(secret); - }); - $('#loadSecretsSpinner').hide(); - }, function(e) { - $('#loadSecretsSpinner').hide(); - Notifications.error('Failure', e, 'Unable to retrieve secrets'); - $scope.secrets = []; - }); - } - $scope.updateServiceAttribute = function updateServiceAttribute(service, name) { if (service[name] !== originalService[name] || !(name in originalService)) { service.hasChanges = true; From 387b4c66d95503986120be4fc41f23d11b94c210 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 24 Jul 2017 16:29:28 +0200 Subject: [PATCH 07/30] fix(containers): fix an issue when only containers without ports are running (#1068) --- app/models/docker/container.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models/docker/container.js b/app/models/docker/container.js index 8041a24e3..552963b22 100644 --- a/app/models/docker/container.js +++ b/app/models/docker/container.js @@ -11,14 +11,18 @@ function ContainerViewModel(data) { this.Command = data.Command; this.Checked = false; this.Labels = data.Labels; - this.Ports = []; this.Mounts = data.Mounts; - for (var i = 0; i < data.Ports.length; ++i) { - var p = data.Ports[i]; - if (p.PublicPort) { - this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort }); + + this.Ports = []; + if (data.Ports) { + for (var i = 0; i < data.Ports.length; ++i) { + var p = data.Ports[i]; + if (p.PublicPort) { + this.Ports.push({ host: p.IP, private: p.PrivatePort, public: p.PublicPort }); + } } } + if (data.Portainer) { if (data.Portainer.ResourceControl) { this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); From aca4f5c286c036d70a377beb50710f86240ed2f5 Mon Sep 17 00:00:00 2001 From: Konstantin Azizov Date: Mon, 24 Jul 2017 17:39:04 +0300 Subject: [PATCH 08/30] fix(containers): Fix available buttons for created container (#1065) --- app/components/containers/containersController.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index b08d41705..82dd10b64 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -205,7 +205,8 @@ angular.module('containers', []) if(container.Status === 'paused') { $scope.state.noPausedItemsSelected = false; - } else if(container.Status === 'stopped') { + } else if(container.Status === 'stopped' || + container.Status === 'created') { $scope.state.noStoppedItemsSelected = false; } else if(container.Status === 'running') { $scope.state.noRunningItemsSelected = false; From 3919ad3ccf11ebe816520fb7bda9ce4baa3cbed6 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 24 Jul 2017 19:11:12 +0200 Subject: [PATCH 09/30] fix(images): show image usage only if endpoint API version >= 1.25 (#1067) --- app/components/images/images.html | 10 +++++++--- app/components/images/imagesController.js | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/components/images/images.html b/app/components/images/images.html index 19dd4669b..d317e9818 100644 --- a/app/components/images/images.html +++ b/app/components/images/images.html @@ -70,7 +70,7 @@
    - + @@ -125,7 +125,11 @@ {{ image.Id|truncate:20}} - Unused + + Unused + + {{ tag }} @@ -135,7 +139,7 @@ Loading... - + No images available. diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index cbb723274..d5f887049 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -94,7 +94,8 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService) function fetchImages() { $('#loadImagesSpinner').show(); var endpointProvider = $scope.applicationState.endpoint.mode.provider; - ImageService.images(endpointProvider !== 'DOCKER_SWARM') + var apiVersion = $scope.applicationState.endpoint.apiVersion; + ImageService.images(apiVersion >= 1.25 && endpointProvider !== 'DOCKER_SWARM') .then(function success(data) { $scope.images = data; }) From b08d2b07bcd65b2e8047dc932206f7ab9b3f55e9 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 25 Jul 2017 16:21:32 +0200 Subject: [PATCH 10/30] feat(volume-creation): add plugin support (#1044) * feat(volume-creation): add plugin support * feat(plugins): only use systemInfo to retrieve plugins when API version < 1.25 * refactor(createVolume): remove unused dependencies --- .../createVolume/createVolumeController.js | 10 ++-- app/models/docker/plugin.js | 9 +++ app/rest/docker/plugin.js | 9 +++ app/services/docker/pluginService.js | 56 +++++++++++++++++++ app/services/docker/systemService.js | 8 +-- 5 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 app/models/docker/plugin.js create mode 100644 app/rest/docker/plugin.js create mode 100644 app/services/docker/pluginService.js diff --git a/app/components/createVolume/createVolumeController.js b/app/components/createVolume/createVolumeController.js index 579b948d1..8319337ba 100644 --- a/app/components/createVolume/createVolumeController.js +++ b/app/components/createVolume/createVolumeController.js @@ -1,6 +1,6 @@ angular.module('createVolume', []) -.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'SystemService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', -function ($scope, $state, VolumeService, SystemService, ResourceControlService, Authentication, Notifications, FormValidator) { +.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', +function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator) { $scope.formValues = { Driver: 'local', @@ -70,8 +70,10 @@ function ($scope, $state, VolumeService, SystemService, ResourceControlService, function initView() { $('#loadingViewSpinner').show(); - if ($scope.applicationState.endpoint.mode.provider !== 'DOCKER_SWARM') { - SystemService.getVolumePlugins() + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + var apiVersion = $scope.applicationState.endpoint.apiVersion; + if (endpointProvider !== 'DOCKER_SWARM') { + PluginService.volumePlugins(apiVersion < 1.25) .then(function success(data) { $scope.availableVolumeDrivers = data; }) diff --git a/app/models/docker/plugin.js b/app/models/docker/plugin.js new file mode 100644 index 000000000..fde1ab840 --- /dev/null +++ b/app/models/docker/plugin.js @@ -0,0 +1,9 @@ +// This model is based on https://github.com/moby/moby/blob/0ac25dfc751fa4304ab45afd5cd8705c2235d101/api/types/plugin.go#L8-L31 +// instead of the official documentation. +// See: https://github.com/moby/moby/issues/34241 +function PluginViewModel(data) { + this.Id = data.Id; + this.Name = data.Name; + this.Enabled = data.Enabled; + this.Config = data.Config; +} diff --git a/app/rest/docker/plugin.js b/app/rest/docker/plugin.js new file mode 100644 index 000000000..a0a342d2d --- /dev/null +++ b/app/rest/docker/plugin.js @@ -0,0 +1,9 @@ +angular.module('portainer.rest') +.factory('Plugin', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function PluginFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/plugins/:id/:action', { + endpointId: EndpointProvider.endpointID + }, { + query: { method: 'GET', isArray: true } + }); +}]); diff --git a/app/services/docker/pluginService.js b/app/services/docker/pluginService.js new file mode 100644 index 000000000..d6e3325e6 --- /dev/null +++ b/app/services/docker/pluginService.js @@ -0,0 +1,56 @@ +angular.module('portainer.services') +.factory('PluginService', ['$q', 'Plugin', 'SystemService', function PluginServiceFactory($q, Plugin, SystemService) { + 'use strict'; + var service = {}; + + service.plugins = function() { + var deferred = $q.defer(); + + Plugin.query({}).$promise + .then(function success(data) { + var plugins = data.map(function (item) { + return new PluginViewModel(item); + }); + deferred.resolve(plugins); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve plugins', err: err }); + }); + + return deferred.promise; + }; + + service.volumePlugins = function(systemOnly) { + var deferred = $q.defer(); + + $q.all({ + system: SystemService.plugins(), + plugins: systemOnly ? [] : service.plugins() + }) + .then(function success(data) { + var volumePlugins = []; + var systemPlugins = data.system; + var plugins = data.plugins; + + if (systemPlugins.Volume) { + volumePlugins = volumePlugins.concat(systemPlugins.Volume); + } + + for (var i = 0; i < plugins.length; i++) { + var plugin = plugins[i]; + if (plugin.Enabled && _.includes(plugin.Config.Interface.Types, 'docker.volumedriver/1.0')) { + volumePlugins.push(plugin.Name); + } + } + + deferred.resolve(volumePlugins); + }) + .catch(function error(err) { + deferred.reject({ msg: err.msg, err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/services/docker/systemService.js b/app/services/docker/systemService.js index 4c017c0be..fcaed9e73 100644 --- a/app/services/docker/systemService.js +++ b/app/services/docker/systemService.js @@ -3,15 +3,15 @@ angular.module('portainer.services') 'use strict'; var service = {}; - service.getVolumePlugins = function() { + service.plugins = function() { var deferred = $q.defer(); System.info({}).$promise .then(function success(data) { - var plugins = data.Plugins.Volume; + var plugins = data.Plugins; deferred.resolve(plugins); }) .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve volume plugin information', err: err}); + deferred.reject({msg: 'Unable to retrieve plugins information from system', err: err}); }); return deferred.promise; }; @@ -40,7 +40,7 @@ angular.module('portainer.services') return deferred.promise; }; - + service.dataUsage = function () { return System.dataUsage().$promise; }; From 635ecdef721feb51efdd76341dfb05e418e0d034 Mon Sep 17 00:00:00 2001 From: Dan Hlavenka Date: Wed, 26 Jul 2017 00:52:44 -0500 Subject: [PATCH 11/30] style(sidebar): crop logo.png to fit in sidebar without scaling (#1072) --- assets/images/logo.png | Bin 1766 -> 2048 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/images/logo.png b/assets/images/logo.png index f572182047bb41b4f7ff8ec6396917e72f123941..82f5a35d4c2dd9a51b700182b1a9e76b53498649 100644 GIT binary patch literal 2048 zcmV+b2>#48hKT$JOdhGB&ehEkWr~l2hggWskCCL9j%JCql3}b(%Qk& zVZ;|xl(tBQI_VU=|;Sn{Ff+DsQup*cg$mQ!F_vYl>+y^&RI?ixbCU>&; zUVH7ee|PQm*yjXdiYca;Vv4DAWOpy|#2AOp!q!hcH^w1JT?*zL1&oXV>zt)MZIYh0 ze~d?AKJ8wc-R!s2JKb!H_jyc=OMcL%`mX!D%aWvTv(&r%#P^(L5TlbFvf-{!msp-2 z5PjnOj5bm9{pHA^W^{aM7-+=@KNG7g2D<*Z@~ z>%w*sQ<(+KqlN_U2m6ms`?|nSLt~CsYdfByVQ+U@0GPq!;b(7t&kfwoau%jZNb+tB z_QkHP4ay~v&txQR@V*$A&Qu9U_6xe+zj#JLoD+P{nMX*A-@3sbpB}bezT`{%KceLs zjOT=cILFY3@ka!-ZcJnt0}A2odKgg81D^j}osYRRviyj+`2$`+c~IAB*!&*10sHs^ z*8=ZE(vQzi3~Q$5h{>*}zA(;oYb&Dv`^up@)# z7|y02>uCrQVP989vnyz{D-|4ID;1Oj6_&6k*c5X0W)$#B@F6&e9;^-o%NWY`;9USm zQWVtLjo}Po7jFeyI;>qx1%qg!KAKfpNg`Nhz?qyBtWM(}N*S+4^ekd96}-pVpgG^^ zu2yxgkE{Athg*=0Nb0fb?>yC&9!lkJ%k38#FY#v{OpWaFYmpj6YrMkU>TJhE^1l@+ zjsdPr^&Ij=d;2G~Boz|$ffU!9Zu3%igq({!He4g{CikXBTKs{b7k9gxtn?jqS%>{E zSIffIGpdfoalRCc0GFAZ>GL7|)4{DQlC%nwr)blj& zQo;2+<>=tnaNY#2ViOPWEIar*Q~3(B*~&iw9K)YEmKs*kg$tQN9Utdd{S=<1fw^p> zAGdHl+o(>B)G&?rnM)I2Vmd$JVkZS{PNyHQu$%jMF^>t&<#u-SAocWR3b!%NsvI`w zY!fE@$VW|>aFZ{XFyS+KtxflOQP^|22W?2%#XR*PU1qa3C#Y*ZwauxxK%HOU zA@#4SK6e<{W~9mZX|l)OCTy@QP^%$?@yC?Qy$nN_b@i9y#U;i zUhp~0R-2RBxLy5n*8R7`R_g(WMZKgOJY@N2m+l_m$+Y%wfYHDxy0A1j5Q$R_)H2-C zRI@H<$sutx@awdmMYQJN-Ot8I>v`Zbus>g5Bd0mF?T5kC(w#OS{uMOl&EQHH!P+1q zIXK8t0!C%`ZGU49u%Gqp1a`8ASAo1susI41&;$%h{dSz$NEHK98{L^x;vi&mT4K=R z7P8k6JA-JF^dV51T6-AFGPo;P3&5_!n8N84)0ClmQf?O5W->X8ki9MmT5~Qjz~bOw z+LB4=nt*{Fcn6Z;?}xSDCPp(Ls6yXXkqmBHYF`nIdqTtkE~`SBqxgBJcV11vytaU3iH}+9by@vbr8j1Ys`t>1Cp@Vw2jqUvVqvf? z+aVp2Q;Feu55&o=_7PSf0~gPUksRxq#&q zaWUU!5AS3g`26$;lbOUi=hMgtuBDQtEXe}e@vP)H%DJb54$rud8>n|aZ_uA>xR@qp zWse`ja^Brm=dBV3x-sN@!=X{7TojHp``z>^A#LqQ0s`Q0*M~Ff9gw?9NRx7Jd)i@< z)nSFQ+lB3^klWo9Ni6=M-kh~c_>Rq?4vRe@XNBqRknNxkd!FYz&dJt#1=z=f*7@gu(uRl)@Y&Dk2m7KQDs0_Tj4 zVT;1{QGs)=v}+7oJFOv6(43wbmnuhjQ222{A>lt3$5=&SEB1%B9u~G=#z|9)Q2e!ov17ps%n*>I{3?Y^Wq>3EZTv9;I)b21D`qqnsTm2j zQzV!*)6|S<0XF!n0Z{~22Q|la&@n+E6c(1IPsY}YGHEff-?IX7-%h))b+~#AoCTo+@NrE=OI;`}?T<73aEAg~1 zr;J)c>+XJsz39TiUe!>WGG&*|9dh7l5{{Zf8RFtH-5Pvu-W9VpasiSDEtG|OMjwx_mT<&v6 zX~hk6@h59|qgu6UELN-5+G7#iv1!v5fqj;N*DSHlI`<@j(^jRkLI^<*XDc&WmaFG|sq;&2hHMOpvFWN~dKt&Q}qFjw(#?O?_M3 zyMqc>`fBbjQ-$+G(8k&Mn<(e0%o%4p<*qQa)z~~ATwaExCAbstN4-3)F}t5z+cc>V zLQv*j_1T)8z8i~6SY?7;xZRN0esEn@qpzp4C5_hT>Ca@QZ!~_C)q2%CF7|$;xxxw2 zJ=(a=zHH3_w`v>J?66IRH}F{A{MVve^lh>avh5H1NzpC73940l$TZceEiMV%Ht%V) z%51Z(#$Fdhd6gIOvIVC2v7NXt25uL&n&&(xyUL%iz*RvFevm*&w~jdv_^g$4eFr<%~&;Ujf+xWlkyaF$@FC8n6~CA?f@-ML+Z275fD zL4$3c)S$s%ih^UG5mCul^=h(HybllQkgfTGjkqWR$FG}Ti@1%`*>(qwiRwoAm;`V< z8QtL=?9KlEJ#1`Re>z!*IT1LXZC;y&z!{BAdPSz?0W2-u#yS+mxIH=-b`Gw?4|Dpwt_ZkTj>dMJ5<&>x&kX~WJ8XYuGf_Q|6Et^6GBG#3XPdRH?GiL{+NXXu2v@rWOTPlh;QXo(aKR zd?&B&5tKJMheUAy)Tn)4%k4?dAxG(VN?-;x`CDRbzE!1co`c`I&`NxoDfxX$W5msfWy zE-l=wy*&v}YH(2-H#Mi1J25*1y)@b!UpdZDXLIv*BoW;8`7gPv6Kz~Ua3T0WW0QAp zG0#@Pg?+1C=YZS#wQ+ugk9H%y&3GLG9XaBtJugp;ht_!7W%^_BxF zqq+<1Jz{Xz;+ZDvtIRKLa8)62DqItJ-)3-Q@tn^_d&0$7mYktTl2t{(ZTF!=erBQx zma2Ea@Cd+bSZjunZuT~o$2Y*qA(s*A)cdLNzUAlY?ezK)f@|Xm)R^V#Zn6=-iymPN zoD=QCT?O`W%TQ~Mk*>A|+kGYpTzMk6MMWcrGi;4CU&;xZPP8Jb+3yFrkwdXq)c~(V zEjR0%961yQ7lL*cMN!KxziRgVIp8{Y57!rr9ImxL8bfs=wh*+nD^jfYprM8tX`Brv zgrK+j!r(&C#<{NXeFNi>URI~K%gi-8{&y!$v1+WOkILLFsdmmc(@f`t5Q0I*B*m&Z z_o|HNqMhs#H@h%*RGC;c_7E!*W7Td(nPZYZt;MPjX0aXg4fFgbYr$sgtc!`RHo$y~ zT7pY6zGa@tyqfOLc~v(=i?&+N6kH*6rF6;ZaMr97H~D+I2S8?5$v(WAh@DY$%W zebtn3hI&)X$lu_S6kA*WDCzJyJs`{jcR`7#_R~{Vx!`7&2=3vORW7*6C4$?TpY%E2 z!S!*tM097Rv~tl^m4vM>Hv^YS(7|0Lth(hXsY3928f%&ta!|!#ZcYD9q!_*)UX4)g zFwnQH@vi4B(kZ1=99?%)t@ealoSM=|C6!cCNhOt3QaR@2zf1-^ifaM!$^ZZW07*qo IM6N<$f;5VEvH$=8 From 252e05e963e00b8ac1eee11c450df6971c5299d9 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 26 Jul 2017 17:12:02 +0200 Subject: [PATCH 12/30] fix(container-details): add missing Created field from ContainerDetailsViewModel (#1075) --- app/models/docker/containerDetails.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/docker/containerDetails.js b/app/models/docker/containerDetails.js index 58ecd17d7..65ce86069 100644 --- a/app/models/docker/containerDetails.js +++ b/app/models/docker/containerDetails.js @@ -1,6 +1,7 @@ function ContainerDetailsViewModel(data) { this.Id = data.Id; this.State = data.State; + this.Created = data.Created; this.Name = data.Name; this.NetworkSettings = data.NetworkSettings; this.Args = data.Args; From 5110f83fae3c8c922ee6387b603620a9baf6ef88 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 27 Jul 2017 10:46:29 +0200 Subject: [PATCH 13/30] fix(rest): fix an issue with rest factories using $http (#1077) --- app/rest/docker/containerLogs.js | 4 ++-- app/rest/docker/containerTop.js | 6 ++++-- app/rest/docker/serviceLogs.js | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/rest/docker/containerLogs.js b/app/rest/docker/containerLogs.js index c7798066f..6b77fabb9 100644 --- a/app/rest/docker/containerLogs.js +++ b/app/rest/docker/containerLogs.js @@ -5,7 +5,7 @@ angular.module('portainer.rest') get: function (id, params, callback) { $http({ method: 'GET', - url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/logs', + url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/containers/' + id + '/logs', params: { 'stdout': params.stdout || 0, 'stderr': params.stderr || 0, @@ -13,7 +13,7 @@ angular.module('portainer.rest') 'tail': params.tail || 'all' } }).success(callback).error(function (data, status, headers, config) { - console.log(error, data); + console.log(data); }); } }; diff --git a/app/rest/docker/containerTop.js b/app/rest/docker/containerTop.js index 5df3020fa..57e51d51c 100644 --- a/app/rest/docker/containerTop.js +++ b/app/rest/docker/containerTop.js @@ -5,11 +5,13 @@ angular.module('portainer.rest') get: function (id, params, callback, errorCallback) { $http({ method: 'GET', - url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/top', + url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/containers/' + id + '/top', params: { ps_args: params.ps_args } - }).success(callback); + }).success(callback).error(function (data, status, headers, config) { + console.log(data); + }); } }; }]); diff --git a/app/rest/docker/serviceLogs.js b/app/rest/docker/serviceLogs.js index cefb57c6e..87e03e6b9 100644 --- a/app/rest/docker/serviceLogs.js +++ b/app/rest/docker/serviceLogs.js @@ -5,7 +5,7 @@ angular.module('portainer.rest') get: function (id, params, callback) { $http({ method: 'GET', - url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/services/' + id + '/logs', + url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/services/' + id + '/logs', params: { 'stdout': params.stdout || 0, 'stderr': params.stderr || 0, @@ -13,7 +13,7 @@ angular.module('portainer.rest') 'tail': params.tail || 'all' } }).success(callback).error(function (data, status, headers, config) { - console.log(error, data); + console.log(data); }); } }; From 0d6ab099ac2ea6a60e34d27180f5df54431b8c15 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 1 Aug 2017 11:24:44 +0200 Subject: [PATCH 14/30] feat(templates): update LinuxServer.io templates feed URL (#1089) --- api/http/handler/templates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/http/handler/templates.go b/api/http/handler/templates.go index 516fc892e..6f3ba019f 100644 --- a/api/http/handler/templates.go +++ b/api/http/handler/templates.go @@ -20,7 +20,7 @@ type TemplatesHandler struct { } const ( - containerTemplatesURLLinuxServerIo = "http://tools.linuxserver.io/portainer.json" + containerTemplatesURLLinuxServerIo = "https://tools.linuxserver.io/portainer.json" ) // NewTemplatesHandler returns a new instance of TemplatesHandler. From 86c450bd91ba84eab06072a8864e7578f3273ed7 Mon Sep 17 00:00:00 2001 From: tfenster Date: Fri, 4 Aug 2017 07:54:03 +0200 Subject: [PATCH 15/30] feat(templates): Use container name as hostname (#1084) --- app/services/templateService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/templateService.js b/app/services/templateService.js index 9a4efc9f1..a28e866ea 100644 --- a/app/services/templateService.js +++ b/app/services/templateService.js @@ -41,6 +41,7 @@ angular.module('portainer.services') configuration.HostConfig.Privileged = template.Privileged; configuration.HostConfig.RestartPolicy = { Name: template.RestartPolicy }; configuration.name = containerName; + configuration.Hostname = containerName; configuration.Image = template.Image; configuration.Env = TemplateHelper.EnvToStringArray(template.Env, containerMapping); configuration.Cmd = ContainerHelper.commandStringToArray(template.Command); From cf5c3ee5366d35da7dbe9be626e4705cc01df312 Mon Sep 17 00:00:00 2001 From: Liam Cottam Date: Fri, 4 Aug 2017 07:02:26 +0100 Subject: [PATCH 16/30] fix(container-console): fix an issue with scrollbar (#932) (#1086) --- .../containerConsole/containerConsoleController.js | 7 ++++++- vendor.yml | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/components/containerConsole/containerConsoleController.js b/app/components/containerConsole/containerConsoleController.js index 2ff1214ad..aa37611f4 100644 --- a/app/components/containerConsole/containerConsoleController.js +++ b/app/components/containerConsole/containerConsoleController.js @@ -38,7 +38,7 @@ function ($scope, $stateParams, Container, Image, EndpointProvider, Notification $scope.connect = function() { $('#loadConsoleSpinner').show(); - var termWidth = Math.round($('#terminal-container').width() / 8.2); + var termWidth = Math.floor(($('#terminal-container').width() - 20) / 8.39); var termHeight = 30; var command = $scope.formValues.isCustomCommand ? $scope.formValues.customCommand : $scope.formValues.command; @@ -97,6 +97,11 @@ function ($scope, $stateParams, Container, Image, EndpointProvider, Notification term.open(document.getElementById('terminal-container'), true); term.resize(width, height); term.setOption('cursorBlink', true); + term.fit(); + + window.onresize = function() { + term.fit(); + }; socket.onmessage = function (e) { term.write(e.data); diff --git a/vendor.yml b/vendor.yml index d2c3bac42..4c1e83937 100644 --- a/vendor.yml +++ b/vendor.yml @@ -12,6 +12,7 @@ js: - bower_components/splitargs/src/splitargs.js - bower_components/toastr/toastr.js - bower_components/xterm.js/dist/xterm.js + - bower_components/xterm.js/dist/addons/fit/fit.js - assets/js/legend.js minified: - bower_components/jquery/dist/jquery.min.js @@ -25,6 +26,7 @@ js: - bower_components/splitargs/src/splitargs.js - bower_components/toastr/toastr.min.js - bower_components/xterm.js/dist/xterm.js + - bower_components/xterm.js/dist/addons/fit/fit.js - assets/js/legend.js css: regular: From b5429f75047a2ead9e15db7746f107115a65cdc8 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 4 Aug 2017 08:09:29 +0200 Subject: [PATCH 17/30] docs(README): add code climate badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 14a404772..58c6a787c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![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/stable/?badge=stable) [![Codefresh build status]( https://g.codefresh.io/api/badges/build?repoOwner=portainer&repoName=portainer&branch=develop&pipelineName=portainer-ci&accountName=deviantony&type=cf-1)]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci) +[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer) [![Slack](https://portainer.io/slack/badge.svg)](https://portainer.io/slack/) [![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) From 12adeadc940e48ca3cdc1e85347ed5818fcf1f95 Mon Sep 17 00:00:00 2001 From: Liam Cottam Date: Sun, 6 Aug 2017 09:42:38 +0100 Subject: [PATCH 18/30] fix(container-details): connected network section disappearing (#1092) --- app/components/container/container.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/components/container/container.html b/app/components/container/container.html index ff270be5c..39069c781 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -262,7 +262,7 @@
    -
    +
    @@ -296,6 +296,9 @@ + + No networks connected. +
    From d7769dec33434c0ffbe64bd35f0536d49178c27d Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 9 Aug 2017 10:40:46 +0200 Subject: [PATCH 19/30] =?UTF-8?q?fix(images):=20fix=20the=20way=20the=20re?= =?UTF-8?q?gistry=20and=20image=20name=20are=20extracted=20fr=E2=80=A6=20(?= =?UTF-8?q?#1099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(images): fix the way the registry and image name are extracted from a repository --- app/helpers/imageHelper.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/helpers/imageHelper.js b/app/helpers/imageHelper.js index 7c3012fbc..324f28519 100644 --- a/app/helpers/imageHelper.js +++ b/app/helpers/imageHelper.js @@ -8,10 +8,15 @@ angular.module('portainer.helpers') var slashCount = _.countBy(repository)['/']; var registry = null; var image = repository; - if (slashCount > 1) { - // assume something/some/thing[/...] + if (slashCount >= 1) { + // assume something/something[/...] registry = repository.substr(0, repository.indexOf('/')); - image = repository.substr(repository.indexOf('/') + 1); + // assume valid DNS name or IP (contains at least one '.') + if (_.countBy(registry)['.'] > 0) { + image = repository.substr(repository.indexOf('/') + 1); + } else { + registry = null; + } } return { From 04ea81e7cd8401690058c4b4264452bf9d7a05eb Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 9 Aug 2017 15:30:50 +0200 Subject: [PATCH 20/30] feat(service): support the Order field for Update Configuration (#1101) --- .../createService/createServiceController.js | 4 +- .../createService/createservice.html | 46 ++++++++++++++----- .../service/includes/updateconfig.html | 24 +++++++++- app/components/service/serviceController.js | 4 +- app/models/docker/service.js | 2 + 5 files changed, 65 insertions(+), 15 deletions(-) diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index dc9831f92..a976c8e8c 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -25,6 +25,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic PlacementConstraints: [], PlacementPreferences: [], UpdateDelay: 0, + UpdateOrder: 'stop-first', FailureAction: 'pause', Secrets: [], AccessControlData: new AccessControlFormData() @@ -199,7 +200,8 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic config.UpdateConfig = { Parallelism: input.Parallelism || 0, Delay: input.UpdateDelay || 0, - FailureAction: input.FailureAction + FailureAction: input.FailureAction, + Order: input.UpdateOrder }; } diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index 5c8ab23d9..94aa92794 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -377,12 +377,12 @@
    - -
    + +
    -
    -

    +

    +

    Maximum number of tasks to be updated simultaneously (0 to update all at once).

    @@ -390,12 +390,12 @@
    - -
    + +
    -
    -

    +

    +

    Amount of time between updates.

    @@ -403,15 +403,39 @@
    -
    - -
    + +
    +
    +
    +

    + Action taken on failure to start after update. +

    +
    +
    + +
    + + +
    +
    + + +
    +
    +
    +

    + Operation order on failure. +

    +
    + +
    +
    diff --git a/app/components/service/includes/updateconfig.html b/app/components/service/includes/updateconfig.html index 0a0b9180d..469b715d2 100644 --- a/app/components/service/includes/updateconfig.html +++ b/app/components/service/includes/updateconfig.html @@ -47,18 +47,38 @@

    + + Order + +
    + + +
    + + +

    + Operation order on failure. +

    + + - +
    diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index 94aa92794..d34ad9bd1 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -101,7 +101,7 @@
    - +
    diff --git a/app/components/createVolume/createvolume.html b/app/components/createVolume/createvolume.html index 2ca08ec84..c9e795dd7 100644 --- a/app/components/createVolume/createvolume.html +++ b/app/components/createVolume/createvolume.html @@ -65,7 +65,7 @@
    - +
    diff --git a/app/components/settingsAuthentication/settingsAuthentication.html b/app/components/settingsAuthentication/settingsAuthentication.html new file mode 100644 index 000000000..6c94043f8 --- /dev/null +++ b/app/components/settingsAuthentication/settingsAuthentication.html @@ -0,0 +1,254 @@ + + + + + + Settings > Authentication + + + +
    +
    + + + +
    +
    + Authentication method +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + Information +
    +
    + + When using internal authentication, Portainer will encrypt user passwords and store credentials locally. + +
    +
    + + When using LDAP authentication, Portainer will delegate user authentication to a LDAP server (exception for the admin user that always use internal authentication). +

    + + Users still need to be created in Portainer beforehand. +

    +
    +
    + +
    +
    + LDAP configuration +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + + +
    +
    + +
    + LDAP security +
    + + +
    +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + + +
    +
    + + +
    +
    + + + +
    + +
    + +
    + + + {{ formValues.TLSCACert.name }} + + + + +
    +
    + +
    + + +
    + +
    + + +
    +
    + +
    + User search configurations +
    + + +
    + +
    + + Extra search configuration + +
    + +
    + +
    + +
    + + +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    + +
    + + add search configuration + +
    + +
    + +
    + + +
    +
    + + + +
    +
    + + +
    +
    +
    +
    +
    diff --git a/app/components/settingsAuthentication/settingsAuthenticationController.js b/app/components/settingsAuthentication/settingsAuthenticationController.js new file mode 100644 index 000000000..faf16873b --- /dev/null +++ b/app/components/settingsAuthentication/settingsAuthenticationController.js @@ -0,0 +1,93 @@ +angular.module('settingsAuthentication', []) +.controller('SettingsAuthenticationController', ['$q', '$scope', 'Notifications', 'SettingsService', 'FileUploadService', +function ($q, $scope, Notifications, SettingsService, FileUploadService) { + + $scope.state = { + successfulConnectivityCheck: false, + failedConnectivityCheck: false, + uploadInProgress: false + }; + + $scope.formValues = { + TLSCACert: '' + }; + + $scope.addSearchConfiguration = function() { + $scope.LDAPSettings.SearchSettings.push({ BaseDN: '', UserNameAttribute: '', Filter: '' }); + }; + + $scope.removeSearchConfiguration = function(index) { + $scope.LDAPSettings.SearchSettings.splice(index, 1); + }; + + $scope.LDAPConnectivityCheck = function() { + $('#connectivityCheckSpinner').show(); + var settings = $scope.settings; + var TLSCAFile = $scope.formValues.TLSCACert !== settings.LDAPSettings.TLSConfig.TLSCACert ? $scope.formValues.TLSCACert : null; + + var uploadRequired = ($scope.LDAPSettings.TLSConfig.TLS || $scope.LDAPSettings.StartTLS) && !$scope.LDAPSettings.TLSConfig.TLSSkipVerify; + $scope.state.uploadInProgress = uploadRequired; + + $q.when(!uploadRequired || FileUploadService.uploadLDAPTLSFiles(TLSCAFile, null, null)) + .then(function success(data) { + return SettingsService.checkLDAPConnectivity(settings); + }) + .then(function success(data) { + $scope.state.failedConnectivityCheck = false; + $scope.state.successfulConnectivityCheck = true; + Notifications.success('Connection to LDAP successful'); + }) + .catch(function error(err) { + $scope.state.failedConnectivityCheck = true; + $scope.state.successfulConnectivityCheck = false; + Notifications.error('Failure', err, 'Connection to LDAP failed'); + }) + .finally(function final() { + $scope.state.uploadInProgress = false; + $('#connectivityCheckSpinner').hide(); + }); + }; + + $scope.saveSettings = function() { + $('#updateSettingsSpinner').show(); + var settings = $scope.settings; + var TLSCAFile = $scope.formValues.TLSCACert !== settings.LDAPSettings.TLSConfig.TLSCACert ? $scope.formValues.TLSCACert : null; + + var uploadRequired = ($scope.LDAPSettings.TLSConfig.TLS || $scope.LDAPSettings.StartTLS) && !$scope.LDAPSettings.TLSConfig.TLSSkipVerify; + $scope.state.uploadInProgress = uploadRequired; + + $q.when(!uploadRequired || FileUploadService.uploadLDAPTLSFiles(TLSCAFile, null, null)) + .then(function success(data) { + return SettingsService.update(settings); + }) + .then(function success(data) { + Notifications.success('Authentication settings updated'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update authentication settings'); + }) + .finally(function final() { + $scope.state.uploadInProgress = false; + $('#updateSettingsSpinner').hide(); + }); + }; + + function initView() { + $('#loadingViewSpinner').show(); + SettingsService.settings() + .then(function success(data) { + var settings = data; + $scope.settings = settings; + $scope.LDAPSettings = settings.LDAPSettings; + $scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve application settings'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); +}]); diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index 3d8d6a2c3..2f4c79424 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -69,6 +69,9 @@ - +
    diff --git a/app/components/user/user.html b/app/components/user/user.html index 62f0a1e75..b1a6d3478 100644 --- a/app/components/user/user.html +++ b/app/components/user/user.html @@ -32,29 +32,6 @@ - @@ -62,7 +39,7 @@
    -
    +
    diff --git a/app/components/user/userController.js b/app/components/user/userController.js index 348457c51..dfb3c0489 100644 --- a/app/components/user/userController.js +++ b/app/components/user/userController.js @@ -1,6 +1,6 @@ angular.module('user', []) -.controller('UserController', ['$q', '$scope', '$state', '$stateParams', 'UserService', 'ModalService', 'Notifications', -function ($q, $scope, $state, $stateParams, UserService, ModalService, Notifications) { +.controller('UserController', ['$q', '$scope', '$state', '$stateParams', 'UserService', 'ModalService', 'Notifications', 'SettingsService', +function ($q, $scope, $state, $stateParams, UserService, ModalService, Notifications, SettingsService) { $scope.state = { updatePasswordError: '' @@ -72,12 +72,14 @@ function ($q, $scope, $state, $stateParams, UserService, ModalService, Notificat function initView() { $('#loadingViewSpinner').show(); $q.all({ - user: UserService.user($stateParams.id) + user: UserService.user($stateParams.id), + settings: SettingsService.publicSettings() }) .then(function success(data) { var user = data.user; $scope.user = user; $scope.formValues.Administrator = user.Role === 1 ? true : false; + $scope.AuthenticationMethod = data.settings.AuthenticationMethod; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve user information'); diff --git a/app/components/userSettings/userSettings.html b/app/components/userSettings/userSettings.html index 8a20d22f8..c7f5405f7 100644 --- a/app/components/userSettings/userSettings.html +++ b/app/components/userSettings/userSettings.html @@ -1,5 +1,6 @@ + User settings @@ -58,7 +59,11 @@
    - + + + + You cannot change your password when using LDAP authentication. +
    diff --git a/app/components/userSettings/userSettingsController.js b/app/components/userSettings/userSettingsController.js index d8e8f4d43..2146d58e0 100644 --- a/app/components/userSettings/userSettingsController.js +++ b/app/components/userSettings/userSettingsController.js @@ -1,6 +1,6 @@ angular.module('userSettings', []) -.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications', -function ($scope, $state, $sanitize, Authentication, UserService, Notifications) { +.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications', 'SettingsService', +function ($scope, $state, $sanitize, Authentication, UserService, Notifications, SettingsService) { $scope.formValues = { currentPassword: '', newPassword: '', @@ -26,4 +26,19 @@ function ($scope, $state, $sanitize, Authentication, UserService, Notifications) } }); }; + + function initView() { + SettingsService.publicSettings() + .then(function success(data) { + $scope.AuthenticationMethod = data.AuthenticationMethod; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve application settings'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); }]); diff --git a/app/components/users/users.html b/app/components/users/users.html index 68e0ef9ea..79c9999d3 100644 --- a/app/components/users/users.html +++ b/app/components/users/users.html @@ -17,7 +17,10 @@
    - +
    @@ -27,8 +30,8 @@
    -
    - +
    +
    @@ -38,8 +41,8 @@
    -
    - +
    +
    @@ -95,7 +98,7 @@
    - + {{ state.userCreationError }} @@ -140,19 +143,26 @@ - + Name - + Role + + + Authentication + + + + @@ -166,6 +176,10 @@ {{ user.RoleName }} + + Internal + LDAP + Edit diff --git a/app/components/users/usersController.js b/app/components/users/usersController.js index bc595c2f6..0184e84f0 100644 --- a/app/components/users/usersController.js +++ b/app/components/users/usersController.js @@ -1,6 +1,6 @@ angular.module('users', []) -.controller('UsersController', ['$q', '$scope', '$state', '$sanitize', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication', -function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication) { +.controller('UsersController', ['$q', '$scope', '$state', '$sanitize', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication', 'SettingsService', +function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication, SettingsService) { $scope.state = { userCreationError: '', selectedItemCount: 0, @@ -140,13 +140,15 @@ function ($q, $scope, $state, $sanitize, UserService, TeamService, TeamMembershi $q.all({ users: UserService.users(true), teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID), - memberships: TeamMembershipService.memberships() + memberships: TeamMembershipService.memberships(), + settings: SettingsService.publicSettings() }) .then(function success(data) { var users = data.users; assignTeamLeaders(users, data.memberships); $scope.users = users; $scope.teams = data.teams; + $scope.AuthenticationMethod = data.settings.AuthenticationMethod; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve users and teams'); diff --git a/app/models/api/settings/ldapSettings.js b/app/models/api/settings/ldapSettings.js new file mode 100644 index 000000000..2da574598 --- /dev/null +++ b/app/models/api/settings/ldapSettings.js @@ -0,0 +1,12 @@ +function LDAPSettingsViewModel(data) { + this.ReaderDN = data.ReaderDN; + this.Password = data.Password; + this.URL = data.URL; + this.SearchSettings = data.SearchSettings; +} + +function LDAPSearchSettings(BaseDN, UsernameAttribute, Filter) { + this.BaseDN = BaseDN; + this.UsernameAttribute = UsernameAttribute; + this.Filter = Filter; +} diff --git a/app/models/api/settings.js b/app/models/api/settings/settings.js similarity index 70% rename from app/models/api/settings.js rename to app/models/api/settings/settings.js index 889d18ad1..b8d473172 100644 --- a/app/models/api/settings.js +++ b/app/models/api/settings/settings.js @@ -3,4 +3,6 @@ function SettingsViewModel(data) { this.LogoURL = data.LogoURL; this.BlackListedLabels = data.BlackListedLabels; this.DisplayExternalContributors = data.DisplayExternalContributors; + this.AuthenticationMethod = data.AuthenticationMethod; + this.LDAPSettings = data.LDAPSettings; } diff --git a/app/models/api/user.js b/app/models/api/user.js index 1177fc137..27afa4b67 100644 --- a/app/models/api/user.js +++ b/app/models/api/user.js @@ -7,5 +7,6 @@ function UserViewModel(data) { } else { this.RoleName = 'user'; } + this.AuthenticationMethod = data.AuthenticationMethod; this.Checked = false; } diff --git a/app/rest/api/settings.js b/app/rest/api/settings.js index 46ee8f336..9a70d8885 100644 --- a/app/rest/api/settings.js +++ b/app/rest/api/settings.js @@ -1,8 +1,10 @@ angular.module('portainer.rest') .factory('Settings', ['$resource', 'API_ENDPOINT_SETTINGS', function SettingsFactory($resource, API_ENDPOINT_SETTINGS) { 'use strict'; - return $resource(API_ENDPOINT_SETTINGS, {}, { + return $resource(API_ENDPOINT_SETTINGS + '/:subResource/:action', {}, { get: { method: 'GET' }, - update: { method: 'PUT' } + update: { method: 'PUT' }, + publicSettings: { method: 'GET', params: { subResource: 'public' } }, + checkLDAPConnectivity: { method: 'PUT', params: { subResource: 'authentication', action: 'checkLDAP' } } }); }]); diff --git a/app/services/api/settingsService.js b/app/services/api/settingsService.js index f467ff844..4455abb97 100644 --- a/app/services/api/settingsService.js +++ b/app/services/api/settingsService.js @@ -22,5 +22,24 @@ angular.module('portainer.services') return Settings.update({}, settings).$promise; }; + service.publicSettings = function() { + var deferred = $q.defer(); + + Settings.publicSettings().$promise + .then(function success(data) { + var settings = new SettingsViewModel(data); + deferred.resolve(settings); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve application settings', err: err }); + }); + + return deferred.promise; + }; + + service.checkLDAPConnectivity = function(settings) { + return Settings.checkLDAPConnectivity({}, settings).$promise; + }; + return service; }]); diff --git a/app/services/fileUpload.js b/app/services/fileUpload.js index 7811afed4..8b256ca00 100644 --- a/app/services/fileUpload.js +++ b/app/services/fileUpload.js @@ -1,44 +1,44 @@ angular.module('portainer.services') .factory('FileUploadService', ['$q', 'Upload', function FileUploadFactory($q, Upload) { 'use strict'; - function uploadFile(url, file) { - var deferred = $q.defer(); - Upload.upload({ - url: url, - data: { file: file } - }).then(function success(data) { - deferred.resolve(data); - }, function error(e) { - deferred.reject(e); - }, function progress(evt) { - }); - return deferred.promise; - } - return { - uploadTLSFilesForEndpoint: function(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) { - var deferred = $q.defer(); - var queue = []; - if (TLSCAFile) { - var uploadTLSCA = uploadFile('api/upload/tls/' + endpointID + '/ca', TLSCAFile); - queue.push(uploadTLSCA); - } - if (TLSCertFile) { - var uploadTLSCert = uploadFile('api/upload/tls/' + endpointID + '/cert', TLSCertFile); - queue.push(uploadTLSCert); - } - if (TLSKeyFile) { - var uploadTLSKey = uploadFile('api/upload/tls/' + endpointID + '/key', TLSKeyFile); - queue.push(uploadTLSKey); - } - $q.all(queue).then(function (data) { - deferred.resolve(data); - }, function (err) { - deferred.reject(err); - }, function update(evt) { - deferred.notify(evt); - }); - return deferred.promise; + var service = {}; + + function uploadFile(url, file) { + return Upload.upload({ url: url, data: { file: file }}); + } + + service.uploadLDAPTLSFiles = function(TLSCAFile, TLSCertFile, TLSKeyFile) { + var queue = []; + + if (TLSCAFile) { + queue.push(uploadFile('api/upload/tls/ca?folder=ldap', TLSCAFile)); } + if (TLSCertFile) { + queue.push(uploadFile('api/upload/tls/cert?folder=ldap', TLSCertFile)); + } + if (TLSKeyFile) { + queue.push(uploadFile('api/upload/tls/key?folder=ldap', TLSKeyFile)); + } + + return $q.all(queue); }; + + service.uploadTLSFilesForEndpoint = function(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) { + var queue = []; + + if (TLSCAFile) { + queue.push(uploadFile('api/upload/tls/ca?folder=' + endpointID, TLSCAFile)); + } + if (TLSCertFile) { + queue.push(uploadFile('api/upload/tls/cert?folder=' + endpointID, TLSCertFile)); + } + if (TLSKeyFile) { + queue.push(uploadFile('api/upload/tls/key?folder=' + endpointID, TLSKeyFile)); + } + + return $q.all(queue); + }; + + return service; }]); diff --git a/app/services/stateManager.js b/app/services/stateManager.js index ae7e7838e..9385237a7 100644 --- a/app/services/stateManager.js +++ b/app/services/stateManager.js @@ -44,7 +44,7 @@ angular.module('portainer.services') deferred.resolve(state); } else { $q.all({ - settings: SettingsService.settings(), + settings: SettingsService.publicSettings(), status: StatusService.status() }) .then(function success(data) { diff --git a/assets/css/app.css b/assets/css/app.css index e9b61a130..2b2f4bb80 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -82,10 +82,6 @@ a[ng-click]{ margin-right: 5px; } -.fa.green-icon { - color: #23ae89; -} - .tooltip.portainer-tooltip .tooltip-inner { font-family: Montserrat; background-color: #ffffff; @@ -106,6 +102,10 @@ a[ng-click]{ color: #337ab7; } +.fa.green-icon { + color: #23ae89; +} + .fa.red-icon { color: #ae2323; } @@ -517,4 +517,4 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { .monospaced { font-family: monospace; font-weight: 600; -} \ No newline at end of file +} From 3d5f9a76e4e9065dfb53cacda9f47fd193ebbab1 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 10 Aug 2017 15:25:53 +0200 Subject: [PATCH 22/30] fix(team-details): fix an issue when sorting columns (#1106) --- app/components/team/team.html | 6 +++--- app/components/teams/teams.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/team/team.html b/app/components/team/team.html index 727372a0d..d292f6fd8 100644 --- a/app/components/team/team.html +++ b/app/components/team/team.html @@ -65,7 +65,7 @@ - + Name @@ -125,14 +125,14 @@ - + Name - + Team Role diff --git a/app/components/teams/teams.html b/app/components/teams/teams.html index 2d082186d..6e42cb2c7 100644 --- a/app/components/teams/teams.html +++ b/app/components/teams/teams.html @@ -95,7 +95,7 @@ - + Name From d814f3aaa4fe6bf98b91f400cd3972e3a76c09c0 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 11 Aug 2017 09:46:55 +0200 Subject: [PATCH 23/30] fix(networks): review how networks are loaded for usage in multiple views (#1104) --- .../container/containerController.js | 38 +++++++-------- .../createContainerController.js | 37 +++++++------- .../createService/createServiceController.js | 5 +- .../templates/templatesController.js | 29 +++++------ app/services/docker/networkService.js | 48 +++++++------------ 5 files changed, 67 insertions(+), 90 deletions(-) diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index 22acf056c..452a835f8 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -1,6 +1,6 @@ angular.module('container', []) -.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'Notifications', 'Pagination', 'ModalService', -function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, Notifications, Pagination, ModalService) { +.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', +function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService) { $scope.activityTime = 0; $scope.portBindings = []; $scope.config = { @@ -213,25 +213,21 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con }); }; - Network.query({}, function (d) { - var networks = d; - if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - networks = d.filter(function (network) { - if (network.Scope === 'global') { - return network; - } - }); - networks.push({Name: 'bridge'}); - networks.push({Name: 'host'}); - networks.push({Name: 'none'}); - } - $scope.availableNetworks = networks; - if (!_.find(networks, {'Name': 'bridge'})) { - networks.push({Name: 'nat'}); - } - }, function (e) { - Notifications.error('Failure', e, 'Unable to retrieve networks'); - }); + var provider = $scope.applicationState.endpoint.mode.provider; + var apiVersion = $scope.applicationState.endpoint.apiVersion; + NetworkService.networks( + provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', + false, + provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25, + provider === 'DOCKER_SWARM' + ) + .then(function success(data) { + var networks = data; + $scope.availableNetworks = networks; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve networks'); + }); update(); }]); diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index 048e3f2ce..bc7163929 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -1,8 +1,8 @@ // @@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', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', -function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator) { +.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', +function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator) { $scope.formValues = { alwaysPull: true, @@ -240,26 +240,25 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Notifications.error('Failure', e, 'Unable to retrieve volumes'); }); - Network.query({}, function (d) { - var networks = d; - if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - networks = d.filter(function (network) { - if (network.Scope === 'global') { - return network; - } - }); - $scope.globalNetworkCount = networks.length; - networks.push({Name: 'bridge'}); - networks.push({Name: 'host'}); - networks.push({Name: 'none'}); - } - networks.push({Name: 'container'}); + var provider = $scope.applicationState.endpoint.mode.provider; + var apiVersion = $scope.applicationState.endpoint.apiVersion; + NetworkService.networks( + provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', + false, + provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25, + provider === 'DOCKER_SWARM' + ) + .then(function success(data) { + var networks = data; + networks.push({ Name: 'container' }); $scope.availableNetworks = networks; - if (!_.find(networks, {'Name': 'bridge'})) { + + if (_.find(networks, {'Name': 'nat'})) { $scope.config.HostConfig.NetworkMode = 'nat'; } - }, function (e) { - Notifications.error('Failure', e, 'Unable to retrieve networks'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve networks'); }); Container.query({}, function (d) { diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index a976c8e8c..d7723503d 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -305,10 +305,11 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic function initView() { $('#loadingViewSpinner').show(); var apiVersion = $scope.applicationState.endpoint.apiVersion; + $q.all({ volumes: VolumeService.volumes(), - networks: NetworkService.retrieveSwarmNetworks(), - secrets: apiVersion >= 1.25 ? SecretService.secrets() : [] + secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], + networks: NetworkService.networks(true, true, false, false) }) .then(function success(data) { $scope.availableVolumes = data.volumes; diff --git a/app/components/templates/templatesController.js b/app/components/templates/templatesController.js index 5b62b5b1b..9a8d90455 100644 --- a/app/components/templates/templatesController.js +++ b/app/components/templates/templatesController.js @@ -144,27 +144,20 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer return containerMapping; } - function filterNetworksBasedOnProvider(networks) { - var endpointProvider = $scope.applicationState.endpoint.mode.provider; - if (endpointProvider === 'DOCKER_SWARM' || endpointProvider === 'DOCKER_SWARM_MODE') { - if (endpointProvider === 'DOCKER_SWARM') { - networks = NetworkService.filterGlobalNetworks(networks); - } else { - networks = NetworkService.filterSwarmModeAttachableNetworks(networks); - } - $scope.globalNetworkCount = networks.length; - NetworkService.addPredefinedLocalNetworks(networks); - } - return networks; - } - function initTemplates() { var templatesKey = $stateParams.key; + var provider = $scope.applicationState.endpoint.mode.provider; + var apiVersion = $scope.applicationState.endpoint.apiVersion; + $q.all({ templates: TemplateService.getTemplates(templatesKey), containers: ContainerService.getContainers(0), - networks: NetworkService.networks(), - volumes: VolumeService.getVolumes() + volumes: VolumeService.getVolumes(), + networks: NetworkService.networks( + provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', + false, + provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25, + provider === 'DOCKER_SWARM') }) .then(function success(data) { $scope.templates = data.templates; @@ -174,8 +167,10 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer }); $scope.availableCategories = _.sortBy(_.uniq(availableCategories)); $scope.runningContainers = data.containers; - $scope.availableNetworks = filterNetworksBasedOnProvider(data.networks); $scope.availableVolumes = data.volumes.Volumes; + var networks = data.networks; + $scope.availableNetworks = networks; + $scope.globalNetworkCount = networks.length; }) .catch(function error(err) { $scope.templates = []; diff --git a/app/services/docker/networkService.js b/app/services/docker/networkService.js index b3bfc236a..013311cf0 100644 --- a/app/services/docker/networkService.js +++ b/app/services/docker/networkService.js @@ -3,21 +3,29 @@ angular.module('portainer.services') 'use strict'; var service = {}; - service.networks = function() { - return Network.query({}).$promise; - }; - - service.retrieveSwarmNetworks = function() { + service.networks = function(localNetworks, swarmNetworks, swarmAttachableNetworks, globalNetworks) { var deferred = $q.defer(); - service.networks() + Network.query({}).$promise .then(function success(data) { - var networks = data.filter(function (network) { - if (network.Scope === 'swarm') { + var networks = data; + + var filteredNetworks = networks.filter(function(network) { + if (localNetworks && network.Scope === 'local') { + return network; + } + if (swarmNetworks && network.Scope === 'swarm') { + return network; + } + if (swarmAttachableNetworks && network.Scope === 'swarm' && network.Attachable === true) { + return network; + } + if (globalNetworks && network.Scope === 'global') { return network; } }); - deferred.resolve(networks); + + deferred.resolve(filteredNetworks); }) .catch(function error(err) { deferred.reject({msg: 'Unable to retrieve networks', err: err}); @@ -26,27 +34,5 @@ angular.module('portainer.services') return deferred.promise; }; - service.filterGlobalNetworks = function(networks) { - return networks.filter(function (network) { - if (network.Scope === 'global') { - return network; - } - }); - }; - - service.filterSwarmModeAttachableNetworks = function(networks) { - return networks.filter(function (network) { - if (network.Scope === 'swarm' && network.Attachable === true) { - return network; - } - }); - }; - - service.addPredefinedLocalNetworks = function(networks) { - networks.push({Scope: 'local', Name: 'bridge'}); - networks.push({Scope: 'local', Name: 'host'}); - networks.push({Scope: 'local', Name: 'none'}); - }; - return service; }]); From c85aa0739dce5e3685d15ca16c05c1f7d8bec643 Mon Sep 17 00:00:00 2001 From: Thomas Krzero Date: Sun, 13 Aug 2017 12:17:41 +0200 Subject: [PATCH 24/30] feat(container-details): add the ability to re-create, duplicate and edit a container (#855) --- app/app.js | 2 +- app/components/container/container.html | 2 + .../container/containerController.js | 71 ++++- .../createContainerController.js | 251 +++++++++++++++++- .../createContainer/createcontainer.html | 8 +- .../porImageRegistryController.js | 6 +- app/helpers/containerHelper.js | 51 ++++ app/models/docker/containerDetails.js | 1 + app/services/modalService.js | 14 + 9 files changed, 386 insertions(+), 20 deletions(-) diff --git a/app/app.js b/app/app.js index eb80ea85b..d7774719f 100644 --- a/app/app.js +++ b/app/app.js @@ -244,7 +244,7 @@ angular.module('portainer', [ } }) .state('actions.create.container', { - url: '/container', + url: '/container/:from', views: { 'content@': { templateUrl: 'app/components/createContainer/createcontainer.html', diff --git a/app/components/container/container.html b/app/components/container/container.html index 39069c781..b399a1eca 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -20,6 +20,8 @@ + +
    diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index 452a835f8..1af934c78 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -1,6 +1,6 @@ angular.module('container', []) -.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', -function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService) { +.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', +function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService, ResourceControlService, RegistryService, ImageService) { $scope.activityTime = 0; $scope.portBindings = []; $scope.config = { @@ -196,6 +196,73 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con }); }; + $scope.duplicate = function() { + ModalService.confirmExperimentalFeature(function (experimental) { + if(!experimental) { return; } + $state.go('actions.create.container', {from: $stateParams.id}, {reload: true}); + }); + }; + + $scope.recreate = function() { + ModalService.confirmExperimentalFeature(function (experimental) { + if(!experimental) { return; } + ModalService.confirm({ + title: 'Are you sure ?', + message: 'You\'re about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.', + buttons: { + confirm: { + label: 'Recreate', + className: 'btn-danger' + } + }, + callback: function onConfirm(confirmed) { + if(!confirmed) { return; } + else { + $('#loadingViewSpinner').show(); + var container = $scope.container; + var config = ContainerHelper.configFromContainer(container.Model); + ContainerService.remove(container, true) + .then(function success() { + return RegistryService.retrieveRegistryFromRepository(container.Config.Image); + }) + .then(function success(data) { + return ImageService.pullImage(container.Config.Image, data, true); + }) + .then(function success() { + return ContainerService.createAndStartContainer(config); + }) + .then(function success(data) { + if (!container.ResourceControl) { + return true; + } else { + var containerIdentifier = data.Id; + var resourceControl = container.ResourceControl; + var users = resourceControl.UserAccesses.map(function(u) { + return u.UserId; + }); + var teams = resourceControl.TeamAccesses.map(function(t) { + return t.TeamId; + }); + return ResourceControlService.createResourceControl(resourceControl.AdministratorsOnly, + users, teams, containerIdentifier, 'container', []); + } + }) + .then(function success(data) { + Notifications.success('Container successfully re-created'); + $state.go('containers', {}, {reload: true}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to re-create container'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + } + }); + }); + }; + $scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) { $('#joinNetworkSpinner').show(); Network.connect({id: networkId}, { Container: $stateParams.id }, function (d) { diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index bc7163929..ecc556182 100644 --- a/app/components/createContainer/createContainerController.js +++ b/app/components/createContainer/createContainerController.js @@ -1,14 +1,13 @@ // @@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', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', -function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator) { +.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', +function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService) { $scope.formValues = { alwaysPull: true, Console: 'none', Volumes: [], - Registry: '', NetworkContainer: '', Labels: [], ExtraHosts: [], @@ -92,6 +91,8 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, $scope.config.HostConfig.Devices.splice(index, 1); }; + $scope.fromContainerMultipleNetworks = false; + function prepareImageConfig(config) { var image = config.Image; var registry = $scope.formValues.Registry; @@ -179,6 +180,7 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, var networkMode = mode; if (containerName) { networkMode += ':' + containerName; + config.Hostname = ''; } config.HostConfig.NetworkMode = networkMode; @@ -233,6 +235,213 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, return config; } + function confirmCreateContainer() { + var deferred = $q.defer(); + Container.query({ all: 1, filters: {name: ['^/' + $scope.config.name + '$'] }}).$promise + .then(function success(data) { + var existingContainer = data[0]; + if (existingContainer) { + ModalService.confirm({ + title: 'Are you sure ?', + message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?', + buttons: { + confirm: { + label: 'Replace', + className: 'btn-danger' + } + }, + callback: function onConfirm(confirmed) { + if(!confirmed) { deferred.resolve(false); } + else { + // Remove old container + ContainerService.remove(existingContainer, true) + .then(function success(data) { + Notifications.success('Container Removed', existingContainer.Id); + deferred.resolve(true); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to remove container', err: err }); + }); + } + } + }); + } else { + deferred.resolve(true); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve containers'); + return undefined; + }); + return deferred.promise; + } + + function loadFromContainerCmd(d) { + if ($scope.config.Cmd) { + $scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd); + } else { + $scope.config.Cmd = ''; + } + } + + function loadFromContainerPortBindings(d) { + var bindings = []; + for (var p in $scope.config.HostConfig.PortBindings) { + if ({}.hasOwnProperty.call($scope.config.HostConfig.PortBindings, p)) { + var hostPort = ''; + if ($scope.config.HostConfig.PortBindings[p][0].HostIp) { + hostPort = $scope.config.HostConfig.PortBindings[p][0].HostIp + ':'; + } + hostPort += $scope.config.HostConfig.PortBindings[p][0].HostPort; + var b = { + 'hostPort': hostPort, + 'containerPort': p.split('/')[0], + 'protocol': p.split('/')[1] + }; + bindings.push(b); + } + } + $scope.config.HostConfig.PortBindings = bindings; + } + + function loadFromContainerVolumes(d) { + for (var v in d.Mounts) { + if ({}.hasOwnProperty.call(d.Mounts, v)) { + var mount = d.Mounts[v]; + var volume = { + 'type': mount.Type, + 'name': mount.Name || mount.Source, + 'containerPath': mount.Destination, + 'readOnly': mount.RW === false + }; + $scope.formValues.Volumes.push(volume); + } + } + } + + function loadFromContainerNetworkConfig(d) { + $scope.config.NetworkingConfig = { + EndpointsConfig: {} + }; + var networkMode = d.HostConfig.NetworkMode; + if (networkMode === 'default') { + $scope.config.HostConfig.NetworkMode = 'bridge'; + if (!_.find($scope.availableNetworks, {'Name': 'bridge'})) { + $scope.config.HostConfig.NetworkMode = 'nat'; + } + } + if ($scope.config.HostConfig.NetworkMode.indexOf('container:') === 0) { + var netContainer = $scope.config.HostConfig.NetworkMode.split(/^container:/)[1]; + $scope.config.HostConfig.NetworkMode = 'container'; + for (var c in $scope.runningContainers) { + if ($scope.runningContainers[c].Names && $scope.runningContainers[c].Names[0] === '/' + netContainer) { + $scope.formValues.NetworkContainer = $scope.runningContainers[c]; + } + } + } + $scope.fromContainerMultipleNetworks = Object.keys(d.NetworkSettings.Networks).length >= 2; + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]) { + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig) { + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address) { + $scope.formValues.IPv4 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv4Address; + } + if (d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address) { + $scope.formValues.IPv6 = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode].IPAMConfig.IPv6Address; + } + } + } + $scope.config.NetworkingConfig.EndpointsConfig[$scope.config.HostConfig.NetworkMode] = d.NetworkSettings.Networks[$scope.config.HostConfig.NetworkMode]; + // ExtraHosts + for (var h in $scope.config.HostConfig.ExtraHosts) { + if ({}.hasOwnProperty.call($scope.config.HostConfig.ExtraHosts, h)) { + $scope.formValues.ExtraHosts.push({'value': $scope.config.HostConfig.ExtraHosts[h]}); + $scope.config.HostConfig.ExtraHosts = []; + } + } + } + + function loadFromContainerEnvrionmentVariables(d) { + var envArr = []; + for (var e in $scope.config.Env) { + if ({}.hasOwnProperty.call($scope.config.Env, e)) { + var arr = $scope.config.Env[e].split(/\=(.+)/); + envArr.push({'name': arr[0], 'value': arr[1]}); + } + } + $scope.config.Env = envArr; + } + + function loadFromContainerLabels(d) { + for (var l in $scope.config.Labels) { + if ({}.hasOwnProperty.call($scope.config.Labels, l)) { + $scope.formValues.Labels.push({ name: l, value: $scope.config.Labels[l]}); + } + } + } + + function loadFromContainerConsole(d) { + if ($scope.config.OpenStdin && $scope.config.Tty) { + $scope.formValues.Console = 'both'; + } else if (!$scope.config.OpenStdin && $scope.config.Tty) { + $scope.formValues.Console = 'tty'; + } else if ($scope.config.OpenStdin && !$scope.config.Tty) { + $scope.formValues.Console = 'interactive'; + } else if (!$scope.config.OpenStdin && !$scope.config.Tty) { + $scope.formValues.Console = 'none'; + } + } + + function loadFromContainerDevices(d) { + var path = []; + for (var dev in $scope.config.HostConfig.Devices) { + if ({}.hasOwnProperty.call($scope.config.HostConfig.Devices, dev)) { + var device = $scope.config.HostConfig.Devices[dev]; + path.push({'pathOnHost': device.PathOnHost, 'pathInContainer': device.PathInContainer}); + } + } + $scope.config.HostConfig.Devices = path; + } + + function loadFromContainerImageConfig(d) { + // If no registry found, we let default DockerHub and let full image path + var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image); + RegistryService.retrieveRegistryFromRepository($scope.config.Image) + .then(function success(data) { + if (data) { + $scope.config.Image = imageInfo.image; + $scope.formValues.Registry = data; + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrive registry'); + }); + } + + function loadFromContainerSpec() { + // Get container + Container.get({ id: $stateParams.from }).$promise + .then(function success(d) { + var fromContainer = new ContainerDetailsViewModel(d); + if (!fromContainer.ResourceControl) { + $scope.formValues.AccessControlData.AccessControlEnabled = false; + } + $scope.fromContainer = fromContainer; + $scope.config = ContainerHelper.configFromContainer(fromContainer.Model); + loadFromContainerCmd(d); + loadFromContainerPortBindings(d); + loadFromContainerVolumes(d); + loadFromContainerNetworkConfig(d); + loadFromContainerEnvrionmentVariables(d); + loadFromContainerLabels(d); + loadFromContainerConsole(d); + loadFromContainerDevices(d); + loadFromContainerImageConfig(d); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve container'); + }); + } + function initView() { Volume.query({}, function (d) { $scope.availableVolumes = d.Volumes; @@ -264,6 +473,12 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Container.query({}, function (d) { var containers = d; $scope.runningContainers = containers; + if ($stateParams.from !== '') { + loadFromContainerSpec(); + } else { + $scope.fromContainer = {}; + $scope.formValues.Registry = {}; + } }, function(e) { Notifications.error('Failure', e, 'Unable to retrieve running containers'); }); @@ -283,19 +498,27 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, } $scope.create = function () { - $('#createContainerSpinner').show(); + confirmCreateContainer() + .then(function success(confirm) { + if (!confirm) { + return false; + } + $('#createContainerSpinner').show(); + var accessControlData = $scope.formValues.AccessControlData; + var userDetails = Authentication.getUserDetails(); + var isAdmin = userDetails.role === 1 ? true : false; - var accessControlData = $scope.formValues.AccessControlData; - var userDetails = Authentication.getUserDetails(); - var isAdmin = userDetails.role === 1 ? true : false; + if (!validateForm(accessControlData, isAdmin)) { + $('#createContainerSpinner').hide(); + return; + } - if (!validateForm(accessControlData, isAdmin)) { - $('#createContainerSpinner').hide(); - return; - } - - var config = prepareConfiguration(); - createContainer(config, accessControlData); + var config = prepareConfiguration(); + createContainer(config, accessControlData); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create container'); + }); }; function createContainer(config, accessControlData) { diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index f633c0ed8..fdae6b4de 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -23,7 +23,7 @@
    - +
    @@ -98,7 +98,7 @@
    - +
    @@ -110,6 +110,10 @@ Cancel {{ state.formValidationError }} + + + This container is connected to multiple networks, only one network will be kept at creation time. +
    diff --git a/app/directives/imageRegistry/porImageRegistryController.js b/app/directives/imageRegistry/porImageRegistryController.js index 3eeb3d0bb..496209be7 100644 --- a/app/directives/imageRegistry/porImageRegistryController.js +++ b/app/directives/imageRegistry/porImageRegistryController.js @@ -12,7 +12,11 @@ function ($q, RegistryService, DockerHubService, Notifications) { var dockerhub = data.dockerhub; var registries = data.registries; ctrl.availableRegistries = [dockerhub].concat(registries); - ctrl.registry = dockerhub; + if (!ctrl.registry.Id) { + ctrl.registry = dockerhub; + } else { + ctrl.registry = _.find(ctrl.availableRegistries, { 'Id': ctrl.registry.Id }); + } }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve registries'); diff --git a/app/helpers/containerHelper.js b/app/helpers/containerHelper.js index 6f74332c7..544bc1ac8 100644 --- a/app/helpers/containerHelper.js +++ b/app/helpers/containerHelper.js @@ -7,5 +7,56 @@ angular.module('portainer.helpers') return splitargs(command); }; + helper.commandArrayToString = function(array) { + return array.map(function(elem) { + return '\'' + elem + '\''; + }).join(' '); + }; + + helper.configFromContainer = function(container) { + var config = container.Config; + // HostConfig + config.HostConfig = container.HostConfig; + // Name + config.name = container.Name.replace(/^\//g, ''); + // Network + var mode = config.HostConfig.NetworkMode; + config.NetworkingConfig = { + 'EndpointsConfig': {} + }; + config.NetworkingConfig.EndpointsConfig = container.NetworkSettings.Networks; + if (mode.indexOf('container:') !== -1) { + delete config.Hostname; + delete config.ExposedPorts; + } + // Set volumes + var binds = []; + var volumes = {}; + for (var v in container.Mounts) { + if ({}.hasOwnProperty.call(container.Mounts, v)) { + var mount = container.Mounts[v]; + var volume = { + 'type': mount.Type, + 'name': mount.Name || mount.Source, + 'containerPath': mount.Destination, + 'readOnly': mount.RW === false + }; + var name = mount.Name || mount.Source; + var containerPath = mount.Destination; + if (name && containerPath) { + var bind = name + ':' + containerPath; + volumes[containerPath] = {}; + if (mount.RW === false) { + bind += ':ro'; + } + binds.push(bind); + } + } + } + config.HostConfig.Binds = binds; + config.Volumes = volumes; + return config; + }; + return helper; }]); diff --git a/app/models/docker/containerDetails.js b/app/models/docker/containerDetails.js index 65ce86069..63945ff41 100644 --- a/app/models/docker/containerDetails.js +++ b/app/models/docker/containerDetails.js @@ -1,4 +1,5 @@ function ContainerDetailsViewModel(data) { + this.Model = data; this.Id = data.Id; this.State = data.State; this.Created = data.Created; diff --git a/app/services/modalService.js b/app/services/modalService.js index ad55be8ed..aa6481e40 100644 --- a/app/services/modalService.js +++ b/app/services/modalService.js @@ -108,5 +108,19 @@ angular.module('portainer.services') }); }; + service.confirmExperimentalFeature = function(callback) { + service.confirm({ + title: 'Experimental feature', + message: 'This feature is currently experimental, please use with caution.', + buttons: { + confirm: { + label: 'Continue', + className: 'btn-danger' + } + }, + callback: callback + }); + }; + return service; }]); From e96e61576131dc0ac1e6b4319075b21e2da79495 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 13 Aug 2017 12:55:52 +0200 Subject: [PATCH 25/30] feat(container-details): add the ability to specify if image should be pulled when re-creating a container --- .../container/containerController.js | 124 ++++++++++-------- app/services/modalService.js | 34 +++++ 2 files changed, 104 insertions(+), 54 deletions(-) diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index 1af934c78..2523c37d2 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -1,6 +1,6 @@ angular.module('container', []) -.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', -function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService, ResourceControlService, RegistryService, ImageService) { +.controller('ContainerController', ['$q', '$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', +function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService, ResourceControlService, RegistryService, ImageService) { $scope.activityTime = 0; $scope.portBindings = []; $scope.config = { @@ -203,62 +203,78 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con }); }; + $scope.confirmRemove = function () { + var title = 'You are about to remove a container.'; + if ($scope.container.State.Running) { + title = 'You are about to remove a running container.'; + } + ModalService.confirmContainerDeletion( + title, + function (result) { + if(!result) { return; } + var cleanAssociatedVolumes = false; + if (result[0]) { + cleanAssociatedVolumes = true; + } + $scope.remove(cleanAssociatedVolumes); + } + ); + }; + + function recreateContainer(pullImage) { + $('#loadingViewSpinner').show(); + var container = $scope.container; + var config = ContainerHelper.configFromContainer(container.Model); + ContainerService.remove(container, true) + .then(function success() { + return RegistryService.retrieveRegistryFromRepository(container.Config.Image); + }) + .then(function success(data) { + return $q.when(!pullImage || ImageService.pullImage(container.Config.Image, data, true)); + }) + .then(function success() { + return ContainerService.createAndStartContainer(config); + }) + .then(function success(data) { + if (!container.ResourceControl) { + return true; + } else { + var containerIdentifier = data.Id; + var resourceControl = container.ResourceControl; + var users = resourceControl.UserAccesses.map(function(u) { + return u.UserId; + }); + var teams = resourceControl.TeamAccesses.map(function(t) { + return t.TeamId; + }); + return ResourceControlService.createResourceControl(resourceControl.AdministratorsOnly, + users, teams, containerIdentifier, 'container', []); + } + }) + .then(function success(data) { + Notifications.success('Container successfully re-created'); + $state.go('containers', {}, {reload: true}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to re-create container'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + $scope.recreate = function() { ModalService.confirmExperimentalFeature(function (experimental) { if(!experimental) { return; } - ModalService.confirm({ - title: 'Are you sure ?', - message: 'You\'re about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.', - buttons: { - confirm: { - label: 'Recreate', - className: 'btn-danger' - } - }, - callback: function onConfirm(confirmed) { - if(!confirmed) { return; } - else { - $('#loadingViewSpinner').show(); - var container = $scope.container; - var config = ContainerHelper.configFromContainer(container.Model); - ContainerService.remove(container, true) - .then(function success() { - return RegistryService.retrieveRegistryFromRepository(container.Config.Image); - }) - .then(function success(data) { - return ImageService.pullImage(container.Config.Image, data, true); - }) - .then(function success() { - return ContainerService.createAndStartContainer(config); - }) - .then(function success(data) { - if (!container.ResourceControl) { - return true; - } else { - var containerIdentifier = data.Id; - var resourceControl = container.ResourceControl; - var users = resourceControl.UserAccesses.map(function(u) { - return u.UserId; - }); - var teams = resourceControl.TeamAccesses.map(function(t) { - return t.TeamId; - }); - return ResourceControlService.createResourceControl(resourceControl.AdministratorsOnly, - users, teams, containerIdentifier, 'container', []); - } - }) - .then(function success(data) { - Notifications.success('Container successfully re-created'); - $state.go('containers', {}, {reload: true}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to re-create container'); - }) - .finally(function final() { - $('#loadingViewSpinner').hide(); - }); - } + + ModalService.confirmContainerRecreation(function (result) { + if(!result) { return; } + console.log(JSON.stringify(result, null, 4)); + var pullImage = false; + if (result[0]) { + pullImage = true; } + recreateContainer(pullImage); }); }); }; diff --git a/app/services/modalService.js b/app/services/modalService.js index aa6481e40..81b0b84a9 100644 --- a/app/services/modalService.js +++ b/app/services/modalService.js @@ -46,6 +46,19 @@ angular.module('portainer.services') applyBoxCSS(box); }; + service.customPrompt = function(options) { + var box = bootbox.prompt({ + title: options.title, + inputType: options.inputType, + inputOptions: options.inputOptions, + buttons: confirmButtons(options), + callback: options.callback + }); + applyBoxCSS(box); + box.find('.bootbox-body').prepend('

    ' + options.message + '

    '); + box.find('.bootbox-input-checkbox').prop('checked', true); + }; + service.confirmAccessControlUpdate = function(callback, msg) { service.confirm({ title: 'Are you sure ?', @@ -108,6 +121,27 @@ angular.module('portainer.services') }); }; + service.confirmContainerRecreation = function(callback) { + service.customPrompt({ + title: 'Are you sure?', + message: 'You\'re about to re-create this container, any non-persisted data will be lost. This container will be removed and another one will be created using the same configuration.', + inputType: 'checkbox', + inputOptions: [ + { + text: 'Pull latest image', + value: '1' + } + ], + buttons: { + confirm: { + label: 'Recreate', + className: 'btn-danger' + } + }, + callback: callback + }); + }; + service.confirmExperimentalFeature = function(callback) { service.confirm({ title: 'Experimental feature', From e5666dfdf2750df05cd20c7fad0a1ee7b45c49ed Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 13 Aug 2017 13:31:50 +0200 Subject: [PATCH 26/30] feat(vic): fix multiple issues when managing a VIC engine (#1069) --- .../container/containerController.js | 1 - .../createVolume/createVolumeController.js | 2 +- app/components/image/imageController.js | 29 +++++++------------ app/components/images/images.html | 4 +-- app/components/images/imagesController.js | 2 +- app/components/network/network.html | 2 +- app/components/network/networkController.js | 17 ++++++++--- app/components/networks/networks.html | 2 +- app/components/sidebar/sidebar.html | 4 +-- app/components/stats/stats.html | 2 +- app/components/stats/statsController.js | 5 +++- app/helpers/infoHelper.js | 6 +++- app/services/docker/volumeService.js | 2 +- 13 files changed, 43 insertions(+), 35 deletions(-) diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index 2523c37d2..1908aa8d4 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -269,7 +269,6 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerCommit, ModalService.confirmContainerRecreation(function (result) { if(!result) { return; } - console.log(JSON.stringify(result, null, 4)); var pullImage = false; if (result[0]) { pullImage = true; diff --git a/app/components/createVolume/createVolumeController.js b/app/components/createVolume/createVolumeController.js index 8319337ba..107042181 100644 --- a/app/components/createVolume/createVolumeController.js +++ b/app/components/createVolume/createVolumeController.js @@ -73,7 +73,7 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi var endpointProvider = $scope.applicationState.endpoint.mode.provider; var apiVersion = $scope.applicationState.endpoint.apiVersion; if (endpointProvider !== 'DOCKER_SWARM') { - PluginService.volumePlugins(apiVersion < 1.25) + PluginService.volumePlugins(apiVersion < 1.25 || endpointProvider === 'VMWARE_VIC') .then(function success(data) { $scope.availableVolumeDrivers = data; }) diff --git a/app/components/image/imageController.js b/app/components/image/imageController.js index f5c0c6142..d76dfb356 100644 --- a/app/components/image/imageController.js +++ b/app/components/image/imageController.js @@ -1,6 +1,6 @@ angular.module('image', []) -.controller('ImageController', ['$scope', '$stateParams', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', -function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService, Notifications) { +.controller('ImageController', ['$q', '$scope', '$stateParams', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', +function ($q, $scope, $stateParams, $state, $timeout, ImageService, RegistryService, Notifications) { $scope.formValues = { Image: '', Registry: '' @@ -109,11 +109,16 @@ function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService, }); }; - function retrieveImageDetails() { + function initView() { $('#loadingViewSpinner').show(); - ImageService.image($stateParams.id) + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + $q.all({ + image: ImageService.image($stateParams.id), + history: endpointProvider !== 'VMWARE_VIC' ? ImageService.history($stateParams.id) : [] + }) .then(function success(data) { - $scope.image = data; + $scope.image = data.image; + $scope.history = data.history; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve image details'); @@ -122,19 +127,7 @@ function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService, .finally(function final() { $('#loadingViewSpinner').hide(); }); - - $('#loadingViewSpinner').show(); - ImageService.history($stateParams.id) - .then(function success(data) { - $scope.history = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve image history'); - }) - .finally(function final() { - $('#loadingViewSpinner').hide(); - }); } - retrieveImageDetails(); + initView(); }]); diff --git a/app/components/images/images.html b/app/components/images/images.html index d317e9818..5959c0bf2 100644 --- a/app/components/images/images.html +++ b/app/components/images/images.html @@ -70,7 +70,7 @@
    - + @@ -126,7 +126,7 @@ {{ image.Id|truncate:20}} + ng-if="::image.Containers === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider !== 'VMWARE_VIC'"> Unused diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index d5f887049..e8c9ca30f 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -95,7 +95,7 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService) $('#loadImagesSpinner').show(); var endpointProvider = $scope.applicationState.endpoint.mode.provider; var apiVersion = $scope.applicationState.endpoint.apiVersion; - ImageService.images(apiVersion >= 1.25 && endpointProvider !== 'DOCKER_SWARM') + ImageService.images(apiVersion >= 1.25 && endpointProvider !== 'DOCKER_SWARM' && endpointProvider !== 'VMWARE_VIC') .then(function success(data) { $scope.images = data; }) diff --git a/app/components/network/network.html b/app/components/network/network.html index b2c1c9b6e..d5eacd7f1 100644 --- a/app/components/network/network.html +++ b/app/components/network/network.html @@ -67,7 +67,7 @@
    -
    +
    diff --git a/app/components/network/networkController.js b/app/components/network/networkController.js index 291b6ca9e..63f9cb4e8 100644 --- a/app/components/network/networkController.js +++ b/app/components/network/networkController.js @@ -51,8 +51,9 @@ function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHe } function getContainersInNetwork(network) { + var apiVersion = $scope.applicationState.endpoint.apiVersion; if (network.Containers) { - if ($scope.applicationState.endpoint.apiVersion < 1.24) { + if (apiVersion < 1.24) { Container.query({}, function success(data) { var containersInNetwork = data.filter(function filter(container) { if (container.HostConfig.NetworkMode === network.Name) { @@ -81,12 +82,20 @@ function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHe function initView() { $('#loadingViewSpinner').show(); - Network.get({id: $stateParams.id}, function success(data) { + Network.get({id: $stateParams.id}).$promise + .then(function success(data) { $scope.network = data; - getContainersInNetwork(data); - }, function error(err) { + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + if (endpointProvider !== 'VMWARE_VIC') { + getContainersInNetwork(data); + } + }) + .catch(function error(err) { $('#loadingViewSpinner').hide(); Notifications.error('Failure', err, 'Unable to retrieve network info'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); }); } diff --git a/app/components/networks/networks.html b/app/components/networks/networks.html index 5d8c110be..7e8442cff 100644 --- a/app/components/networks/networks.html +++ b/app/components/networks/networks.html @@ -29,7 +29,7 @@ Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.
    -
    +
    Note: The network will be created using the bridge driver.
    diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index 2f4c79424..0e44812a6 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -43,13 +43,13 @@ - -
    -
    +
    diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js index d5a76616e..1b8de729d 100644 --- a/app/components/stats/statsController.js +++ b/app/components/stats/statsController.js @@ -213,5 +213,8 @@ function (Pagination, $scope, Notifications, $timeout, Container, ContainerTop, }, function (e) { Notifications.error('Failure', e, 'Unable to retrieve container info'); }); - $scope.getTop(); + var endpointProvider = $scope.applicationState.endpoint.mode.provider; + if (endpointProvider !== 'VMWARE_VIC') { + $scope.getTop(); + } }]); diff --git a/app/helpers/infoHelper.js b/app/helpers/infoHelper.js index fd3aa511a..c78e86bbc 100644 --- a/app/helpers/infoHelper.js +++ b/app/helpers/infoHelper.js @@ -16,7 +16,11 @@ angular.module('portainer.helpers') } } else { if (!info.Swarm || _.isEmpty(info.Swarm.NodeID)) { - mode.provider = 'DOCKER_STANDALONE'; + if (info.ID === 'vSphere Integrated Containers') { + mode.provider = 'VMWARE_VIC'; + } else { + mode.provider = 'DOCKER_STANDALONE'; + } } else { mode.provider = 'DOCKER_SWARM_MODE'; if (info.Swarm.ControlAvailable) { diff --git a/app/services/docker/volumeService.js b/app/services/docker/volumeService.js index 2a67bef4e..8ae9425c6 100644 --- a/app/services/docker/volumeService.js +++ b/app/services/docker/volumeService.js @@ -94,7 +94,7 @@ angular.module('portainer.services') service.createXAutoGeneratedLocalVolumes = function (x) { var createVolumeQueries = []; for (var i = 0; i < x; i++) { - createVolumeQueries.push(service.createVolume({})); + createVolumeQueries.push(service.createVolume({ Driver: 'local' })); } return $q.all(createVolumeQueries); }; From d3e87b2435a0f6384aa2aa3e983ff9238a37146d Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 13 Aug 2017 15:04:24 +0200 Subject: [PATCH 27/30] style(settings): fix typo --- .../settingsAuthentication/settingsAuthentication.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/settingsAuthentication/settingsAuthentication.html b/app/components/settingsAuthentication/settingsAuthentication.html index 6c94043f8..92fd050d7 100644 --- a/app/components/settingsAuthentication/settingsAuthentication.html +++ b/app/components/settingsAuthentication/settingsAuthentication.html @@ -51,7 +51,7 @@
    - When using LDAP authentication, Portainer will delegate user authentication to a LDAP server (exception for the admin user that always use internal authentication). + When using LDAP authentication, Portainer will delegate user authentication to a LDAP server (exception for the admin user that always uses internal authentication).

    Users still need to be created in Portainer beforehand. From 92391254bcd8ec71ae95b8171d790b0ed1df20c2 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 13 Aug 2017 16:45:55 +0200 Subject: [PATCH 28/30] feat(api): introduces swagger.yml (#1112) --- api/http/error/error.go | 8 - api/http/handler/auth.go | 25 +- api/http/handler/dockerhub.go | 18 +- api/http/handler/endpoint.go | 48 +- api/http/handler/handler.go | 42 +- api/http/handler/registry.go | 52 +- api/http/handler/resource_control.go | 34 +- api/http/handler/settings.go | 40 +- api/http/handler/team.go | 38 +- api/http/handler/team_membership.go | 34 +- api/http/handler/templates.go | 7 +- api/http/handler/upload.go | 7 +- api/http/handler/user.go | 117 +- api/http/security/bouncer.go | 2 +- api/swagger.yaml | 2471 +++++++++++++++++ .../porAccessControlFormController.js | 6 +- .../porAccessControlPanelController.js | 6 +- app/rest/api/user.js | 1 - app/services/api/userService.js | 21 +- 19 files changed, 2691 insertions(+), 286 deletions(-) create mode 100644 api/swagger.yaml diff --git a/api/http/error/error.go b/api/http/error/error.go index 03f5220a8..f94b924ed 100644 --- a/api/http/error/error.go +++ b/api/http/error/error.go @@ -4,7 +4,6 @@ import ( "encoding/json" "log" "net/http" - "strings" ) // errorResponse is a generic response for sending a error. @@ -21,10 +20,3 @@ func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log. w.WriteHeader(code) json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()}) } - -// WriteMethodNotAllowedResponse writes an error message to the response and sets the Allow header. -func WriteMethodNotAllowedResponse(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)}) -} diff --git a/api/http/handler/auth.go b/api/http/handler/auth.go index d6af6597b..eb5e86c00 100644 --- a/api/http/handler/auth.go +++ b/api/http/handler/auth.go @@ -44,17 +44,23 @@ func NewAuthHandler(bouncer *security.RequestBouncer, authDisabled bool) *AuthHa authDisabled: authDisabled, } h.Handle("/auth", - bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth))) + bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth))).Methods(http.MethodPost) return h } -func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost}) - return +type ( + postAuthRequest struct { + Username string `valid:"required"` + Password string `valid:"required"` } + postAuthResponse struct { + JWT string `json:"jwt"` + } +) + +func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) { if handler.authDisabled { httperror.WriteErrorResponse(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger) return @@ -118,12 +124,3 @@ func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Reques encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger) } - -type postAuthRequest struct { - Username string `valid:"required"` - Password string `valid:"required"` -} - -type postAuthResponse struct { - JWT string `json:"jwt"` -} diff --git a/api/http/handler/dockerhub.go b/api/http/handler/dockerhub.go index 56b8eed4e..9da51c90e 100644 --- a/api/http/handler/dockerhub.go +++ b/api/http/handler/dockerhub.go @@ -22,20 +22,28 @@ type DockerHubHandler struct { DockerHubService portainer.DockerHubService } -// NewDockerHubHandler returns a new instance of OldDockerHubHandler. +// NewDockerHubHandler returns a new instance of NewDockerHubHandler. func NewDockerHubHandler(bouncer *security.RequestBouncer) *DockerHubHandler { h := &DockerHubHandler{ Router: mux.NewRouter(), Logger: log.New(os.Stderr, "", log.LstdFlags), } h.Handle("/dockerhub", - bouncer.PublicAccess(http.HandlerFunc(h.handleGetDockerHub))).Methods(http.MethodGet) + bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetDockerHub))).Methods(http.MethodGet) h.Handle("/dockerhub", bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutDockerHub))).Methods(http.MethodPut) return h } +type ( + putDockerHubRequest struct { + Authentication bool `valid:""` + Username string `valid:""` + Password string `valid:""` + } +) + // handleGetDockerHub handles GET requests on /dockerhub func (handler *DockerHubHandler) handleGetDockerHub(w http.ResponseWriter, r *http.Request) { dockerhub, err := handler.DockerHubService.DockerHub() @@ -79,9 +87,3 @@ func (handler *DockerHubHandler) handlePutDockerHub(w http.ResponseWriter, r *ht httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) } } - -type putDockerHubRequest struct { - Authentication bool `valid:""` - Username string `valid:""` - Password string `valid:""` -} diff --git a/api/http/handler/endpoint.go b/api/http/handler/endpoint.go index fd8d85598..07950d790 100644 --- a/api/http/handler/endpoint.go +++ b/api/http/handler/endpoint.go @@ -55,6 +55,31 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag return h } +type ( + postEndpointsRequest struct { + Name string `valid:"required"` + URL string `valid:"required"` + PublicURL string `valid:"-"` + TLS bool + } + + postEndpointsResponse struct { + ID int `json:"Id"` + } + + putEndpointAccessRequest struct { + AuthorizedUsers []int `valid:"-"` + AuthorizedTeams []int `valid:"-"` + } + + putEndpointsRequest struct { + Name string `valid:"-"` + URL string `valid:"-"` + PublicURL string `valid:"-"` + TLS bool `valid:"-"` + } +) + // handleGetEndpoints handles GET requests on /endpoints func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) { securityContext, err := security.RetrieveRestrictedRequestContext(r) @@ -130,17 +155,6 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger) } -type postEndpointsRequest struct { - Name string `valid:"required"` - URL string `valid:"required"` - PublicURL string `valid:"-"` - 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) @@ -219,11 +233,6 @@ func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r } } -type putEndpointAccessRequest struct { - AuthorizedUsers []int `valid:"-"` - AuthorizedTeams []int `valid:"-"` -} - // handlePutEndpoint handles PUT requests on /endpoints/:id func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) { if !handler.authorizeEndpointManagement { @@ -307,13 +316,6 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http } } -type putEndpointsRequest struct { - Name string `valid:"-"` - URL string `valid:"-"` - PublicURL 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 { diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 9c3eb45ea..4a83f6743 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -36,48 +36,48 @@ const ( 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") { + + switch { + case 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/teams") { - http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/team_memberships") { - http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/endpoints") { + case strings.HasPrefix(r.URL.Path, "/api/dockerhub"): + http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/endpoints"): if strings.Contains(r.URL.Path, "/docker") { http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r) } else { http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) } - } else if strings.HasPrefix(r.URL.Path, "/api/registries") { + case strings.HasPrefix(r.URL.Path, "/api/registries"): http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/dockerhub") { - http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/resource_controls") { + case strings.HasPrefix(r.URL.Path, "/api/resource_controls"): http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/settings") { + case strings.HasPrefix(r.URL.Path, "/api/settings"): http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/status") { + case strings.HasPrefix(r.URL.Path, "/api/status"): http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/templates") { + case strings.HasPrefix(r.URL.Path, "/api/templates"): http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/upload") { + case strings.HasPrefix(r.URL.Path, "/api/upload"): http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/api/websocket") { + case strings.HasPrefix(r.URL.Path, "/api/users"): + http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/teams"): + http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/team_memberships"): + http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/websocket"): http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r) - } else if strings.HasPrefix(r.URL.Path, "/") { + case strings.HasPrefix(r.URL.Path, "/"): h.FileHandler.ServeHTTP(w, r) } } -// encodeJSON encodes v to w in JSON format. Error() is called if encoding fails. +// encodeJSON encodes v to w in JSON format. WriteErrorResponse() is called if encoding fails. func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) { if err := json.NewEncoder(w).Encode(v); err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, logger) diff --git a/api/http/handler/registry.go b/api/http/handler/registry.go index 164a5f3c1..9afeb1178 100644 --- a/api/http/handler/registry.go +++ b/api/http/handler/registry.go @@ -44,6 +44,33 @@ func NewRegistryHandler(bouncer *security.RequestBouncer) *RegistryHandler { return h } +type ( + postRegistriesRequest struct { + Name string `valid:"required"` + URL string `valid:"required"` + Authentication bool `valid:""` + Username string `valid:""` + Password string `valid:""` + } + + postRegistriesResponse struct { + ID int `json:"Id"` + } + + putRegistryAccessRequest struct { + AuthorizedUsers []int `valid:"-"` + AuthorizedTeams []int `valid:"-"` + } + + putRegistriesRequest struct { + Name string `valid:"required"` + URL string `valid:"required"` + Authentication bool `valid:""` + Username string `valid:""` + Password string `valid:""` + } +) + // handleGetRegistries handles GET requests on /registries func (handler *RegistryHandler) handleGetRegistries(w http.ResponseWriter, r *http.Request) { securityContext, err := security.RetrieveRestrictedRequestContext(r) @@ -112,18 +139,6 @@ func (handler *RegistryHandler) handlePostRegistries(w http.ResponseWriter, r *h encodeJSON(w, &postRegistriesResponse{ID: int(registry.ID)}, handler.Logger) } -type postRegistriesRequest struct { - Name string `valid:"required"` - URL string `valid:"required"` - Authentication bool `valid:""` - Username string `valid:""` - Password string `valid:""` -} - -type postRegistriesResponse struct { - ID int `json:"Id"` -} - // handleGetRegistry handles GET requests on /registries/:id func (handler *RegistryHandler) handleGetRegistry(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -202,11 +217,6 @@ func (handler *RegistryHandler) handlePutRegistryAccess(w http.ResponseWriter, r } } -type putRegistryAccessRequest struct { - AuthorizedUsers []int `valid:"-"` - AuthorizedTeams []int `valid:"-"` -} - // handlePutRegistry handles PUT requests on /registries/:id func (handler *RegistryHandler) handlePutRegistry(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -276,14 +286,6 @@ func (handler *RegistryHandler) handlePutRegistry(w http.ResponseWriter, r *http } } -type putRegistriesRequest struct { - Name string `valid:"required"` - URL string `valid:"required"` - Authentication bool `valid:""` - Username string `valid:""` - Password string `valid:""` -} - // handleDeleteRegistry handles DELETE requests on /registries/:id func (handler *RegistryHandler) handleDeleteRegistry(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) diff --git a/api/http/handler/resource_control.go b/api/http/handler/resource_control.go index 7952cfbda..7c35dec39 100644 --- a/api/http/handler/resource_control.go +++ b/api/http/handler/resource_control.go @@ -39,6 +39,23 @@ func NewResourceHandler(bouncer *security.RequestBouncer) *ResourceHandler { return h } +type ( + postResourcesRequest struct { + ResourceID string `valid:"required"` + Type string `valid:"required"` + AdministratorsOnly bool `valid:"-"` + Users []int `valid:"-"` + Teams []int `valid:"-"` + SubResourceIDs []string `valid:"-"` + } + + putResourcesRequest struct { + AdministratorsOnly bool `valid:"-"` + Users []int `valid:"-"` + Teams []int `valid:"-"` + } +) + // handlePostResources handles POST requests on /resources func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *http.Request) { var req postResourcesRequest @@ -121,22 +138,13 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht err = handler.ResourceControlService.CreateResourceControl(&resourceControl) if err != nil { - httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } return } -type postResourcesRequest struct { - ResourceID string `valid:"required"` - Type string `valid:"required"` - AdministratorsOnly bool `valid:"-"` - Users []int `valid:"-"` - Teams []int `valid:"-"` - SubResourceIDs []string `valid:"-"` -} - // handlePutResources handles PUT requests on /resources/:id func (handler *ResourceHandler) handlePutResources(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) @@ -210,12 +218,6 @@ func (handler *ResourceHandler) handlePutResources(w http.ResponseWriter, r *htt } } -type putResourcesRequest struct { - AdministratorsOnly bool `valid:"-"` - Users []int `valid:"-"` - Teams []int `valid:"-"` -} - // handleDeleteResources handles DELETE requests on /resources/:id func (handler *ResourceHandler) handleDeleteResources(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) diff --git a/api/http/handler/settings.go b/api/http/handler/settings.go index 12187625f..52e957f6d 100644 --- a/api/http/handler/settings.go +++ b/api/http/handler/settings.go @@ -43,6 +43,27 @@ func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler { return h } +type ( + publicSettingsResponse struct { + LogoURL string `json:"LogoURL"` + DisplayExternalContributors bool `json:"DisplayExternalContributors"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + } + + putSettingsRequest struct { + TemplatesURL string `valid:"required"` + LogoURL string `valid:""` + BlackListedLabels []portainer.Pair `valid:""` + DisplayExternalContributors bool `valid:""` + AuthenticationMethod int `valid:"required"` + LDAPSettings portainer.LDAPSettings `valid:""` + } + + putSettingsLDAPCheckRequest struct { + LDAPSettings portainer.LDAPSettings `valid:""` + } +) + // handleGetSettings handles GET requests on /settings func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) { settings, err := handler.SettingsService.Settings() @@ -73,12 +94,6 @@ func (handler *SettingsHandler) handleGetPublicSettings(w http.ResponseWriter, r return } -type publicSettingsResponse struct { - LogoURL string `json:"LogoURL"` - DisplayExternalContributors bool `json:"DisplayExternalContributors"` - AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` -} - // handlePutSettings handles PUT requests on /settings func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) { var req putSettingsRequest @@ -127,15 +142,6 @@ func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http } } -type putSettingsRequest struct { - TemplatesURL string `valid:"required"` - LogoURL string `valid:""` - BlackListedLabels []portainer.Pair `valid:""` - DisplayExternalContributors bool `valid:""` - AuthenticationMethod int `valid:"required"` - LDAPSettings portainer.LDAPSettings `valid:""` -} - // handlePutSettingsLDAPCheck handles PUT requests on /settings/ldap/check func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter, r *http.Request) { var req putSettingsLDAPCheckRequest @@ -161,7 +167,3 @@ func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter return } } - -type putSettingsLDAPCheckRequest struct { - LDAPSettings portainer.LDAPSettings `valid:""` -} diff --git a/api/http/handler/team.go b/api/http/handler/team.go index 3f4d9fc50..1bf90e689 100644 --- a/api/http/handler/team.go +++ b/api/http/handler/team.go @@ -34,7 +34,7 @@ func NewTeamHandler(bouncer *security.RequestBouncer) *TeamHandler { h.Handle("/teams", bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostTeams))).Methods(http.MethodPost) h.Handle("/teams", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet) + bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet) h.Handle("/teams/{id}", bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeam))).Methods(http.MethodGet) h.Handle("/teams/{id}", @@ -47,6 +47,20 @@ func NewTeamHandler(bouncer *security.RequestBouncer) *TeamHandler { return h } +type ( + postTeamsRequest struct { + Name string `valid:"required"` + } + + postTeamsResponse struct { + ID int `json:"Id"` + } + + putTeamRequest struct { + Name string `valid:"-"` + } +) + // handlePostTeams handles POST requests on /teams func (handler *TeamHandler) handlePostTeams(w http.ResponseWriter, r *http.Request) { var req postTeamsRequest @@ -84,23 +98,23 @@ func (handler *TeamHandler) handlePostTeams(w http.ResponseWriter, r *http.Reque encodeJSON(w, &postTeamsResponse{ID: int(team.ID)}, handler.Logger) } -type postTeamsResponse struct { - ID int `json:"Id"` -} - -type postTeamsRequest struct { - Name string `valid:"required"` -} - // handleGetTeams handles GET requests on /teams func (handler *TeamHandler) handleGetTeams(w http.ResponseWriter, r *http.Request) { + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + teams, err := handler.TeamService.Teams() if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } - encodeJSON(w, teams, handler.Logger) + filteredTeams := security.FilterUserTeams(teams, securityContext) + + encodeJSON(w, filteredTeams, handler.Logger) } // handleGetTeam handles GET requests on /teams/:id @@ -181,10 +195,6 @@ func (handler *TeamHandler) handlePutTeam(w http.ResponseWriter, r *http.Request } } -type putTeamRequest struct { - Name string `valid:"-"` -} - // handleDeleteTeam handles DELETE requests on /teams/:id func (handler *TeamHandler) handleDeleteTeam(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) diff --git a/api/http/handler/team_membership.go b/api/http/handler/team_membership.go index e6c9075ef..c96f5c8ca 100644 --- a/api/http/handler/team_membership.go +++ b/api/http/handler/team_membership.go @@ -42,6 +42,24 @@ func NewTeamMembershipHandler(bouncer *security.RequestBouncer) *TeamMembershipH return h } +type ( + postTeamMembershipsRequest struct { + UserID int `valid:"required"` + TeamID int `valid:"required"` + Role int `valid:"required"` + } + + postTeamMembershipsResponse struct { + ID int `json:"Id"` + } + + putTeamMembershipRequest struct { + UserID int `valid:"required"` + TeamID int `valid:"required"` + Role int `valid:"required"` + } +) + // handlePostTeamMemberships handles POST requests on /team_memberships func (handler *TeamMembershipHandler) handlePostTeamMemberships(w http.ResponseWriter, r *http.Request) { securityContext, err := security.RetrieveRestrictedRequestContext(r) @@ -100,16 +118,6 @@ func (handler *TeamMembershipHandler) handlePostTeamMemberships(w http.ResponseW encodeJSON(w, &postTeamMembershipsResponse{ID: int(membership.ID)}, handler.Logger) } -type postTeamMembershipsResponse struct { - ID int `json:"Id"` -} - -type postTeamMembershipsRequest struct { - UserID int `valid:"required"` - TeamID int `valid:"required"` - Role int `valid:"required"` -} - // handleGetTeamsMemberships handles GET requests on /team_memberships func (handler *TeamMembershipHandler) handleGetTeamsMemberships(w http.ResponseWriter, r *http.Request) { securityContext, err := security.RetrieveRestrictedRequestContext(r) @@ -195,12 +203,6 @@ func (handler *TeamMembershipHandler) handlePutTeamMembership(w http.ResponseWri } } -type putTeamMembershipRequest struct { - UserID int `valid:"required"` - TeamID int `valid:"required"` - Role int `valid:"required"` -} - // handleDeleteTeamMembership handles DELETE requests on /team_memberships/:id func (handler *TeamMembershipHandler) handleDeleteTeamMembership(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) diff --git a/api/http/handler/templates.go b/api/http/handler/templates.go index 6f3ba019f..25e2e288b 100644 --- a/api/http/handler/templates.go +++ b/api/http/handler/templates.go @@ -30,17 +30,12 @@ func NewTemplatesHandler(bouncer *security.RequestBouncer) *TemplatesHandler { Logger: log.New(os.Stderr, "", log.LstdFlags), } h.Handle("/templates", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates))) + bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates))).Methods(http.MethodGet) return h } // handleGetTemplates handles GET requests on /templates?key= func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet}) - return - } - key := r.FormValue("key") if key == "" { httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger) diff --git a/api/http/handler/upload.go b/api/http/handler/upload.go index c3d417208..7395fe888 100644 --- a/api/http/handler/upload.go +++ b/api/http/handler/upload.go @@ -26,17 +26,12 @@ func NewUploadHandler(bouncer *security.RequestBouncer) *UploadHandler { Logger: log.New(os.Stderr, "", log.LstdFlags), } h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUploadTLS))) + bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostUploadTLS))).Methods(http.MethodPost) return h } // handlePostUploadTLS handles POST requests on /upload/tls/{certificate:(?:ca|cert|key)}?folder=folder func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost}) - return - } - vars := mux.Vars(r) certificate := vars["certificate"] diff --git a/api/http/handler/user.go b/api/http/handler/user.go index 2f4079459..7aa4e11c9 100644 --- a/api/http/handler/user.go +++ b/api/http/handler/user.go @@ -47,18 +47,45 @@ func NewUserHandler(bouncer *security.RequestBouncer) *UserHandler { bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete) h.Handle("/users/{id}/memberships", bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetMemberships))).Methods(http.MethodGet) - h.Handle("/users/{id}/teams", - bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet) h.Handle("/users/{id}/passwd", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUserPasswd))) + bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUserPasswd))).Methods(http.MethodPost) h.Handle("/users/admin/check", - bouncer.PublicAccess(http.HandlerFunc(h.handleGetAdminCheck))) + bouncer.PublicAccess(http.HandlerFunc(h.handleGetAdminCheck))).Methods(http.MethodGet) h.Handle("/users/admin/init", - bouncer.PublicAccess(http.HandlerFunc(h.handlePostAdminInit))) + bouncer.PublicAccess(http.HandlerFunc(h.handlePostAdminInit))).Methods(http.MethodPost) return h } +type ( + postUsersRequest struct { + Username string `valid:"required"` + Password string `valid:""` + Role int `valid:"required"` + } + + postUsersResponse struct { + ID int `json:"Id"` + } + + postUserPasswdRequest struct { + Password string `valid:"required"` + } + + postUserPasswdResponse struct { + Valid bool `json:"valid"` + } + + putUserRequest struct { + Password string `valid:"-"` + Role int `valid:"-"` + } + + postAdminInitRequest struct { + Password string `valid:"required"` + } +) + // handlePostUsers handles POST requests on /users func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) { var req postUsersRequest @@ -139,16 +166,6 @@ func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Reque encodeJSON(w, &postUsersResponse{ID: int(user.ID)}, handler.Logger) } -type postUsersResponse struct { - ID int `json:"Id"` -} - -type postUsersRequest struct { - Username string `valid:"required"` - Password string `valid:""` - Role int `valid:"required"` -} - // handleGetUsers handles GET requests on /users func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) { securityContext, err := security.RetrieveRestrictedRequestContext(r) @@ -174,11 +191,6 @@ func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Reques // handlePostUserPasswd handles POST requests on /users/:id/passwd func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost}) - return - } - vars := mux.Vars(r) id := vars["id"] @@ -220,14 +232,6 @@ func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http. 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) @@ -327,18 +331,8 @@ func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request } } -type putUserRequest struct { - Password string `valid:"-"` - Role int `valid:"-"` -} - -// handlePostAdminInit handles GET requests on /users/admin/check +// handleGetAdminCheck handles GET requests on /users/admin/check func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet}) - return - } - users, err := handler.UserService.UsersByRole(portainer.AdministratorRole) if err != nil { httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) @@ -352,11 +346,6 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R // handlePostAdminInit handles POST requests on /users/admin/init func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodPost}) - return - } - var req postAdminInitRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) @@ -391,15 +380,11 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R return } if user != nil { - httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger) + httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, 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) @@ -464,37 +449,3 @@ func (handler *UserHandler) handleGetMemberships(w http.ResponseWriter, r *http. encodeJSON(w, memberships, handler.Logger) } - -// handleGetTeams handles GET requests on /users/:id/teams -func (handler *UserHandler) handleGetTeams(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - - uid, err := strconv.Atoi(id) - if err != nil { - httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) - return - } - userID := portainer.UserID(uid) - - securityContext, err := security.RetrieveRestrictedRequestContext(r) - if err != nil { - httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) - return - } - - if !security.AuthorizedUserManagement(userID, securityContext) { - httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger) - return - } - - teams, err := handler.TeamService.Teams() - if err != nil { - httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) - return - } - - filteredTeams := security.FilterUserTeams(teams, securityContext) - - encodeJSON(w, filteredTeams, handler.Logger) -} diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 9f7920c6c..e6a8fc962 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -50,7 +50,7 @@ func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler return h } -// RestrictedAccess defines defines a security check for restricted endpoints. +// RestrictedAccess defines a security check for restricted endpoints. // Authentication is required to access these endpoints. // The request context will be enhanced with a RestrictedRequestContext object // that might be used later to authorize/filter access to resources. diff --git a/api/swagger.yaml b/api/swagger.yaml new file mode 100644 index 000000000..dd13980bf --- /dev/null +++ b/api/swagger.yaml @@ -0,0 +1,2471 @@ +--- +swagger: "2.0" +info: + description: "Portainer API is an HTTP API served by Portainer. It is used by the\ + \ Portainer UI and everything you can do with the UI can be done using the HTTP\ + \ API.\nYou can find out more about Portainer at [http://portainer.io](http://portainer.io)\ + \ and get some support on [Slack](http://portainer.io/slack/).\n\n# Authentication\n\ + \nMost of the API endpoints require to be authenticated as well as some level\ + \ of authorization to be used.\nPortainer API uses JSON Web Token to manage authentication\ + \ and thus requires you to provide a token in the **Authorization** header of\ + \ each request\nwith the **Bearer** authentication mechanism.\n\nExample:\n```\n\ + Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE\n\ + ```\n\n# Security\n\nEach API endpoint has an associated access policy, it is\ + \ documented in the description of each endpoint.\n\nDifferent access policies\ + \ are available:\n* Public access\n* Authenticated access\n* Restricted access\n\ + * Administrator access\n\n### Public access\n\nNo authentication is required to\ + \ access the endpoints with this access policy.\n\n### Authenticated access\n\n\ + Authentication is required to access the endpoints with this access policy.\n\n\ + ### Restricted access\n\nAuthentication is required to access the endpoints with\ + \ this access policy.\nExtra-checks might be added to ensure access to the resource\ + \ is granted. Returned data might also be filtered.\n\n### Administrator access\n\ + \nAuthentication as well as an administrator role are required to access the endpoints\ + \ with this access policy.\n" + version: "1.13.6" + title: "Portainer API" + contact: + email: "info@portainer.io" +host: "portainer.domain" +basePath: "/api" +tags: +- name: "auth" + description: "Authenticate against Portainer HTTP API" +- name: "dockerhub" + description: "Manage how Portainer connects to the DockerHub" +- name: "endpoints" + description: "Manage Docker environments" +- name: "registries" + description: "Manage Docker registries" +- name: "resource_controls" + description: "Manage access control on Docker resources" +- name: "settings" + description: "Manage Portainer settings" +- name: "status" + description: "Information about the Portainer instance" +- name: "users" + description: "Manage users" +- name: "teams" + description: "Manage teams" +- name: "team_memberships" + description: "Manage team memberships" +- name: "templates" + description: "Manage App Templates" +- name: "upload" + description: "Upload files" +- name: "websocket" + description: "Create exec sessions using websockets" +schemes: +- "http" +- "https" +paths: + /auth: + post: + tags: + - "auth" + summary: "Authenticate a user" + description: "Use this endpoint to authenticate against Portainer using a username\ + \ and password. \n**Access policy**: public\n" + operationId: "AuthenticateUser" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Credentials used for authentication" + required: true + schema: + $ref: "#/definitions/AuthenticateUserRequest" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/AuthenticateUserResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid credentials" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + 503: + description: "Authentication disabled" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Authentication is disabled" + /dockerhub: + get: + tags: + - "dockerhub" + summary: "Retrieve DockerHub information" + description: "Use this endpoint to retrieve the information used to connect\ + \ to the DockerHub \n**Access policy**: authenticated\n" + operationId: "DockerHubInspect" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/DockerHubInspectResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + put: + tags: + - "dockerhub" + summary: "Update DockerHub information" + description: "Use this endpoint to update the information used to connect to\ + \ the DockerHub \n**Access policy**: administrator\n" + operationId: "DockerHubUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "DockerHub information" + required: true + schema: + $ref: "#/definitions/DockerHubUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /endpoints: + get: + tags: + - "endpoints" + summary: "List endpoints" + description: "List all endpoints based on the current user authorizations. Will\n\ + return all endpoints if using an administrator account otherwise it will\n\ + only return authorized endpoints. \n**Access policy**: restricted \n" + operationId: "EndpointList" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/EndpointListResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + post: + tags: + - "endpoints" + summary: "Create a new endpoint" + description: "Create a new endpoint that will be used to manage a Docker environment.\ + \ \n**Access policy**: administrator\n" + operationId: "EndpointCreate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Endpoint details" + required: true + schema: + $ref: "#/definitions/EndpointCreateRequest" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/EndpointCreateResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + 503: + description: "Endpoint management disabled" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint management is disabled" + /endpoints/{id}: + get: + tags: + - "endpoints" + summary: "Inspect an endpoint" + description: "Retrieve details abount an endpoint. \n**Access policy**: administrator\ + \ \n" + operationId: "EndpointInspect" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Endpoint identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/Endpoint" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "Endpoint not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + put: + tags: + - "endpoints" + summary: "Update an endpoint" + description: "Update an endpoint. \n**Access policy**: administrator\n" + operationId: "EndpointUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Endpoint identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "Endpoint details" + required: true + schema: + $ref: "#/definitions/EndpointUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 404: + description: "Endpoint not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + 503: + description: "Endpoint management disabled" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint management is disabled" + delete: + tags: + - "endpoints" + summary: "Remove an endpoint" + description: "Remove an endpoint. \n**Access policy**: administrator \n" + operationId: "EndpointDelete" + parameters: + - name: "id" + in: "path" + description: "Endpoint identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "Endpoint not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + 503: + description: "Endpoint management disabled" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint management is disabled" + /endpoints/{id}/access: + put: + tags: + - "endpoints" + summary: "Manage accesses to an endpoint" + description: "Manage user and team accesses to an endpoint. \n**Access policy**:\ + \ administrator \n" + operationId: "EndpointAccessUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Endpoint identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "Authorizations details" + required: true + schema: + $ref: "#/definitions/EndpointAccessUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 404: + description: "Endpoint not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /registries: + get: + tags: + - "registries" + summary: "List registries" + description: "List all registries based on the current user authorizations.\n\ + Will return all registries if using an administrator account otherwise it\n\ + will only return authorized registries. \n**Access policy**: restricted \ + \ \n" + operationId: "RegistryList" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/RegistryListResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + post: + tags: + - "registries" + summary: "Create a new registry" + description: "Create a new registry. \n**Access policy**: administrator \ + \ \n" + operationId: "RegistryCreate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Registry details" + required: true + schema: + $ref: "#/definitions/RegistryCreateRequest" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/RegistryCreateResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 409: + description: "Registry already exists" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "A registry is already defined for this URL" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /registries/{id}: + get: + tags: + - "registries" + summary: "Inspect a registry" + description: "Retrieve details about a registry. \n**Access policy**: administrator\ + \ \n" + operationId: "RegistryInspect" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Registry identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/Registry" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "Registry not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + put: + tags: + - "registries" + summary: "Update a registry" + description: "Update a registry. \n**Access policy**: administrator \n" + operationId: "RegistryUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Registry identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "Registry details" + required: true + schema: + $ref: "#/definitions/RegistryUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 404: + description: "Registry not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint not found" + 409: + description: "Registry already exists" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "A registry is already defined for this URL" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + 503: + description: "Endpoint management disabled" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Endpoint management is disabled" + delete: + tags: + - "registries" + summary: "Remove a registry" + description: "Remove a registry. \n**Access policy**: administrator \ + \ \n" + operationId: "RegistryDelete" + parameters: + - name: "id" + in: "path" + description: "Registry identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "Registry not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Registry not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /registries/{id}/access: + put: + tags: + - "registries" + summary: "Manage accesses to a registry" + description: "Manage user and team accesses to a registry. \n**Access policy**:\ + \ administrator \n" + operationId: "RegistryAccessUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Registry identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "Authorizations details" + required: true + schema: + $ref: "#/definitions/RegistryAccessUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 404: + description: "Registry not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Registry not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /resource_controls: + post: + tags: + - "resource_controls" + summary: "Create a new resource control" + description: "Create a new resource control to restrict access to a Docker resource.\ + \ \n**Access policy**: restricted \n" + operationId: "ResourceControlCreate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Resource control details" + required: true + schema: + $ref: "#/definitions/ResourceControlCreateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 409: + description: "Resource control already exists" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "A resource control is already applied on this resource" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /resource_controls/{id}: + put: + tags: + - "resource_controls" + summary: "Update a resource control" + description: "Update a resource control. \n**Access policy**: restricted \ + \ \n" + operationId: "ResourceControlUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Resource control identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "Resource control details" + required: true + schema: + $ref: "#/definitions/ResourceControlUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 404: + description: "Resource control not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Resource control not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + delete: + tags: + - "resource_controls" + summary: "Remove a resource control" + description: "Remove a resource control. \n**Access policy**: restricted \ + \ \n" + operationId: "ResourceControlDelete" + parameters: + - name: "id" + in: "path" + description: "Resource control identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 404: + description: "Resource control not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Resource control not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /settings: + get: + tags: + - "settings" + summary: "Retrieve Portainer settings" + description: "Retrieve Portainer settings. \n**Access policy**: administrator\ + \ \n" + operationId: "SettingsInspect" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/Settings" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + put: + tags: + - "settings" + summary: "Update Portainer settings" + description: "Update Portainer settings. \n**Access policy**: administrator\ + \ \n" + operationId: "SettingsUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "New settings" + required: true + schema: + $ref: "#/definitions/SettingsUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /settings/public: + get: + tags: + - "settings" + summary: "Retrieve Portainer public settings" + description: "Retrieve public settings. Returns a small set of settings that\ + \ are not reserved to administrators only. \n**Access policy**: public \ + \ \n" + operationId: "PublicSettingsInspect" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/PublicSettingsInspectResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /settings/authentication/checkLDAP: + put: + tags: + - "settings" + summary: "Test LDAP connectivity" + description: "Test LDAP connectivity using LDAP details. \n**Access policy**:\ + \ administrator \n" + operationId: "SettingsLDAPCheck" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "LDAP settings" + required: true + schema: + $ref: "#/definitions/SettingsLDAPCheckRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /status: + get: + tags: + - "status" + summary: "Check Portainer status" + description: "Retrieve Portainer status. \n**Access policy**: public \ + \ \n" + operationId: "StatusInspect" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/Status" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /users: + get: + tags: + - "users" + summary: "List users" + description: "List Portainer users. Non-administrator users will only be able\ + \ to list other non-administrator user accounts. \n**Access policy**: restricted\ + \ \n" + operationId: "UserList" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/UserListResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + post: + tags: + - "users" + summary: "Create a new user" + description: "Create a new Portainer user. Only team leaders and administrators\ + \ can create users. Only administrators can\ncreate an administrator user\ + \ account. \n**Access policy**: restricted \n" + operationId: "UserCreate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "User details" + required: true + schema: + $ref: "#/definitions/UserCreateRequest" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/UserCreateResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 409: + description: "User already exists" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "User already exists" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /users/{id}: + get: + tags: + - "users" + summary: "Inspect a user" + description: "Retrieve details about a user. \n**Access policy**: administrator\ + \ \n" + operationId: "UserInspect" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "User identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/User" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "User not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "User not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + put: + tags: + - "users" + summary: "Update a user" + description: "Update user details. A regular user account can only update his\ + \ details. \n**Access policy**: authenticated \n" + operationId: "UserUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "User identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "User details" + required: true + schema: + $ref: "#/definitions/UserUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 404: + description: "User not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "User not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + delete: + tags: + - "users" + summary: "Remove a user" + description: "Remove a user. \n**Access policy**: administrator \n" + operationId: "UserDelete" + parameters: + - name: "id" + in: "path" + description: "User identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "User not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "User not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /users/{id}/memberships: + get: + tags: + - "users" + summary: "Inspect a user memberships" + description: "Inspect a user memberships. \n**Access policy**: authenticated\ + \ \n" + operationId: "UserMembershipsInspect" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "User identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/UserMembershipsResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /users/{id}/passwd: + post: + tags: + - "users" + summary: "Check password validity for a user" + description: "Check if the submitted password is valid for the specified user.\ + \ \n**Access policy**: authenticated \n" + operationId: "UserPasswordCheck" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "User identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "User details" + required: true + schema: + $ref: "#/definitions/UserPasswordCheckRequest" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/UserPasswordCheckResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 404: + description: "User not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "User not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /users/admin/check: + get: + tags: + - "users" + summary: "Check administrator account existence" + description: "Check if an administrator account exists in the database.\n**Access\ + \ policy**: public \n" + operationId: "UserAdminCheck" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/UserListResponse" + 404: + description: "User not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "User not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /users/admin/init: + post: + tags: + - "users" + summary: "Initialize administrator account" + description: "Initialize the 'admin' user account.\n**Access policy**: public\ + \ \n" + operationId: "UserAdminInit" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "User details" + required: true + schema: + $ref: "#/definitions/UserAdminInitRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 409: + description: "Admin user already initialized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "User already exists" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /upload/tls/{certificate}: + post: + tags: + - "upload" + summary: "Upload TLS files" + description: "Use this endpoint to upload TLS files. \n**Access policy**: administrator\n" + operationId: "UploadTLS" + consumes: + - "multipart/form-data" + produces: + - "application/json" + parameters: + - name: "certificate" + in: "path" + description: "TLS file type. Valid values are 'ca', 'cert' or 'key'." + required: true + type: "string" + - name: "folder" + in: "query" + description: "Folder where the TLS file will be stored. Will be created if\ + \ not existing." + required: true + type: "string" + - name: "file" + in: "formData" + description: "The file to upload." + required: false + type: "file" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /teams: + get: + tags: + - "teams" + summary: "List teams" + description: "List teams. For non-administrator users, will only list the teams\ + \ they are member of. \n**Access policy**: restricted \n" + operationId: "TeamList" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/TeamListResponse" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + post: + tags: + - "teams" + summary: "Create a new team" + description: "Create a new team. \n**Access policy**: administrator \ + \ \n" + operationId: "TeamCreate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Team details" + required: true + schema: + $ref: "#/definitions/TeamCreateRequest" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/TeamCreateResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 409: + description: "Team already exists" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Team already exists" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /teams/{id}: + get: + tags: + - "teams" + summary: "Inspect a team" + description: "Retrieve details about a team. Access is only available for administrator\ + \ and leaders of that team. \n**Access policy**: restricted \n" + operationId: "TeamInspect" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Team identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/Team" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 404: + description: "Team not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Team not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + put: + tags: + - "teams" + summary: "Update a team" + description: "Update a team. \n**Access policy**: administrator \ + \ \n" + operationId: "TeamUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Team identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "Team details" + required: true + schema: + $ref: "#/definitions/TeamUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 404: + description: "Team not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Team not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + delete: + tags: + - "teams" + summary: "Remove a team" + description: "Remove a team. \n**Access policy**: administrator \n" + operationId: "TeamDelete" + parameters: + - name: "id" + in: "path" + description: "Team identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 404: + description: "Team not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Team not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /teams/{id}/memberships: + get: + tags: + - "teams" + summary: "Inspect a team memberships" + description: "Inspect a team memberships. Access is only available for administrator\ + \ and leaders of that team. \n**Access policy**: restricted \n" + operationId: "TeamMembershipsInspect" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Team identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/TeamMembershipsResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /team_memberships: + get: + tags: + - "team_memberships" + summary: "List team memberships" + description: "List team memberships. Access is only available to administrators\ + \ and team leaders. \n**Access policy**: restricted \n" + operationId: "TeamMembershipList" + produces: + - "application/json" + parameters: [] + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/TeamMembershipListResponse" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + post: + tags: + - "team_memberships" + summary: "Create a new team membership" + description: "Create a new team memberships. Access is only available to administrators\ + \ leaders of the associated team. \n**Access policy**: restricted \n" + operationId: "TeamMembershipCreate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - in: "body" + name: "body" + description: "Team membership details" + required: true + schema: + $ref: "#/definitions/TeamMembershipCreateRequest" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/TeamMembershipCreateResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 409: + description: "Team membership already exists" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Team membership already exists for this user and team." + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /team_memberships/{id}: + put: + tags: + - "team_memberships" + summary: "Update a team membership" + description: "Update a team membership. Access is only available to administrators\ + \ leaders of the associated team. \n**Access policy**: restricted \ + \ \n" + operationId: "TeamMembershipUpdate" + consumes: + - "application/json" + produces: + - "application/json" + parameters: + - name: "id" + in: "path" + description: "Team membership identifier" + required: true + type: "integer" + - in: "body" + name: "body" + description: "Team membership details" + required: true + schema: + $ref: "#/definitions/TeamMembershipUpdateRequest" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request data format" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 404: + description: "Team membership not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Team membership not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + delete: + tags: + - "team_memberships" + summary: "Remove a team membership" + description: "Remove a team membership. Access is only available to administrators\ + \ leaders of the associated team. \n**Access policy**: restricted \n" + operationId: "TeamMembershipDelete" + parameters: + - name: "id" + in: "path" + description: "TeamMembership identifier" + required: true + type: "integer" + responses: + 200: + description: "Success" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid request" + 403: + description: "Unauthorized" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Access denied to resource" + 404: + description: "Team membership not found" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Team membership not found" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" + /templates: + get: + tags: + - "templates" + summary: "Retrieve App templates" + description: "Retrieve App templates. \nYou can find more information about\ + \ the format at http://portainer.readthedocs.io/en/stable/templates.html \ + \ \n**Access policy**: authenticated \n" + operationId: "TemplateList" + produces: + - "application/json" + parameters: + - name: "key" + in: "query" + description: "Templates key. Valid values are 'container' or 'linuxserver.io'." + required: true + type: "string" + responses: + 200: + description: "Success" + schema: + $ref: "#/definitions/TemplateListResponse" + 400: + description: "Invalid request" + schema: + $ref: "#/definitions/GenericError" + examples: + application/json: + err: "Invalid query format" + 500: + description: "Server error" + schema: + $ref: "#/definitions/GenericError" +securityDefinitions: + jwt: + type: "apiKey" + name: "Authorization" + in: "header" +definitions: + Team: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Team identifier" + Name: + type: "string" + example: "developers" + description: "Team name" + TeamMembership: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Membership identifier" + UserID: + type: "integer" + example: 1 + description: "User identifier" + TeamID: + type: "integer" + example: 1 + description: "Team identifier" + Role: + type: "integer" + example: 1 + description: "Team role (1 for team leader and 2 for team member)" + User: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "User identifier" + Username: + type: "string" + example: "bob" + description: "Username" + Role: + type: "integer" + example: 1 + description: "User role (1 for administrator account and 2 for regular account)" + Status: + type: "object" + properties: + Authentication: + type: "boolean" + example: true + description: "Is authentication enabled" + EndpointManagement: + type: "boolean" + example: true + description: "Is endpoint management enabled" + Analytics: + type: "boolean" + example: true + description: "Is analytics enabled" + Version: + type: "string" + example: "1.13.6" + description: "Portainer API version" + PublicSettingsInspectResponse: + type: "object" + properties: + LogoURL: + type: "string" + example: "https://mycompany.mydomain.tld/logo.png" + description: "URL to a logo that will be displayed on the login page as well\ + \ as on top of the sidebar. Will use default Portainer logo when value is\ + \ empty string" + DisplayExternalContributors: + type: "boolean" + example: false + description: "Whether to display or not external templates contributions as\ + \ sub-menus in the UI." + AuthenticationMethod: + type: "integer" + example: 1 + description: "Active authentication method for the Portainer instance. Valid\ + \ values are: 1 for managed or 2 for LDAP." + TLSConfiguration: + type: "object" + properties: + TLS: + type: "boolean" + example: true + description: "Use TLS" + TLSSkipVerify: + type: "boolean" + example: false + description: "Skip the verification of the server TLS certificate" + TLSCACertPath: + type: "string" + example: "/data/tls/ca.pem" + description: "Path to the TLS CA certificate file" + TLSCertPath: + type: "string" + example: "/data/tls/cert.pem" + description: "Path to the TLS client certificate file" + TLSKeyPath: + type: "string" + example: "/data/tls/key.pem" + description: "Path to the TLS client key file" + LDAPSearchSettings: + type: "object" + properties: + BaseDN: + type: "string" + example: "dc=ldap,dc=domain,dc=tld" + description: "The distinguished name of the element from which the LDAP server\ + \ will search for users" + Filter: + type: "string" + example: "(objectClass=account)" + description: "Optional LDAP search filter used to select user elements" + UserNameAttribute: + type: "string" + example: "uid" + description: "LDAP attribute which denotes the username" + LDAPSettings: + type: "object" + properties: + ReaderDN: + type: "string" + example: "cn=readonly-account,dc=ldap,dc=domain,dc=tld" + description: "Account that will be used to search for users" + Password: + type: "string" + example: "readonly-password" + description: "Password of the account that will be used to search users" + URL: + type: "string" + example: "myldap.domain.tld:389" + description: "URL or IP address of the LDAP server" + TLSConfig: + $ref: "#/definitions/TLSConfiguration" + StartTLS: + type: "boolean" + example: true + description: "Whether LDAP connection should use StartTLS" + SearchSettings: + type: "array" + items: + $ref: "#/definitions/LDAPSearchSettings" + Settings: + type: "object" + properties: + TemplatesURL: + type: "string" + example: "https://raw.githubusercontent.com/portainer/templates/master/templates.json" + description: "URL to the templates that will be displayed in the UI when navigating\ + \ to App Templates" + LogoURL: + type: "string" + example: "https://mycompany.mydomain.tld/logo.png" + description: "URL to a logo that will be displayed on the login page as well\ + \ as on top of the sidebar. Will use default Portainer logo when value is\ + \ empty string" + BlackListedLabels: + type: "array" + description: "A list of label name & value that will be used to hide containers\ + \ when querying containers" + items: + $ref: "#/definitions/Settings_BlackListedLabels" + DisplayExternalContributors: + type: "boolean" + example: false + description: "Whether to display or not external templates contributions as\ + \ sub-menus in the UI." + AuthenticationMethod: + type: "integer" + example: 1 + description: "Active authentication method for the Portainer instance. Valid\ + \ values are: 1 for managed or 2 for LDAP." + LDAPSettings: + $ref: "#/definitions/LDAPSettings" + Settings_BlackListedLabels: + properties: + name: + type: "string" + example: "com.foo" + value: + type: "string" + example: "bar" + Registry: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Registry identifier" + Name: + type: "string" + example: "my-registry" + description: "Registry name" + URL: + type: "string" + example: "registry.mydomain.tld:2375" + description: "URL or IP address of the Docker registry" + Authentication: + type: "boolean" + example: true + description: "Is authentication against this registry enabled" + Username: + type: "string" + example: "registry_user" + description: "Username used to authenticate against this registry" + Password: + type: "string" + example: "registry_password" + description: "Password used to authenticate against this registry" + AuthorizedUsers: + type: "array" + description: "List of user identifiers authorized to use this registry" + items: + type: "integer" + example: 1 + description: "User identifier" + AuthorizedTeams: + type: "array" + description: "List of team identifiers authorized to use this registry" + items: + type: "integer" + example: 1 + description: "Team identifier" + Endpoint: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Endpoint identifier" + Name: + type: "string" + example: "my-endpoint" + description: "Endpoint name" + URL: + type: "string" + example: "docker.mydomain.tld:2375" + description: "URL or IP address of the Docker host associated to this endpoint" + AuthorizedUsers: + type: "array" + description: "List of user identifiers authorized to connect to this endpoint" + items: + type: "integer" + example: 1 + description: "User identifier" + AuthorizedTeams: + type: "array" + description: "List of team identifiers authorized to connect to this endpoint" + items: + type: "integer" + example: 1 + description: "Team identifier" + GenericError: + type: "object" + properties: + err: + type: "string" + example: "Something bad happened" + description: "Error message" + AuthenticateUserRequest: + type: "object" + required: + - "Password" + - "Username" + properties: + Username: + type: "string" + example: "admin" + description: "Username" + Password: + type: "string" + example: "mypassword" + description: "Password" + AuthenticateUserResponse: + type: "object" + properties: + jwt: + type: "string" + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE" + description: "JWT token used to authenticate against the API" + DockerHubInspectResponse: + type: "object" + properties: + Authentication: + type: "boolean" + example: true + description: "Is authentication against DockerHub enabled" + Username: + type: "string" + example: "hub_user" + description: "Username used to authenticate against the DockerHub" + Password: + type: "string" + example: "hub_password" + description: "Password used to authenticate against the DockerHub" + DockerHubUpdateRequest: + type: "object" + required: + - "Authentication" + - "Password" + - "Username" + properties: + Authentication: + type: "boolean" + example: true + description: "Enable authentication against DockerHub" + Username: + type: "string" + example: "hub_user" + description: "Username used to authenticate against the DockerHub" + Password: + type: "string" + example: "hub_password" + description: "Password used to authenticate against the DockerHub" + EndpointCreateRequest: + type: "object" + required: + - "Name" + - "URL" + properties: + Name: + type: "string" + example: "my-endpoint" + description: "Name that will be used to identify this endpoint" + URL: + type: "string" + example: "docker.mydomain.tld:2375" + description: "URL or IP address of a Docker host" + PublicURL: + type: "string" + example: "docker.mydomain.tld:2375" + description: "URL or IP address where exposed containers will be reachable.\ + \ Defaults to URL if not specified" + TLS: + type: "boolean" + example: true + description: "Require TLS to connect against this endpoint" + EndpointCreateResponse: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Id of the endpoint" + EndpointListResponse: + type: "array" + items: + $ref: "#/definitions/Endpoint" + EndpointUpdateRequest: + type: "object" + properties: + Name: + type: "string" + example: "my-endpoint" + description: "Name that will be used to identify this endpoint" + URL: + type: "string" + example: "docker.mydomain.tld:2375" + description: "URL or IP address of a Docker host" + PublicURL: + type: "string" + example: "docker.mydomain.tld:2375" + description: "URL or IP address where exposed containers will be reachable.\ + \ Defaults to URL if not specified" + TLS: + type: "boolean" + example: true + description: "Require TLS to connect against this endpoint" + EndpointAccessUpdateRequest: + type: "object" + properties: + AuthorizedUsers: + type: "array" + description: "List of user identifiers authorized to connect to this endpoint" + items: + type: "integer" + example: 1 + description: "User identifier" + AuthorizedTeams: + type: "array" + description: "List of team identifiers authorized to connect to this endpoint" + items: + type: "integer" + example: 1 + description: "Team identifier" + RegistryCreateRequest: + type: "object" + required: + - "Authentication" + - "Name" + - "Password" + - "URL" + - "Username" + properties: + Name: + type: "string" + example: "my-registry" + description: "Name that will be used to identify this registry" + URL: + type: "string" + example: "registry.mydomain.tld:2375" + description: "URL or IP address of the Docker registry" + Authentication: + type: "boolean" + example: true + description: "Is authentication against this registry enabled" + Username: + type: "string" + example: "registry_user" + description: "Username used to authenticate against this registry" + Password: + type: "string" + example: "registry_password" + description: "Password used to authenticate against this registry" + RegistryCreateResponse: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Id of the registry" + RegistryListResponse: + type: "array" + items: + $ref: "#/definitions/Registry" + RegistryUpdateRequest: + type: "object" + required: + - "Name" + - "URL" + properties: + Name: + type: "string" + example: "my-registry" + description: "Name that will be used to identify this registry" + URL: + type: "string" + example: "registry.mydomain.tld:2375" + description: "URL or IP address of the Docker registry" + Authentication: + type: "boolean" + example: true + description: "Is authentication against this registry enabled" + Username: + type: "string" + example: "registry_user" + description: "Username used to authenticate against this registry" + Password: + type: "string" + example: "registry_password" + description: "Password used to authenticate against this registry" + RegistryAccessUpdateRequest: + type: "object" + properties: + AuthorizedUsers: + type: "array" + description: "List of user identifiers authorized to use thi registry" + items: + type: "integer" + example: 1 + description: "User identifier" + AuthorizedTeams: + type: "array" + description: "List of team identifiers authorized to use thi registry" + items: + type: "integer" + example: 1 + description: "Team identifier" + ResourceControlCreateRequest: + type: "object" + required: + - "ResourceID" + - "Type" + properties: + ResourceID: + type: "string" + example: "617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08" + description: "Docker resource identifier on which access control will be applied" + Type: + type: "string" + example: "container" + description: "Type of Docker resource. Valid values are: container, volume\ + \ or service" + AdministratorsOnly: + type: "boolean" + example: true + description: "Restrict access to the associated resource to administrators\ + \ only" + Users: + type: "array" + description: "List of user identifiers with access to the associated resource" + items: + type: "integer" + example: 1 + description: "User identifier" + Teams: + type: "array" + description: "List of team identifiers with access to the associated resource" + items: + type: "integer" + example: 1 + description: "Team identifier" + SubResourceIDs: + type: "array" + description: "List of Docker resources that will inherit this access control" + items: + type: "string" + example: "617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08" + description: "Docker resource identifier" + ResourceControlUpdateRequest: + type: "object" + properties: + AdministratorsOnly: + type: "boolean" + example: false + description: "Restrict access to the associated resource to administrators\ + \ only" + Users: + type: "array" + description: "List of user identifiers with access to the associated resource" + items: + type: "integer" + example: 1 + description: "User identifier" + Teams: + type: "array" + description: "List of team identifiers with access to the associated resource" + items: + type: "integer" + example: 1 + description: "Team identifier" + SettingsUpdateRequest: + type: "object" + required: + - "AuthenticationMethod" + - "TemplatesURL" + properties: + TemplatesURL: + type: "string" + example: "https://raw.githubusercontent.com/portainer/templates/master/templates.json" + description: "URL to the templates that will be displayed in the UI when navigating\ + \ to App Templates" + LogoURL: + type: "string" + example: "https://mycompany.mydomain.tld/logo.png" + description: "URL to a logo that will be displayed on the login page as well\ + \ as on top of the sidebar. Will use default Portainer logo when value is\ + \ empty string" + BlackListedLabels: + type: "array" + description: "A list of label name & value that will be used to hide containers\ + \ when querying containers" + items: + $ref: "#/definitions/Settings_BlackListedLabels" + DisplayExternalContributors: + type: "boolean" + example: false + description: "Whether to display or not external templates contributions as\ + \ sub-menus in the UI." + AuthenticationMethod: + type: "integer" + example: 1 + description: "Active authentication method for the Portainer instance. Valid\ + \ values are: 1 for managed or 2 for LDAP." + LDAPSettings: + $ref: "#/definitions/LDAPSettings" + UserCreateRequest: + type: "object" + required: + - "Password" + - "Role" + - "Username" + properties: + Username: + type: "string" + example: "bob" + description: "Username" + Password: + type: "string" + example: "cg9Wgky3" + description: "Password" + Role: + type: "integer" + example: 1 + description: "User role (1 for administrator account and 2 for regular account)" + UserCreateResponse: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Id of the user" + UserListResponse: + type: "array" + items: + $ref: "#/definitions/User" + UserUpdateRequest: + type: "object" + properties: + Password: + type: "string" + example: "cg9Wgky3" + description: "Password" + Role: + type: "integer" + example: 1 + description: "User role (1 for administrator account and 2 for regular account)" + UserMembershipsResponse: + type: "array" + items: + $ref: "#/definitions/TeamMembership" + UserPasswordCheckRequest: + type: "object" + required: + - "Password" + properties: + Password: + type: "string" + example: "cg9Wgky3" + description: "Password" + UserPasswordCheckResponse: + type: "object" + properties: + valid: + type: "boolean" + example: true + description: "Is the password valid" + TeamCreateRequest: + type: "object" + required: + - "Name" + properties: + Name: + type: "string" + example: "developers" + description: "Name" + TeamCreateResponse: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Id of the team" + TeamListResponse: + type: "array" + items: + $ref: "#/definitions/Team" + TeamUpdateRequest: + type: "object" + required: + - "Name" + properties: + Name: + type: "string" + example: "developers" + description: "Name" + TeamMembershipsResponse: + type: "array" + items: + $ref: "#/definitions/TeamMembership" + TeamMembershipCreateRequest: + type: "object" + required: + - "Role" + - "TeamID" + - "UserID" + properties: + UserID: + type: "integer" + example: 1 + description: "User identifier" + TeamID: + type: "integer" + example: 1 + description: "Team identifier" + Role: + type: "integer" + example: 1 + description: "Role for the user inside the team (1 for leader and 2 for regular\ + \ member)" + TeamMembershipCreateResponse: + type: "object" + properties: + Id: + type: "integer" + example: 1 + description: "Id of the team membership" + TeamMembershipListResponse: + type: "array" + items: + $ref: "#/definitions/TeamMembership" + TeamMembershipUpdateRequest: + type: "object" + required: + - "Role" + - "TeamID" + - "UserID" + properties: + UserID: + type: "integer" + example: 1 + description: "User identifier" + TeamID: + type: "integer" + example: 1 + description: "Team identifier" + Role: + type: "integer" + example: 1 + description: "Role for the user inside the team (1 for leader and 2 for regular\ + \ member)" + SettingsLDAPCheckRequest: + type: "object" + properties: + LDAPSettings: + $ref: "#/definitions/LDAPSettings" + UserAdminInitRequest: + type: "object" + properties: + Password: + type: "string" + example: "admin-password" + description: "Password for the admin user" + TemplateListResponse: + type: "array" + items: + $ref: "#/definitions/Template" + Template: + type: "object" + properties: + title: + type: "string" + example: "Nginx" + description: "Title of the template" + description: + type: "string" + example: "High performance web server" + description: "Description of the template" + logo: + type: "string" + example: "https://cloudinovasi.id/assets/img/logos/nginx.png" + description: "URL of the template's logo" + image: + type: "string" + example: "nginx:latest" + description: "The Docker image associated to the template" diff --git a/app/directives/accessControlForm/porAccessControlFormController.js b/app/directives/accessControlForm/porAccessControlFormController.js index cd40cc10c..c567bf290 100644 --- a/app/directives/accessControlForm/porAccessControlFormController.js +++ b/app/directives/accessControlForm/porAccessControlFormController.js @@ -1,6 +1,6 @@ angular.module('portainer') -.controller('porAccessControlFormController', ['$q', 'UserService', 'Notifications', 'Authentication', 'ResourceControlService', -function ($q, UserService, Notifications, Authentication, ResourceControlService) { +.controller('porAccessControlFormController', ['$q', 'UserService', 'TeamService', 'Notifications', 'Authentication', 'ResourceControlService', +function ($q, UserService, TeamService, Notifications, Authentication, ResourceControlService) { var ctrl = this; ctrl.availableTeams = []; @@ -42,7 +42,7 @@ function ($q, UserService, Notifications, Authentication, ResourceControlService } $q.all({ - availableTeams: UserService.userTeams(userDetails.ID), + availableTeams: TeamService.teams(), availableUsers: isAdmin ? UserService.users(false) : [] }) .then(function success(data) { diff --git a/app/directives/accessControlPanel/porAccessControlPanelController.js b/app/directives/accessControlPanel/porAccessControlPanelController.js index 32c3f8635..36cec2a97 100644 --- a/app/directives/accessControlPanel/porAccessControlPanelController.js +++ b/app/directives/accessControlPanel/porAccessControlPanelController.js @@ -1,6 +1,6 @@ angular.module('portainer') -.controller('porAccessControlPanelController', ['$q', '$state', 'UserService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'FormValidator', -function ($q, $state, UserService, ResourceControlService, Notifications, Authentication, ModalService, FormValidator) { +.controller('porAccessControlPanelController', ['$q', '$state', 'UserService', 'TeamService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'FormValidator', +function ($q, $state, UserService, TeamService, ResourceControlService, Notifications, Authentication, ModalService, FormValidator) { var ctrl = this; @@ -121,7 +121,7 @@ function ($q, $state, UserService, ResourceControlService, Notifications, Authen return $q.all({ availableUsers: isAdmin ? UserService.users(false) : [], - availableTeams: isAdmin || data.isPartOfRestrictedUsers ? UserService.userTeams(userId) : [] + availableTeams: isAdmin || data.isPartOfRestrictedUsers ? TeamService.teams() : [] }); }) .then(function success(data) { diff --git a/app/rest/api/user.js b/app/rest/api/user.js index f5b59873d..12a8df34a 100644 --- a/app/rest/api/user.js +++ b/app/rest/api/user.js @@ -8,7 +8,6 @@ angular.module('portainer.rest') update: { method: 'PUT', params: { id: '@id' } }, remove: { method: 'DELETE', params: { id: '@id'} }, queryMemberships: { method: 'GET', isArray: true, params: { id: '@id', entity: 'memberships' } }, - queryTeams: { method: 'GET', isArray: true, params: { id: '@id', entity: 'teams' } }, // RPCs should be moved to a specific endpoint checkPassword: { method: 'POST', params: { id: '@id', entity: 'passwd' } }, checkAdminUser: { method: 'GET', params: { id: 'admin', entity: 'check' }, isArray: true }, diff --git a/app/services/api/userService.js b/app/services/api/userService.js index 24e0f97a2..7e3bf2b66 100644 --- a/app/services/api/userService.js +++ b/app/services/api/userService.js @@ -1,5 +1,5 @@ angular.module('portainer.services') -.factory('UserService', ['$q', 'Users', 'UserHelper', 'TeamMembershipService', function UserServiceFactory($q, Users, UserHelper, TeamMembershipService) { +.factory('UserService', ['$q', 'Users', 'UserHelper', 'TeamService', 'TeamMembershipService', function UserServiceFactory($q, Users, UserHelper, TeamService, TeamMembershipService) { 'use strict'; var service = {}; @@ -110,28 +110,11 @@ angular.module('portainer.services') return deferred.promise; }; - service.userTeams = function(id) { - var deferred = $q.defer(); - - Users.queryTeams({id: id}).$promise - .then(function success(data) { - var teams = data.map(function (item) { - return new TeamViewModel(item); - }); - deferred.resolve(teams); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve user teams', err: err }); - }); - - return deferred.promise; - }; - service.userLeadingTeams = function(id) { var deferred = $q.defer(); $q.all({ - teams: service.userTeams(id), + teams: TeamService.teams(), memberships: service.userMemberships(id) }) .then(function success(data) { From ef13f6fb3b65600e8bc672140a18a0c1db97e5a8 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sun, 13 Aug 2017 16:55:02 +0200 Subject: [PATCH 29/30] feat(sidebar): do not display services and secrets when managing a worker node (#1114) --- app/components/sidebar/sidebar.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index 0e44812a6..068dab150 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -25,7 +25,7 @@ LinuxServer.io

    - -