From d8f8ab785c2f7e76632fc8207562e87e4e8da93d Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 15 Jun 2017 22:52:49 +0200 Subject: [PATCH 01/10] fix(service-details): fix the ability to sort tasks (#931) --- app/components/service/includes/tasks.html | 16 ++++++++-------- app/components/service/serviceController.js | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/components/service/includes/tasks.html b/app/components/service/includes/tasks.html index 51d8fdf9f..f90df4270 100644 --- a/app/components/service/includes/tasks.html +++ b/app/components/service/includes/tasks.html @@ -18,28 +18,28 @@ Id - + Status - - + + - + Slot - + Node - - + + - + Last update diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 8cc6453e1..535c10d60 100644 --- a/app/components/service/serviceController.js +++ b/app/components/service/serviceController.js @@ -5,8 +5,8 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); $scope.tasks = []; - $scope.sortType = 'Status'; - $scope.sortReverse = false; + $scope.sortType = 'Updated'; + $scope.sortReverse = true; $scope.lastVersion = 0; From f2b970034583826175da7461c70ae8bc5b97c32b Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sat, 17 Jun 2017 15:20:19 +0200 Subject: [PATCH 02/10] chore(codeclimate): update mass_threshold for the duplication engine --- .codeclimate.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index b6deabfde..84d9c8eda 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -12,7 +12,8 @@ engines: enabled: true config: languages: - - javascript + javascript: + mass_threshold: 80 eslint: enabled: true config: From 4ec65a80dfa7ab3a900f76e935b4925d48d9363c Mon Sep 17 00:00:00 2001 From: Gabriel Lewertowski Date: Sat, 17 Jun 2017 15:25:23 +0200 Subject: [PATCH 03/10] fix(user-creation): sanitize username and password (#934) --- app/components/users/usersController.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/users/usersController.js b/app/components/users/usersController.js index c9c940ec7..bc595c2f6 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', 'UserService', 'TeamService', 'TeamMembershipService', 'ModalService', 'Notifications', 'Pagination', 'Authentication', -function ($q, $scope, $state, UserService, TeamService, TeamMembershipService, ModalService, Notifications, Pagination, Authentication) { +.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) { $scope.state = { userCreationError: '', selectedItemCount: 0, @@ -59,8 +59,8 @@ function ($q, $scope, $state, UserService, TeamService, TeamMembershipService, M $scope.addUser = function() { $('#createUserSpinner').show(); $scope.state.userCreationError = ''; - var username = $scope.formValues.Username; - var password = $scope.formValues.Password; + var username = $sanitize($scope.formValues.Username); + var password = $sanitize($scope.formValues.Password); var role = $scope.formValues.Administrator ? 1 : 2; var teamIds = []; angular.forEach($scope.formValues.Teams, function(team) { From db324998e31b4effef5c850296b15bfd584bba30 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sat, 17 Jun 2017 16:50:35 +0200 Subject: [PATCH 04/10] fix(templates): display templates without platform (#937) --- app/models/api/template.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/api/template.js b/app/models/api/template.js index b7ade6535..01532f6f6 100644 --- a/app/models/api/template.js +++ b/app/models/api/template.js @@ -3,7 +3,7 @@ function TemplateViewModel(data) { this.Description = data.description; this.Note = data.note; this.Categories = data.categories ? data.categories : []; - this.Platform = data.platform ? data.platform : ''; + this.Platform = data.platform ? data.platform : 'undefined'; this.Logo = data.logo; this.Image = data.image; this.Registry = data.registry ? data.registry : ''; From a812f4729c9ba4d4b7439e3736721d6b73df8153 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sat, 17 Jun 2017 17:05:34 +0200 Subject: [PATCH 05/10] docs(README): update links to portainer.io --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4312eb017..a3483f6f9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@

- +

[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/) [![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) -[![Slack](http://portainer.io/slack/badge.svg)](http://portainer.io/slack/) +[![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) @@ -19,7 +19,7 @@ ## Demo - + You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**). @@ -35,7 +35,7 @@ Please note that the public demo cluster is **reset every 15min**. * Issues: https://github.com/portainer/portainer/issues * FAQ: https://portainer.readthedocs.io/en/latest/faq.html * Gitter (chat): https://gitter.im/portainer/Lobby -* Slack: http://portainer.io/slack/ +* Slack: https://portainer.io/slack/ ## Reporting bugs and contributing From 9360f24d893aabc1379d4556c2339228af5ab44b Mon Sep 17 00:00:00 2001 From: eliat123 Date: Tue, 20 Jun 2017 13:54:27 +0300 Subject: [PATCH 06/10] feat(service-details): add quick navigation menu anchors (#875) --- .../service/includes/constraints.html | 2 +- .../service/includes/containerlabels.html | 2 +- .../service/includes/environmentvariables.html | 2 +- app/components/service/includes/mounts.html | 2 +- app/components/service/includes/networks.html | 2 +- app/components/service/includes/resources.html | 2 +- app/components/service/includes/restart.html | 2 +- app/components/service/includes/secrets.html | 2 +- .../service/includes/servicelabels.html | 2 +- app/components/service/includes/tasks.html | 5 +++-- .../service/includes/updateconfig.html | 2 +- app/components/service/service.html | 2 +- app/components/service/serviceController.js | 17 ++++++++++++++--- 13 files changed, 28 insertions(+), 16 deletions(-) diff --git a/app/components/service/includes/constraints.html b/app/components/service/includes/constraints.html index 6a880db95..3817f939d 100644 --- a/app/components/service/includes/constraints.html +++ b/app/components/service/includes/constraints.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/containerlabels.html b/app/components/service/includes/containerlabels.html index f41864357..78607b23d 100644 --- a/app/components/service/includes/containerlabels.html +++ b/app/components/service/includes/containerlabels.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/environmentvariables.html b/app/components/service/includes/environmentvariables.html index 6b0352e2e..4b594a8fb 100644 --- a/app/components/service/includes/environmentvariables.html +++ b/app/components/service/includes/environmentvariables.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/mounts.html b/app/components/service/includes/mounts.html index bdfddc8d7..0b3a41743 100644 --- a/app/components/service/includes/mounts.html +++ b/app/components/service/includes/mounts.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/networks.html b/app/components/service/includes/networks.html index f3c551ac4..309d4b40c 100644 --- a/app/components/service/includes/networks.html +++ b/app/components/service/includes/networks.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/resources.html b/app/components/service/includes/resources.html index f96b8dac4..12228cb2f 100644 --- a/app/components/service/includes/resources.html +++ b/app/components/service/includes/resources.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/restart.html b/app/components/service/includes/restart.html index 22fee2774..e25b0e0dc 100644 --- a/app/components/service/includes/restart.html +++ b/app/components/service/includes/restart.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/secrets.html b/app/components/service/includes/secrets.html index 2ca48e907..4a2d16572 100644 --- a/app/components/service/includes/secrets.html +++ b/app/components/service/includes/secrets.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/servicelabels.html b/app/components/service/includes/servicelabels.html index e8cd0aa1e..0857dbe30 100644 --- a/app/components/service/includes/servicelabels.html +++ b/app/components/service/includes/servicelabels.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/includes/tasks.html b/app/components/service/includes/tasks.html index f90df4270..49a2845a8 100644 --- a/app/components/service/includes/tasks.html +++ b/app/components/service/includes/tasks.html @@ -1,4 +1,4 @@ -
+
@@ -57,9 +57,10 @@ -
+
+ diff --git a/app/components/service/includes/updateconfig.html b/app/components/service/includes/updateconfig.html index 2fe2d89d8..0a0b9180d 100644 --- a/app/components/service/includes/updateconfig.html +++ b/app/components/service/includes/updateconfig.html @@ -1,4 +1,4 @@ -
+
diff --git a/app/components/service/service.html b/app/components/service/service.html index e66b4f0b0..c854c8da1 100644 --- a/app/components/service/service.html +++ b/app/components/service/service.html @@ -111,7 +111,7 @@
  • Service labels
  • Secrets
  • Tasks
  • -
      +
    diff --git a/app/components/service/serviceController.js b/app/components/service/serviceController.js index 535c10d60..a69c2fb78 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', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline', -function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) { +.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$timeout', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline', +function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('service_tasks'); @@ -37,7 +37,11 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer }; $scope.goToItem = function(hash) { - $anchorScroll(hash); + if ($location.hash() !== hash) { + $location.hash(hash); + } else { + $anchorScroll(); + } }; $scope.addEnvironmentVariable = function addEnvironmentVariable(service) { @@ -291,15 +295,22 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer .then(function success(data) { $scope.tasks = data.tasks; $scope.nodes = data.nodes; + $scope.secrets = data.secrets.map(function (secret) { return new SecretViewModel(secret); }); + + $timeout(function() { + $anchorScroll(); + }); + }) .catch(function error(err) { $scope.secrets = []; Notifications.error('Failure', err, 'Unable to retrieve service details'); }) .finally(function final() { + $('#loadingViewSpinner').hide(); }); } From 08c5a5a4f65878fff82e6dc98a875a6d443f4cdf Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 20 Jun 2017 13:00:32 +0200 Subject: [PATCH 07/10] feat(registries): add registry management (#930) --- api/bolt/datastore.go | 48 ++- api/bolt/dockerhub_service.go | 61 ++++ api/bolt/endpoint_service.go | 2 +- api/bolt/internal/internal.go | 20 ++ api/bolt/registry_service.go | 114 +++++++ api/cmd/portainer/main.go | 23 ++ api/errors.go | 11 + api/http/handler/dockerhub.go | 87 +++++ api/http/handler/handler.go | 6 + api/http/handler/registry.go | 312 ++++++++++++++++++ api/http/security/filter.go | 34 ++ api/http/server.go | 8 + api/portainer.go | 39 +++ app/app.js | 58 ++++ .../accessControlForm/accessControlForm.html | 10 +- .../accessControlPanel.html | 10 +- app/components/container/container.html | 16 +- .../containers/containersController.js | 21 +- .../createContainerController.js | 6 +- .../createContainer/createcontainer.html | 16 +- .../createRegistryController.js | 49 +++ .../createRegistry/createregistry.html | 117 +++++++ .../createService/createServiceController.js | 12 +- .../createService/createservice.html | 16 +- .../createVolume/createVolumeController.js | 6 +- .../dashboard/dashboardController.js | 6 +- app/components/docker/docker.html | 6 +- app/components/docker/dockerController.js | 36 +- .../endpointAccess/endpointAccess.html | 136 +------- .../endpointAccessController.js | 180 +--------- app/components/events/eventsController.js | 32 +- app/components/image/image.html | 18 +- app/components/image/imageController.js | 47 +-- app/components/images/images.html | 18 +- app/components/images/imagesController.js | 7 +- app/components/registries/registries.html | 150 +++++++++ .../registries/registriesController.js | 113 +++++++ app/components/registry/registry.html | 78 +++++ app/components/registry/registryController.js | 37 +++ .../registryAccess/registryAccess.html | 45 +++ .../registryAccessController.js | 24 ++ app/components/sidebar/sidebar.html | 3 + app/components/swarm/swarm.html | 1 + app/components/swarm/swarmController.js | 67 ++-- .../accessManagement/por-access-management.js | 8 + .../accessManagement/porAccessManagement.html | 134 ++++++++ .../porAccessManagementController.js | 157 +++++++++ .../imageRegistry/por-image-registry.js | 8 + .../imageRegistry/porImageRegistry.html | 12 + .../porImageRegistryController.js | 23 ++ app/helpers/imageHelper.js | 11 +- app/helpers/registryHelper.js | 18 + .../api/{endpointAccess.js => access.js} | 4 +- app/models/api/dockerhub.js | 7 + app/models/api/registry.js | 11 + app/rest/api/dockerhub.js | 8 + app/rest/api/registry.js | 12 + app/rest/docker/event.js | 13 - app/rest/docker/image.js | 9 +- app/rest/docker/info.js | 7 - app/rest/docker/service.js | 7 +- app/rest/docker/system.js | 17 + app/rest/docker/version.js | 7 - app/services/api/accessService.js | 58 ++++ app/services/api/dockerhubService.js | 26 ++ app/services/api/registryService.js | 92 ++++++ app/services/api/settingsService.js | 4 +- app/services/docker/imageService.js | 53 +-- app/services/docker/infoService.js | 20 -- app/services/docker/systemService.js | 45 +++ app/services/httpRequestHelper.js | 17 + app/services/notifications.js | 4 + app/services/stateManager.js | 20 +- assets/css/app.css | 18 +- gruntfile.js | 2 +- 75 files changed, 2317 insertions(+), 621 deletions(-) create mode 100644 api/bolt/dockerhub_service.go create mode 100644 api/bolt/registry_service.go create mode 100644 api/http/handler/dockerhub.go create mode 100644 api/http/handler/registry.go create mode 100644 app/components/createRegistry/createRegistryController.js create mode 100644 app/components/createRegistry/createregistry.html create mode 100644 app/components/registries/registries.html create mode 100644 app/components/registries/registriesController.js create mode 100644 app/components/registry/registry.html create mode 100644 app/components/registry/registryController.js create mode 100644 app/components/registryAccess/registryAccess.html create mode 100644 app/components/registryAccess/registryAccessController.js create mode 100644 app/directives/accessManagement/por-access-management.js create mode 100644 app/directives/accessManagement/porAccessManagement.html create mode 100644 app/directives/accessManagement/porAccessManagementController.js create mode 100644 app/directives/imageRegistry/por-image-registry.js create mode 100644 app/directives/imageRegistry/porImageRegistry.html create mode 100644 app/directives/imageRegistry/porImageRegistryController.js create mode 100644 app/helpers/registryHelper.js rename app/models/api/{endpointAccess.js => access.js} (61%) create mode 100644 app/models/api/dockerhub.js create mode 100644 app/models/api/registry.js create mode 100644 app/rest/api/dockerhub.js create mode 100644 app/rest/api/registry.js delete mode 100644 app/rest/docker/event.js delete mode 100644 app/rest/docker/info.js create mode 100644 app/rest/docker/system.js delete mode 100644 app/rest/docker/version.js create mode 100644 app/services/api/accessService.js create mode 100644 app/services/api/dockerhubService.js create mode 100644 app/services/api/registryService.js delete mode 100644 app/services/docker/infoService.js create mode 100644 app/services/docker/systemService.js create mode 100644 app/services/httpRequestHelper.js diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 3c56ede26..f0357a2a4 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -23,6 +23,8 @@ type Store struct { ResourceControlService *ResourceControlService VersionService *VersionService SettingsService *SettingsService + RegistryService *RegistryService + DockerHubService *DockerHubService db *bolt.DB checkForDataMigration bool @@ -37,6 +39,8 @@ const ( endpointBucketName = "endpoints" resourceControlBucketName = "resource_control" settingsBucketName = "settings" + registryBucketName = "registries" + dockerhubBucketName = "dockerhub" ) // NewStore initializes a new Store and the associated services @@ -50,6 +54,8 @@ func NewStore(storePath string) (*Store, error) { ResourceControlService: &ResourceControlService{}, VersionService: &VersionService{}, SettingsService: &SettingsService{}, + RegistryService: &RegistryService{}, + DockerHubService: &DockerHubService{}, } store.UserService.store = store store.TeamService.store = store @@ -58,6 +64,8 @@ func NewStore(storePath string) (*Store, error) { store.ResourceControlService.store = store store.VersionService.store = store store.SettingsService.store = store + store.RegistryService.store = store + store.DockerHubService.store = store _, err := os.Stat(storePath + "/" + databaseFileName) if err != nil && os.IsNotExist(err) { @@ -74,40 +82,26 @@ func NewStore(storePath string) (*Store, error) { // Open opens and initializes the BoltDB database. func (store *Store) Open() error { path := store.Path + "/" + databaseFileName + db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second}) if err != nil { return err } store.db = db + + bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName, + resourceControlBucketName, teamMembershipBucketName, settingsBucketName, + registryBucketName, dockerhubBucketName} + return db.Update(func(tx *bolt.Tx) error { - _, err := tx.CreateBucketIfNotExists([]byte(versionBucketName)) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists([]byte(userBucketName)) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists([]byte(teamBucketName)) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName)) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists([]byte(resourceControlBucketName)) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists([]byte(teamMembershipBucketName)) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists([]byte(settingsBucketName)) - if err != nil { - return err + + for _, bucket := range bucketsToCreate { + _, err := tx.CreateBucketIfNotExists([]byte(bucket)) + if err != nil { + return err + } } + return nil }) } diff --git a/api/bolt/dockerhub_service.go b/api/bolt/dockerhub_service.go new file mode 100644 index 000000000..34acd5594 --- /dev/null +++ b/api/bolt/dockerhub_service.go @@ -0,0 +1,61 @@ +package bolt + +import ( + "github.com/portainer/portainer" + "github.com/portainer/portainer/bolt/internal" + + "github.com/boltdb/bolt" +) + +// DockerHubService represents a service for managing registries. +type DockerHubService struct { + store *Store +} + +const ( + dbDockerHubKey = "DOCKERHUB" +) + +// DockerHub returns the DockerHub object. +func (service *DockerHubService) DockerHub() (*portainer.DockerHub, error) { + var data []byte + err := service.store.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(dockerhubBucketName)) + value := bucket.Get([]byte(dbDockerHubKey)) + if value == nil { + return portainer.ErrDockerHubNotFound + } + + data = make([]byte, len(value)) + copy(data, value) + return nil + }) + if err != nil { + return nil, err + } + + var dockerhub portainer.DockerHub + err = internal.UnmarshalDockerHub(data, &dockerhub) + if err != nil { + return nil, err + } + return &dockerhub, nil +} + +// StoreDockerHub persists a DockerHub object. +func (service *DockerHubService) StoreDockerHub(dockerhub *portainer.DockerHub) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(dockerhubBucketName)) + + data, err := internal.MarshalDockerHub(dockerhub) + if err != nil { + return err + } + + err = bucket.Put([]byte(dbDockerHubKey), data) + if err != nil { + return err + } + return nil + }) +} diff --git a/api/bolt/endpoint_service.go b/api/bolt/endpoint_service.go index d7c230e9a..a92048d7a 100644 --- a/api/bolt/endpoint_service.go +++ b/api/bolt/endpoint_service.go @@ -7,7 +7,7 @@ import ( "github.com/boltdb/bolt" ) -// EndpointService represents a service for managing users. +// EndpointService represents a service for managing endpoints. type EndpointService struct { store *Store } diff --git a/api/bolt/internal/internal.go b/api/bolt/internal/internal.go index f55f72118..3378f93b7 100644 --- a/api/bolt/internal/internal.go +++ b/api/bolt/internal/internal.go @@ -47,6 +47,16 @@ func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error { return json.Unmarshal(data, endpoint) } +// MarshalRegistry encodes a registry to binary format. +func MarshalRegistry(registry *portainer.Registry) ([]byte, error) { + return json.Marshal(registry) +} + +// UnmarshalRegistry decodes a registry from a binary data. +func UnmarshalRegistry(data []byte, registry *portainer.Registry) error { + return json.Unmarshal(data, registry) +} + // MarshalResourceControl encodes a resource control object to binary format. func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) { return json.Marshal(rc) @@ -67,6 +77,16 @@ func UnmarshalSettings(data []byte, settings *portainer.Settings) error { return json.Unmarshal(data, settings) } +// MarshalDockerHub encodes a Dockerhub object to binary format. +func MarshalDockerHub(settings *portainer.DockerHub) ([]byte, error) { + return json.Marshal(settings) +} + +// UnmarshalDockerHub decodes a Dockerhub object from a binary data. +func UnmarshalDockerHub(data []byte, settings *portainer.DockerHub) error { + return json.Unmarshal(data, settings) +} + // Itob returns an 8-byte big endian representation of v. // This function is typically used for encoding integer IDs to byte slices // so that they can be used as BoltDB keys. diff --git a/api/bolt/registry_service.go b/api/bolt/registry_service.go new file mode 100644 index 000000000..4c0c393ae --- /dev/null +++ b/api/bolt/registry_service.go @@ -0,0 +1,114 @@ +package bolt + +import ( + "github.com/portainer/portainer" + "github.com/portainer/portainer/bolt/internal" + + "github.com/boltdb/bolt" +) + +// RegistryService represents a service for managing registries. +type RegistryService struct { + store *Store +} + +// Registry returns an registry by ID. +func (service *RegistryService) Registry(ID portainer.RegistryID) (*portainer.Registry, error) { + var data []byte + err := service.store.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(registryBucketName)) + value := bucket.Get(internal.Itob(int(ID))) + if value == nil { + return portainer.ErrRegistryNotFound + } + + data = make([]byte, len(value)) + copy(data, value) + return nil + }) + if err != nil { + return nil, err + } + + var registry portainer.Registry + err = internal.UnmarshalRegistry(data, ®istry) + if err != nil { + return nil, err + } + return ®istry, nil +} + +// Registries returns an array containing all the registries. +func (service *RegistryService) Registries() ([]portainer.Registry, error) { + var registries = make([]portainer.Registry, 0) + err := service.store.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(registryBucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var registry portainer.Registry + err := internal.UnmarshalRegistry(v, ®istry) + if err != nil { + return err + } + registries = append(registries, registry) + } + + return nil + }) + if err != nil { + return nil, err + } + + return registries, nil +} + +// CreateRegistry creates a new registry. +func (service *RegistryService) CreateRegistry(registry *portainer.Registry) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(registryBucketName)) + + id, _ := bucket.NextSequence() + registry.ID = portainer.RegistryID(id) + + data, err := internal.MarshalRegistry(registry) + if err != nil { + return err + } + + err = bucket.Put(internal.Itob(int(registry.ID)), data) + if err != nil { + return err + } + return nil + }) +} + +// UpdateRegistry updates an registry. +func (service *RegistryService) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error { + data, err := internal.MarshalRegistry(registry) + if err != nil { + return err + } + + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(registryBucketName)) + err = bucket.Put(internal.Itob(int(ID)), data) + if err != nil { + return err + } + return nil + }) +} + +// DeleteRegistry deletes an registry. +func (service *RegistryService) DeleteRegistry(ID portainer.RegistryID) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(registryBucketName)) + err := bucket.Delete(internal.Itob(int(ID))) + if err != nil { + return err + } + return nil + }) +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 9876de6aa..a3e265544 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -91,6 +91,22 @@ func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portaine } } +func initDockerHub(dockerHubService portainer.DockerHubService) error { + _, err := dockerHubService.DockerHub() + if err == portainer.ErrDockerHubNotFound { + dockerhub := &portainer.DockerHub{ + Authentication: false, + Username: "", + Password: "", + } + return dockerHubService.StoreDockerHub(dockerhub) + } else if err != nil { + return err + } + + return nil +} + func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error { _, err := settingsService.Settings() if err == portainer.ErrSettingsNotFound { @@ -146,6 +162,11 @@ func main() { log.Fatal(err) } + err = initDockerHub(store.DockerHubService) + if err != nil { + log.Fatal(err) + } + applicationStatus := initStatus(authorizeEndpointMgmt, flags) if *flags.Endpoint != "" { @@ -199,6 +220,8 @@ func main() { EndpointService: store.EndpointService, ResourceControlService: store.ResourceControlService, SettingsService: store.SettingsService, + RegistryService: store.RegistryService, + DockerHubService: store.DockerHubService, CryptoService: cryptoService, JWTService: jwtService, FileService: fileService, diff --git a/api/errors.go b/api/errors.go index 0be2338d8..bf5f5517a 100644 --- a/api/errors.go +++ b/api/errors.go @@ -42,6 +42,12 @@ const ( ErrEndpointAccessDenied = Error("Access denied to endpoint") ) +// Registry errors. +const ( + ErrRegistryNotFound = Error("Registry not found") + ErrRegistryAlreadyExists = Error("A registry is already defined for this URL") +) + // Version errors. const ( ErrDBVersionNotFound = Error("DB version not found") @@ -52,6 +58,11 @@ const ( ErrSettingsNotFound = Error("Settings not found") ) +// DockerHub errors. +const ( + ErrDockerHubNotFound = Error("Dockerhub not found") +) + // Crypto errors. const ( ErrCryptoHashFailure = Error("Unable to hash data") diff --git a/api/http/handler/dockerhub.go b/api/http/handler/dockerhub.go new file mode 100644 index 000000000..56b8eed4e --- /dev/null +++ b/api/http/handler/dockerhub.go @@ -0,0 +1,87 @@ +package handler + +import ( + "encoding/json" + + "github.com/asaskevich/govalidator" + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/security" + + "log" + "net/http" + "os" + + "github.com/gorilla/mux" +) + +// DockerHubHandler represents an HTTP API handler for managing DockerHub. +type DockerHubHandler struct { + *mux.Router + Logger *log.Logger + DockerHubService portainer.DockerHubService +} + +// NewDockerHubHandler returns a new instance of OldDockerHubHandler. +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) + h.Handle("/dockerhub", + bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutDockerHub))).Methods(http.MethodPut) + + return h +} + +// handleGetDockerHub handles GET requests on /dockerhub +func (handler *DockerHubHandler) handleGetDockerHub(w http.ResponseWriter, r *http.Request) { + dockerhub, err := handler.DockerHubService.DockerHub() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + encodeJSON(w, dockerhub, handler.Logger) + return +} + +// handlePutDockerHub handles PUT requests on /dockerhub +func (handler *DockerHubHandler) handlePutDockerHub(w http.ResponseWriter, r *http.Request) { + var req putDockerHubRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) + return + } + + _, err := govalidator.ValidateStruct(req) + if err != nil { + httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + + dockerhub := &portainer.DockerHub{ + Authentication: false, + Username: "", + Password: "", + } + + if req.Authentication { + dockerhub.Authentication = true + dockerhub.Username = req.Username + dockerhub.Password = req.Password + } + + err = handler.DockerHubService.StoreDockerHub(dockerhub) + if err != nil { + 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/handler.go b/api/http/handler/handler.go index 0692dc6de..7fcb58c56 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -17,6 +17,8 @@ type Handler struct { TeamHandler *TeamHandler TeamMembershipHandler *TeamMembershipHandler EndpointHandler *EndpointHandler + RegistryHandler *RegistryHandler + DockerHubHandler *DockerHubHandler ResourceHandler *ResourceHandler StatusHandler *StatusHandler SettingsHandler *SettingsHandler @@ -50,6 +52,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 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) + } 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") { + http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) } else if 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") { diff --git a/api/http/handler/registry.go b/api/http/handler/registry.go new file mode 100644 index 000000000..164a5f3c1 --- /dev/null +++ b/api/http/handler/registry.go @@ -0,0 +1,312 @@ +package handler + +import ( + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/security" + + "encoding/json" + "log" + "net/http" + "os" + "strconv" + + "github.com/asaskevich/govalidator" + "github.com/gorilla/mux" +) + +// RegistryHandler represents an HTTP API handler for managing Docker registries. +type RegistryHandler struct { + *mux.Router + Logger *log.Logger + RegistryService portainer.RegistryService +} + +// NewRegistryHandler returns a new instance of RegistryHandler. +func NewRegistryHandler(bouncer *security.RequestBouncer) *RegistryHandler { + h := &RegistryHandler{ + Router: mux.NewRouter(), + Logger: log.New(os.Stderr, "", log.LstdFlags), + } + h.Handle("/registries", + bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostRegistries))).Methods(http.MethodPost) + h.Handle("/registries", + bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetRegistries))).Methods(http.MethodGet) + h.Handle("/registries/{id}", + bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetRegistry))).Methods(http.MethodGet) + h.Handle("/registries/{id}", + bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutRegistry))).Methods(http.MethodPut) + h.Handle("/registries/{id}/access", + bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutRegistryAccess))).Methods(http.MethodPut) + h.Handle("/registries/{id}", + bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteRegistry))).Methods(http.MethodDelete) + + return h +} + +// handleGetRegistries handles GET requests on /registries +func (handler *RegistryHandler) handleGetRegistries(w http.ResponseWriter, r *http.Request) { + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + registries, err := handler.RegistryService.Registries() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + filteredRegistries, err := security.FilterRegistries(registries, securityContext) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + encodeJSON(w, filteredRegistries, handler.Logger) +} + +// handlePostRegistries handles POST requests on /registries +func (handler *RegistryHandler) handlePostRegistries(w http.ResponseWriter, r *http.Request) { + var req postRegistriesRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) + return + } + + _, err := govalidator.ValidateStruct(req) + if err != nil { + httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + + registries, err := handler.RegistryService.Registries() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + for _, r := range registries { + if r.URL == req.URL { + httperror.WriteErrorResponse(w, portainer.ErrRegistryAlreadyExists, http.StatusConflict, handler.Logger) + return + } + } + + registry := &portainer.Registry{ + Name: req.Name, + URL: req.URL, + Authentication: req.Authentication, + Username: req.Username, + Password: req.Password, + AuthorizedUsers: []portainer.UserID{}, + AuthorizedTeams: []portainer.TeamID{}, + } + + err = handler.RegistryService.CreateRegistry(registry) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + 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) + id := vars["id"] + + registryID, err := strconv.Atoi(id) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) + return + } + + registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + if err == portainer.ErrRegistryNotFound { + httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + encodeJSON(w, registry, handler.Logger) +} + +// handlePutRegistryAccess handles PUT requests on /registries/:id/access +func (handler *RegistryHandler) handlePutRegistryAccess(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + registryID, err := strconv.Atoi(id) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) + return + } + + var req putRegistryAccessRequest + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) + return + } + + _, err = govalidator.ValidateStruct(req) + if err != nil { + httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + + registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + if err == portainer.ErrRegistryNotFound { + httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + if req.AuthorizedUsers != nil { + authorizedUserIDs := []portainer.UserID{} + for _, value := range req.AuthorizedUsers { + authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value)) + } + registry.AuthorizedUsers = authorizedUserIDs + } + + if req.AuthorizedTeams != nil { + authorizedTeamIDs := []portainer.TeamID{} + for _, value := range req.AuthorizedTeams { + authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value)) + } + registry.AuthorizedTeams = authorizedTeamIDs + } + + err = handler.RegistryService.UpdateRegistry(registry.ID, registry) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } +} + +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) + id := vars["id"] + + registryID, err := strconv.Atoi(id) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) + return + } + + var req putRegistriesRequest + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) + return + } + + _, err = govalidator.ValidateStruct(req) + if err != nil { + httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + + registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + if err == portainer.ErrRegistryNotFound { + httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + registries, err := handler.RegistryService.Registries() + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + for _, r := range registries { + if r.URL == req.URL && r.ID != registry.ID { + httperror.WriteErrorResponse(w, portainer.ErrRegistryAlreadyExists, http.StatusConflict, handler.Logger) + return + } + } + + if req.Name != "" { + registry.Name = req.Name + } + + if req.URL != "" { + registry.URL = req.URL + } + + if req.Authentication { + registry.Authentication = true + registry.Username = req.Username + registry.Password = req.Password + } else { + registry.Authentication = false + registry.Username = "" + registry.Password = "" + } + + err = handler.RegistryService.UpdateRegistry(registry.ID, registry) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } +} + +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) + id := vars["id"] + + registryID, err := strconv.Atoi(id) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) + return + } + + _, err = handler.RegistryService.Registry(portainer.RegistryID(registryID)) + if err == portainer.ErrRegistryNotFound { + httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + err = handler.RegistryService.DeleteRegistry(portainer.RegistryID(registryID)) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } +} diff --git a/api/http/security/filter.go b/api/http/security/filter.go index ec83a1ebc..7e7f56c7c 100644 --- a/api/http/security/filter.go +++ b/api/http/security/filter.go @@ -60,6 +60,24 @@ func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []po return filteredUsers } +// FilterRegistries filters registries based on user role and team memberships. +// Non administrator users only have access to authorized endpoints. +func FilterRegistries(registries []portainer.Registry, context *RestrictedRequestContext) ([]portainer.Registry, error) { + + filteredRegistries := registries + if !context.IsAdmin { + filteredRegistries = make([]portainer.Registry, 0) + + for _, registry := range registries { + if isRegistryAccessAuthorized(®istry, context.UserID, context.UserMemberships) { + filteredRegistries = append(filteredRegistries, registry) + } + } + } + + return filteredRegistries, nil +} + // FilterEndpoints filters endpoints based on user role and team memberships. // Non administrator users only have access to authorized endpoints. func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestContext) ([]portainer.Endpoint, error) { @@ -78,6 +96,22 @@ func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestC return filteredEndpoints, nil } +func isRegistryAccessAuthorized(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool { + for _, authorizedUserID := range registry.AuthorizedUsers { + if authorizedUserID == userID { + return true + } + } + for _, membership := range memberships { + for _, authorizedTeamID := range registry.AuthorizedTeams { + if membership.TeamID == authorizedTeamID { + return true + } + } + } + return false +} + func isEndpointAccessAuthorized(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool { for _, authorizedUserID := range endpoint.AuthorizedUsers { if authorizedUserID == userID { diff --git a/api/http/server.go b/api/http/server.go index c183e81a5..14a069eae 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -25,6 +25,8 @@ type Server struct { CryptoService portainer.CryptoService JWTService portainer.JWTService FileService portainer.FileService + RegistryService portainer.RegistryService + DockerHubService portainer.DockerHubService Handler *handler.Handler SSL bool SSLCert string @@ -66,6 +68,10 @@ func (server *Server) Start() error { endpointHandler.EndpointService = server.EndpointService endpointHandler.FileService = server.FileService endpointHandler.ProxyManager = proxyManager + var registryHandler = handler.NewRegistryHandler(requestBouncer) + registryHandler.RegistryService = server.RegistryService + var dockerHubHandler = handler.NewDockerHubHandler(requestBouncer) + dockerHubHandler.DockerHubService = server.DockerHubService var resourceHandler = handler.NewResourceHandler(requestBouncer) resourceHandler.ResourceControlService = server.ResourceControlService var uploadHandler = handler.NewUploadHandler(requestBouncer) @@ -78,6 +84,8 @@ func (server *Server) Start() error { TeamHandler: teamHandler, TeamMembershipHandler: teamMembershipHandler, EndpointHandler: endpointHandler, + RegistryHandler: registryHandler, + DockerHubHandler: dockerHubHandler, ResourceHandler: resourceHandler, SettingsHandler: settingsHandler, StatusHandler: statusHandler, diff --git a/api/portainer.go b/api/portainer.go index ec651c3d6..4e3f757d2 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -94,6 +94,30 @@ type ( Role UserRole } + // RegistryID represents a registry identifier. + RegistryID int + + // Registry represents a Docker registry with all the info required + // to connect to it. + Registry struct { + ID RegistryID `json:"Id"` + Name string `json:"Name"` + URL string `json:"URL"` + Authentication bool `json:"Authentication"` + Username string `json:"Username"` + Password string `json:"Password"` + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` + } + + // DockerHub represents all the required information to connect and use the + // Docker Hub. + DockerHub struct { + Authentication bool `json:"Authentication"` + Username string `json:"Username"` + Password string `json:"Password"` + } + // EndpointID represents an endpoint identifier. EndpointID int @@ -217,6 +241,21 @@ type ( Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error } + // RegistryService represents a service for managing registry data. + RegistryService interface { + Registry(ID RegistryID) (*Registry, error) + Registries() ([]Registry, error) + CreateRegistry(registry *Registry) error + UpdateRegistry(ID RegistryID, registry *Registry) error + DeleteRegistry(ID RegistryID) error + } + + // DockerHubService represents a service for managing the DockerHub object. + DockerHubService interface { + DockerHub() (*DockerHub, error) + StoreDockerHub(registry *DockerHub) error + } + // SettingsService represents a service for managing application settings. SettingsService interface { Settings() (*Settings, error) diff --git a/app/app.js b/app/app.js index 446e3dd18..ab86566f4 100644 --- a/app/app.js +++ b/app/app.js @@ -28,6 +28,7 @@ angular.module('portainer', [ 'containers', 'createContainer', 'createNetwork', + 'createRegistry', 'createSecret', 'createService', 'createVolume', @@ -43,6 +44,9 @@ angular.module('portainer', [ 'network', 'networks', 'node', + 'registries', + 'registry', + 'registryAccess', 'secrets', 'secret', 'service', @@ -253,6 +257,19 @@ angular.module('portainer', [ } } }) + .state('actions.create.registry', { + url: '/registry', + views: { + 'content@': { + templateUrl: 'app/components/createRegistry/createregistry.html', + controller: 'CreateRegistryController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) .state('actions.create.secret', { url: '/secret', views: { @@ -431,6 +448,45 @@ angular.module('portainer', [ } } }) + .state('registries', { + url: '/registries/', + views: { + 'content@': { + templateUrl: 'app/components/registries/registries.html', + controller: 'RegistriesController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('registry', { + url: '^/registries/:id', + views: { + 'content@': { + templateUrl: 'app/components/registry/registry.html', + controller: 'RegistryController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) + .state('registry.access', { + url: '^/registries/:id/access', + views: { + 'content@': { + templateUrl: 'app/components/registryAccess/registryAccess.html', + controller: 'RegistryAccessController' + }, + 'sidebar@': { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + } + }) .state('secrets', { url: '^/secrets/', views: { @@ -687,6 +743,8 @@ angular.module('portainer', [ .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('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json') .constant('PAGINATION_MAX_ITEMS', 10); diff --git a/app/components/common/accessControlForm/accessControlForm.html b/app/components/common/accessControlForm/accessControlForm.html index 70961858e..21e4c3869 100644 --- a/app/components/common/accessControlForm/accessControlForm.html +++ b/app/components/common/accessControlForm/accessControlForm.html @@ -17,11 +17,11 @@
    -
    +
    +
    Administrators
    @@ -31,7 +31,7 @@
    +
    Restricted
    @@ -43,7 +43,7 @@
    +
    Private
    @@ -55,7 +55,7 @@
    +
    Restricted
    diff --git a/app/components/common/accessControlPanel/accessControlPanel.html b/app/components/common/accessControlPanel/accessControlPanel.html index c4339d38b..eedd4ae66 100644 --- a/app/components/common/accessControlPanel/accessControlPanel.html +++ b/app/components/common/accessControlPanel/accessControlPanel.html @@ -63,11 +63,11 @@ -
    +
    +
    Administrators
    @@ -77,7 +77,7 @@
    +
    Restricted
    @@ -89,7 +89,7 @@
    +
    Restricted
    @@ -104,7 +104,7 @@
    - +
    - -
    - -
    - -
    - -
    +
    - +
    diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index b3cf31d85..da858ade5 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -1,6 +1,6 @@ angular.module('containers', []) - .controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'Info', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider', - function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, Info, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) { + .controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'SystemService', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider', + function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, SystemService, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('containers'); $scope.state.displayAll = true; @@ -202,15 +202,18 @@ angular.module('containers', []) return swarm_hosts; } - function initView(){ - if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') { - Info.get({}, function (d) { + function initView() { + var provider = $scope.applicationState.endpoint.mode.provider; + $q.when(provider !== 'DOCKER_SWARM' || SystemService.info()) + .then(function success(data) { + if (provider === 'DOCKER_SWARM') { $scope.swarm_hosts = retrieveSwarmHostsInfo(d); - update({all: $scope.state.displayAll ? 1 : 0}); - }); - } else { + } update({all: $scope.state.displayAll ? 1 : 0}); - } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve cluster information'); + }); } initView(); diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js index e19d03063..77cefd4ac 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', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator', -function ($q, $scope, $state, $stateParams, $filter, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) { +.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator', +function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) { $scope.formValues = { alwaysPull: true, @@ -94,7 +94,7 @@ function ($q, $scope, $state, $stateParams, $filter, Info, Container, ContainerH function prepareImageConfig(config) { var image = config.Image; var registry = $scope.formValues.Registry; - var imageConfig = ImageHelper.createImageConfigForContainer(image, registry); + var imageConfig = ImageHelper.createImageConfigForContainer(image, registry.URL); config.Image = imageConfig.fromImage + ':' + imageConfig.tag; $scope.imageConfig = imageConfig; } diff --git a/app/components/createContainer/createcontainer.html b/app/components/createContainer/createcontainer.html index e931126c9..1798c0230 100644 --- a/app/components/createContainer/createcontainer.html +++ b/app/components/createContainer/createcontainer.html @@ -21,21 +21,11 @@
    Image configuration
    - +
    - -
    - -
    - -
    - -
    +
    - +
    diff --git a/app/components/createRegistry/createRegistryController.js b/app/components/createRegistry/createRegistryController.js new file mode 100644 index 000000000..d4017f98c --- /dev/null +++ b/app/components/createRegistry/createRegistryController.js @@ -0,0 +1,49 @@ +angular.module('createRegistry', []) +.controller('CreateRegistryController', ['$scope', '$state', 'RegistryService', 'Notifications', +function ($scope, $state, RegistryService, Notifications) { + + $scope.state = { + RegistryType: 'quay' + }; + + $scope.formValues = { + Name: 'Quay', + URL: 'quay.io', + Authentication: true, + Username: '', + Password: '' + }; + + $scope.selectQuayRegistry = function() { + $scope.formValues.Name = 'Quay'; + $scope.formValues.URL = 'quay.io'; + $scope.formValues.Authentication = true; + }; + + $scope.selectCustomRegistry = function() { + $scope.formValues.Name = ''; + $scope.formValues.URL = ''; + $scope.formValues.Authentication = false; + }; + + $scope.addRegistry = function() { + $('#createRegistrySpinner').show(); + var registryName = $scope.formValues.Name; + var registryURL = $scope.formValues.URL.replace(/^https?\:\/\//i, ''); + var authentication = $scope.formValues.Authentication; + var username = $scope.formValues.Username; + var password = $scope.formValues.Password; + + RegistryService.createRegistry(registryName, registryURL, authentication, username, password) + .then(function success(data) { + Notifications.success('Registry successfully created'); + $state.go('registries'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create registry'); + }) + .finally(function final() { + $('#createRegistrySpinner').hide(); + }); + }; +}]); diff --git a/app/components/createRegistry/createregistry.html b/app/components/createRegistry/createregistry.html new file mode 100644 index 000000000..def77d867 --- /dev/null +++ b/app/components/createRegistry/createregistry.html @@ -0,0 +1,117 @@ + + + + + + Registries > Add registry + + + +
    +
    + + +
    +
    + Registry provider +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + Important notice +
    +
    + + Docker requires you to connect to a secure registry. + You can find more information about how to connect to an insecure registry in the Docker documentation. + +
    +
    + Registry details +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + +
    + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    diff --git a/app/components/createService/createServiceController.js b/app/components/createService/createServiceController.js index 28812e93f..ca5b9f7dd 100644 --- a/app/components/createService/createServiceController.js +++ b/app/components/createService/createServiceController.js @@ -1,13 +1,13 @@ // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // See app/components/templates/templatesController.js as a reference. angular.module('createService', []) -.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator', -function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator) { +.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator', 'RegistryService', 'HttpRequestHelper', +function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator, RegistryService, HttpRequestHelper) { $scope.formValues = { Name: '', Image: '', - Registry: '', + Registry: {}, Mode: 'replicated', Replicas: 1, Command: '', @@ -105,7 +105,7 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic }; function prepareImageConfig(config, input) { - var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry); + var imageConfig = ImageHelper.createImageConfigForContainer(input.Image, input.Registry.URL); config.TaskTemplate.ContainerSpec.Image = imageConfig.fromImage + ':' + imageConfig.tag; } @@ -257,6 +257,10 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic } function createNewService(config, accessControlData) { + + var registry = $scope.formValues.Registry; + var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : ''; + HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); Service.create(config).$promise .then(function success(data) { var serviceIdentifier = data.ID; diff --git a/app/components/createService/createservice.html b/app/components/createService/createservice.html index e6ecc79b2..358f8a7c2 100644 --- a/app/components/createService/createservice.html +++ b/app/components/createService/createservice.html @@ -23,21 +23,11 @@
    Image configuration
    - +
    - -
    - -
    - -
    - -
    +
    - +
    Scheduling
    diff --git a/app/components/createVolume/createVolumeController.js b/app/components/createVolume/createVolumeController.js index a4a9037a2..a7b5f3d85 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', 'InfoService', 'ResourceControlService', 'Authentication', 'Notifications', 'ControllerDataPipeline', 'FormValidator', -function ($scope, $state, VolumeService, InfoService, ResourceControlService, Authentication, Notifications, ControllerDataPipeline, FormValidator) { +.controller('CreateVolumeController', ['$scope', '$state', 'VolumeService', 'SystemService', 'ResourceControlService', 'Authentication', 'Notifications', 'ControllerDataPipeline', 'FormValidator', +function ($scope, $state, VolumeService, SystemService, ResourceControlService, Authentication, Notifications, ControllerDataPipeline, FormValidator) { $scope.formValues = { Driver: 'local', @@ -69,7 +69,7 @@ function ($scope, $state, VolumeService, InfoService, ResourceControlService, Au function initView() { $('#loadingViewSpinner').show(); - InfoService.getVolumePlugins() + SystemService.getVolumePlugins() .then(function success(data) { $scope.availableVolumeDrivers = data; }) diff --git a/app/components/dashboard/dashboardController.js b/app/components/dashboard/dashboardController.js index 9b01ceb56..c9598209c 100644 --- a/app/components/dashboard/dashboardController.js +++ b/app/components/dashboard/dashboardController.js @@ -1,6 +1,6 @@ angular.module('dashboard', []) -.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications', -function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) { +.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'SystemService', 'Notifications', +function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, SystemService, Notifications) { $scope.containerData = { total: 0 @@ -68,7 +68,7 @@ function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, Info, Image.query({}).$promise, Volume.query({}).$promise, Network.query({}).$promise, - Info.get({}).$promise + SystemService.info() ]).then(function (d) { prepareContainerData(d[0]); prepareImageData(d[1]); diff --git a/app/components/docker/docker.html b/app/components/docker/docker.html index ac5f797a8..67aa6db5b 100644 --- a/app/components/docker/docker.html +++ b/app/components/docker/docker.html @@ -8,7 +8,7 @@ Docker -
    +
    @@ -50,7 +50,7 @@
    -
    +
    @@ -92,7 +92,7 @@
    -
    +
    diff --git a/app/components/docker/dockerController.js b/app/components/docker/dockerController.js index 56b80455c..4e3a160dc 100644 --- a/app/components/docker/dockerController.js +++ b/app/components/docker/dockerController.js @@ -1,24 +1,26 @@ angular.module('docker', []) -.controller('DockerController', ['$scope', 'Info', 'Version', 'Notifications', -function ($scope, Info, Version, Notifications) { - $scope.state = { - loaded: false - }; +.controller('DockerController', ['$q', '$scope', 'SystemService', 'Notifications', +function ($q, $scope, SystemService, Notifications) { $scope.info = {}; $scope.version = {}; - Info.get({}, function (infoData) { - $scope.info = infoData; - Version.get({}, function (versionData) { - $scope.version = versionData; - $scope.state.loaded = true; - $('#loadingViewSpinner').hide(); - }, function (e) { - Notifications.error('Failure', e, 'Unable to retrieve engine details'); + function initView() { + $('#loadingViewSpinner').show(); + $q.all({ + version: SystemService.version(), + info: SystemService.info() + }) + .then(function success(data) { + $scope.version = data.version; + $scope.info = data.info; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve engine details'); + }) + .finally(function final() { $('#loadingViewSpinner').hide(); }); - }, function (e) { - Notifications.error('Failure', e, 'Unable to retrieve engine information'); - $('#loadingViewSpinner').hide(); - }); + } + + initView(); }]); diff --git a/app/components/endpointAccess/endpointAccess.html b/app/components/endpointAccess/endpointAccess.html index 98ee856dc..e4b14312c 100644 --- a/app/components/endpointAccess/endpointAccess.html +++ b/app/components/endpointAccess/endpointAccess.html @@ -41,137 +41,5 @@
    -
    -
    - - -
    - Items per page: - -
    -
    - -
    - -
    -
    - -
    -
    - -
    - - - - - - - - - - - - - - - - - - - -
    - - Name - - - - - - Type - - - -
    {{ user.Name }} - - {{ user.Type }} -
    Loading...
    No user or team available.
    -
    - -
    -
    -
    -
    -
    -
    - - -
    - Items per page: - -
    -
    - -
    - -
    -
    - -
    -
    - -
    - - - - - - - - - - - - - - - - - - - -
    - - Name - - - - - - Type - - - -
    {{ user.Name }} - - {{ user.Type }} -
    Loading...
    No authorized user or team.
    -
    - -
    -
    -
    -
    -
    -
    + + diff --git a/app/components/endpointAccess/endpointAccessController.js b/app/components/endpointAccess/endpointAccessController.js index 549664be6..413fded43 100644 --- a/app/components/endpointAccess/endpointAccessController.js +++ b/app/components/endpointAccess/endpointAccessController.js @@ -1,177 +1,18 @@ angular.module('endpointAccess', []) -.controller('EndpointAccessController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'UserService', 'TeamService', 'Pagination', 'Notifications', -function ($q, $scope, $state, $stateParams, $filter, EndpointService, UserService, TeamService, Pagination, Notifications) { +.controller('EndpointAccessController', ['$scope', '$stateParams', 'EndpointService', 'Notifications', +function ($scope, $stateParams, EndpointService, Notifications) { - $scope.state = { - pagination_count_accesses: Pagination.getPaginationCount('endpoint_access_accesses'), - pagination_count_authorizedAccesses: Pagination.getPaginationCount('endpoint_access_authorizedAccesses') - }; - - $scope.sortTypeAccesses = 'Type'; - $scope.sortReverseAccesses = false; - - $scope.orderAccesses = function(sortType) { - $scope.sortReverseAccesses = ($scope.sortTypeAccesses === sortType) ? !$scope.sortReverseAccesses : false; - $scope.sortTypeAccesses = sortType; - }; - - $scope.changePaginationCountAccesses = function() { - Pagination.setPaginationCount('endpoint_access_accesses', $scope.state.pagination_count_accesses); - }; - - $scope.sortTypeAuthorizedAccesses = 'Type'; - $scope.sortReverseAuthorizedAccesses = false; - - $scope.orderAuthorizedAccesses = function(sortType) { - $scope.sortReverseAuthorizedAccesses = ($scope.sortTypeAuthorizedAccesses === sortType) ? !$scope.sortReverseAuthorizedAccesses : false; - $scope.sortTypeAuthorizedAccesses = sortType; - }; - - $scope.changePaginationCountAuthorizedAccesses = function() { - Pagination.setPaginationCount('endpoint_access_authorizedAccesses', $scope.state.pagination_count_authorizedAccesses); - }; - - $scope.authorizeAllAccesses = function() { - var authorizedUsers = []; - var authorizedTeams = []; - angular.forEach($scope.authorizedAccesses, function (a) { - if (a.Type === 'user') { - authorizedUsers.push(a.Id); - } else if (a.Type === 'team') { - authorizedTeams.push(a.Id); - } - }); - angular.forEach($scope.accesses, function (a) { - if (a.Type === 'user') { - authorizedUsers.push(a.Id); - } else if (a.Type === 'team') { - authorizedTeams.push(a.Id); - } - }); - - EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams) - .then(function success(data) { - $scope.authorizedAccesses = $scope.authorizedAccesses.concat($scope.accesses); - $scope.accesses = []; - Notifications.success('Endpoint accesses successfully updated'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update endpoint accesses'); - }); - }; - - $scope.unauthorizeAllAccesses = function() { - EndpointService.updateAccess($stateParams.id, [], []) - .then(function success(data) { - $scope.accesses = $scope.accesses.concat($scope.authorizedAccesses); - $scope.authorizedAccesses = []; - Notifications.success('Endpoint accesses successfully updated'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update endpoint accesses'); - }); - }; - - $scope.authorizeAccess = function(access) { - var authorizedUsers = []; - var authorizedTeams = []; - angular.forEach($scope.authorizedAccesses, function (a) { - if (a.Type === 'user') { - authorizedUsers.push(a.Id); - } else if (a.Type === 'team') { - authorizedTeams.push(a.Id); - } - }); - - if (access.Type === 'user') { - authorizedUsers.push(access.Id); - } else if (access.Type === 'team') { - authorizedTeams.push(access.Id); - } - - EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams) - .then(function success(data) { - removeAccessFromArray(access, $scope.accesses); - $scope.authorizedAccesses.push(access); - Notifications.success('Endpoint accesses successfully updated', access.Name); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update endpoint accesses'); - }); - }; - - $scope.unauthorizeAccess = function(access) { - var authorizedUsers = []; - var authorizedTeams = []; - angular.forEach($scope.authorizedAccesses, function (a) { - if (a.Type === 'user') { - authorizedUsers.push(a.Id); - } else if (a.Type === 'team') { - authorizedTeams.push(a.Id); - } - }); - - if (access.Type === 'user') { - _.remove(authorizedUsers, function(n) { - return n === access.Id; - }); - } else if (access.Type === 'team') { - _.remove(authorizedTeams, function(n) { - return n === access.Id; - }); - } - - EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams) - .then(function success(data) { - removeAccessFromArray(access, $scope.authorizedAccesses); - $scope.accesses.push(access); - Notifications.success('Endpoint accesses successfully updated', access.Name); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update endpoint accesses'); - }); + $scope.updateAccess = function(authorizedUsers, authorizedTeams) { + return EndpointService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams); }; function initView() { $('#loadingViewSpinner').show(); - $q.all({ - endpoint: EndpointService.endpoint($stateParams.id), - users: UserService.users(false), - teams: TeamService.teams() - }) + EndpointService.endpoint($stateParams.id) .then(function success(data) { - $scope.endpoint = data.endpoint; - $scope.accesses = []; - var users = data.users.map(function (user) { - return new EndpointAccessUserViewModel(user); - }); - var teams = data.teams.map(function (team) { - return new EndpointAccessTeamViewModel(team); - }); - $scope.accesses = $scope.accesses.concat(users, teams); - $scope.authorizedAccesses = []; - angular.forEach($scope.endpoint.AuthorizedUsers, function(userID) { - for (var i = 0, l = $scope.accesses.length; i < l; i++) { - if ($scope.accesses[i].Type === 'user' && $scope.accesses[i].Id === userID) { - $scope.authorizedAccesses.push($scope.accesses[i]); - $scope.accesses.splice(i, 1); - return; - } - } - }); - angular.forEach($scope.endpoint.AuthorizedTeams, function(teamID) { - for (var i = 0, l = $scope.accesses.length; i < l; i++) { - if ($scope.accesses[i].Type === 'team' && $scope.accesses[i].Id === teamID) { - $scope.authorizedAccesses.push($scope.accesses[i]); - $scope.accesses.splice(i, 1); - return; - } - } - }); + $scope.endpoint = data; }) .catch(function error(err) { - $scope.accesses = []; - $scope.authorizedAccesses = []; Notifications.error('Failure', err, 'Unable to retrieve endpoint details'); }) .finally(function final(){ @@ -179,14 +20,5 @@ function ($q, $scope, $state, $stateParams, $filter, EndpointService, UserServic }); } - function removeAccessFromArray(access, accesses) { - for (var i = 0, l = accesses.length; i < l; i++) { - if (access.Type === accesses[i].Type && access.Id === accesses[i].Id) { - accesses.splice(i, 1); - return; - } - } - } - initView(); }]); diff --git a/app/components/events/eventsController.js b/app/components/events/eventsController.js index c0027376f..97bb1b91a 100644 --- a/app/components/events/eventsController.js +++ b/app/components/events/eventsController.js @@ -1,6 +1,6 @@ angular.module('events', []) -.controller('EventsController', ['$scope', 'Notifications', 'Events', 'Pagination', -function ($scope, Notifications, Events, Pagination) { +.controller('EventsController', ['$scope', 'Notifications', 'SystemService', 'Pagination', +function ($scope, Notifications, SystemService, Pagination) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('events'); $scope.sortType = 'Time'; @@ -15,18 +15,22 @@ function ($scope, Notifications, Events, Pagination) { Pagination.setPaginationCount('events', $scope.state.pagination_count); }; - var from = moment().subtract(24, 'hour').unix(); - var to = moment().unix(); + function initView() { + var from = moment().subtract(24, 'hour').unix(); + var to = moment().unix(); - Events.query({since: from, until: to}, - function(d) { - $scope.events = d.map(function (item) { - return new EventViewModel(item); + $('#loadEventsSpinner').show(); + SystemService.events(from, to) + .then(function success(data) { + $scope.events = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to load events'); + }) + .finally(function final() { + $('#loadEventsSpinner').hide(); }); - $('#loadEventsSpinner').hide(); - }, - function (e) { - $('#loadEventsSpinner').hide(); - Notifications.error('Failure', e, 'Unable to load events'); - }); + } + + initView(); }]); diff --git a/app/components/image/image.html b/app/components/image/image.html index 4f10a93e6..799dd2386 100644 --- a/app/components/image/image.html +++ b/app/components/image/image.html @@ -54,21 +54,11 @@
    - +
    - -
    - -
    - -
    - -
    +
    - +
    @@ -78,7 +68,7 @@
    - +
    diff --git a/app/components/image/imageController.js b/app/components/image/imageController.js index 907b664c3..5b8825bf4 100644 --- a/app/components/image/imageController.js +++ b/app/components/image/imageController.js @@ -1,17 +1,17 @@ angular.module('image', []) -.controller('ImageController', ['$scope', '$stateParams', '$state', 'ImageService', 'Notifications', -function ($scope, $stateParams, $state, ImageService, Notifications) { - $scope.config = { +.controller('ImageController', ['$scope', '$stateParams', '$state', '$timeout', 'ImageService', 'RegistryService', 'Notifications', +function ($scope, $stateParams, $state, $timeout, ImageService, RegistryService, Notifications) { + $scope.formValues = { Image: '', Registry: '' }; $scope.tagImage = function() { $('#loadingViewSpinner').show(); - var image = $scope.config.Image; - var registry = $scope.config.Registry; + var image = $scope.formValues.Image; + var registry = $scope.formValues.Registry; - ImageService.tagImage($stateParams.id, image, registry) + ImageService.tagImage($stateParams.id, image, registry.URL) .then(function success(data) { Notifications.success('Image successfully tagged'); $state.go('image', {id: $stateParams.id}, {reload: true}); @@ -24,28 +24,35 @@ function ($scope, $stateParams, $state, ImageService, Notifications) { }); }; - $scope.pushTag = function(tag) { + $scope.pushTag = function(repository) { $('#loadingViewSpinner').show(); - ImageService.pushImage(tag) - .then(function success() { - Notifications.success('Image successfully pushed'); + RegistryService.retrieveRegistryFromRepository(repository) + .then(function success(data) { + var registry = data; + return ImageService.pushImage(repository, registry); + }) + .then(function success(data) { + Notifications.success('Image successfully pushed', repository); }) .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to push image tag'); + Notifications.error('Failure', err, 'Unable to push image to repository'); }) .finally(function final() { $('#loadingViewSpinner').hide(); }); }; - $scope.pullTag = function(tag) { + $scope.pullTag = function(repository) { $('#loadingViewSpinner').show(); - - ImageService.pullTag(tag) + RegistryService.retrieveRegistryFromRepository(repository) .then(function success(data) { - Notifications.success('Image successfully pulled', tag); + var registry = data; + return ImageService.pullImage(repository, registry); }) - .catch(function error(err){ + .then(function success(data) { + Notifications.success('Image successfully pulled', repository); + }) + .catch(function error(err) { Notifications.error('Failure', err, 'Unable to pull image'); }) .finally(function final() { @@ -53,15 +60,15 @@ function ($scope, $stateParams, $state, ImageService, Notifications) { }); }; - $scope.removeTag = function(id) { + $scope.removeTag = function(repository) { $('#loadingViewSpinner').show(); - ImageService.deleteImage(id, false) + ImageService.deleteImage(repository, false) .then(function success() { if ($scope.image.RepoTags.length === 1) { - Notifications.success('Image successfully deleted', id); + Notifications.success('Image successfully deleted', repository); $state.go('images', {}, {reload: true}); } else { - Notifications.success('Tag successfully deleted', id); + Notifications.success('Tag successfully deleted', repository); $state.go('image', {id: $stateParams.id}, {reload: true}); } }) diff --git a/app/components/images/images.html b/app/components/images/images.html index ff574e502..cfecf029a 100644 --- a/app/components/images/images.html +++ b/app/components/images/images.html @@ -15,21 +15,11 @@ - +
    - -
    - -
    - -
    - -
    +
    - +
    @@ -39,7 +29,7 @@
    - +
    diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js index f5ecbc618..6a1c56335 100644 --- a/app/components/images/imagesController.js +++ b/app/components/images/imagesController.js @@ -7,7 +7,7 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService) $scope.sortReverse = true; $scope.state.selectedItemCount = 0; - $scope.config = { + $scope.formValues = { Image: '', Registry: '' }; @@ -40,10 +40,11 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService) $scope.pullImage = function() { $('#pullImageSpinner').show(); - var image = $scope.config.Image; - var registry = $scope.config.Registry; + var image = $scope.formValues.Image; + var registry = $scope.formValues.Registry; ImageService.pullImage(image, registry) .then(function success(data) { + Notifications.success('Image successfully pulled', image); $state.reload(); }) .catch(function error(err) { diff --git a/app/components/registries/registries.html b/app/components/registries/registries.html new file mode 100644 index 000000000..cf5772dcc --- /dev/null +++ b/app/components/registries/registries.html @@ -0,0 +1,150 @@ + + + + + + + + Registry management + + +
    +
    + + + + + + +
    + + The DockerHub registry can be used by any user. You can specify the credentials that will be used to push & pull images here. + +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    + +
    +
    + + +
    + Items per page: + +
    +
    + +
    +
    + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + Name + + + + + + URL + + + +
    + {{ registry.Name }} + + + + {{ registry.URL }} + + Manage access + +
    Loading...
    No registries available.
    +
    + +
    +
    +
    + +
    +
    diff --git a/app/components/registries/registriesController.js b/app/components/registries/registriesController.js new file mode 100644 index 000000000..dd98be5b3 --- /dev/null +++ b/app/components/registries/registriesController.js @@ -0,0 +1,113 @@ +angular.module('registries', []) +.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'Pagination', +function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, Pagination) { + + $scope.state = { + selectedItemCount: 0, + pagination_count: Pagination.getPaginationCount('registries') + }; + $scope.sortType = 'Name'; + $scope.sortReverse = true; + + $scope.updateDockerHub = function() { + $('#updateDockerhubSpinner').show(); + var dockerhub = $scope.dockerhub; + DockerHubService.update(dockerhub) + .then(function success(data) { + Notifications.success('DockerHub registry updated'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update DockerHub details'); + }) + .finally(function final() { + $('#updateDockerhubSpinner').hide(); + }); + }; + + $scope.order = function(sortType) { + $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; + $scope.sortType = sortType; + }; + + $scope.changePaginationCount = function() { + Pagination.setPaginationCount('endpoints', $scope.state.pagination_count); + }; + + $scope.selectItems = function (allSelected) { + angular.forEach($scope.state.filteredRegistries, function (registry) { + if (registry.Checked !== allSelected) { + registry.Checked = allSelected; + $scope.selectItem(registry); + } + }); + }; + + $scope.selectItem = function (item) { + if (item.Checked) { + $scope.state.selectedItemCount++; + } else { + $scope.state.selectedItemCount--; + } + }; + + $scope.removeAction = function() { + ModalService.confirmDeletion( + 'Do you want to remove the selected registries?', + function onConfirm(confirmed) { + if(!confirmed) { return; } + removeRegistries(); + } + ); + }; + + function removeRegistries() { + $('#loadingViewSpinner').show(); + var counter = 0; + var complete = function () { + counter = counter - 1; + if (counter === 0) { + $('#loadingViewSpinner').hide(); + } + }; + + var registries = $scope.registries; + angular.forEach(registries, function (registry) { + if (registry.Checked) { + counter = counter + 1; + RegistryService.deleteRegistry(registry.Id) + .then(function success(data) { + var index = registries.indexOf(registry); + registries.splice(index, 1); + Notifications.success('Registry deleted', registry.Name); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove registry'); + }) + .finally(function final() { + complete(); + }); + } + }); + } + + function initView() { + $('#loadingViewSpinner').show(); + $q.all({ + registries: RegistryService.registries(), + dockerhub: DockerHubService.dockerhub() + }) + .then(function success(data) { + $scope.registries = data.registries; + $scope.dockerhub = data.dockerhub; + }) + .catch(function error(err) { + $scope.registries = []; + Notifications.error('Failure', err, 'Unable to retrieve registries'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); +}]); diff --git a/app/components/registry/registry.html b/app/components/registry/registry.html new file mode 100644 index 000000000..2bbb7c2e6 --- /dev/null +++ b/app/components/registry/registry.html @@ -0,0 +1,78 @@ + + + + + + Registries > {{ registry.Name }} + + + +
    +
    + + +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    +
    + +
    + +
    +
    + + Cancel + +
    +
    +
    +
    +
    +
    +
    diff --git a/app/components/registry/registryController.js b/app/components/registry/registryController.js new file mode 100644 index 000000000..38dbd1b27 --- /dev/null +++ b/app/components/registry/registryController.js @@ -0,0 +1,37 @@ +angular.module('registry', []) +.controller('RegistryController', ['$scope', '$state', '$stateParams', '$filter', 'RegistryService', 'Notifications', +function ($scope, $state, $stateParams, $filter, RegistryService, Notifications) { + + $scope.updateRegistry = function() { + $('#updateRegistrySpinner').show(); + var registry = $scope.registry; + RegistryService.updateRegistry(registry) + .then(function success(data) { + Notifications.success('Registry successfully updated'); + $state.go('registries'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update registry'); + }) + .finally(function final() { + $('#updateRegistrySpinner').hide(); + }); + }; + + function initView() { + $('#loadingViewSpinner').show(); + var registryID = $stateParams.id; + RegistryService.registry(registryID) + .then(function success(data) { + $scope.registry = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve registry details'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); +}]); diff --git a/app/components/registryAccess/registryAccess.html b/app/components/registryAccess/registryAccess.html new file mode 100644 index 000000000..84c3d2395 --- /dev/null +++ b/app/components/registryAccess/registryAccess.html @@ -0,0 +1,45 @@ + + + + + + Registries > {{ registry.Name }} > Access management + + + +
    +
    + + + + + + + + + + + + + + + + + +
    Name + {{ registry.Name }} +
    URL + {{ registry.URL }} +
    + + You can select which user or team can access this registry by moving them to the authorized accesses table. Simply click + on a user or team entry to move it from one table to the other. + +
    +
    +
    +
    +
    + + + diff --git a/app/components/registryAccess/registryAccessController.js b/app/components/registryAccess/registryAccessController.js new file mode 100644 index 000000000..9c6efe719 --- /dev/null +++ b/app/components/registryAccess/registryAccessController.js @@ -0,0 +1,24 @@ +angular.module('registryAccess', []) +.controller('RegistryAccessController', ['$scope', '$stateParams', 'RegistryService', 'Notifications', +function ($scope, $stateParams, RegistryService, Notifications) { + + $scope.updateAccess = function(authorizedUsers, authorizedTeams) { + return RegistryService.updateAccess($stateParams.id, authorizedUsers, authorizedTeams); + }; + + function initView() { + $('#loadingViewSpinner').show(); + RegistryService.registry($stateParams.id) + .then(function success(data) { + $scope.registry = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve registry details'); + }) + .finally(function final(){ + $('#loadingViewSpinner').hide(); + }); + } + + initView(); +}]); diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index c1e0dc577..b4a590722 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -64,6 +64,9 @@ + diff --git a/app/components/swarm/swarm.html b/app/components/swarm/swarm.html index d3aac8e75..f07b3d296 100644 --- a/app/components/swarm/swarm.html +++ b/app/components/swarm/swarm.html @@ -3,6 +3,7 @@ + Swarm diff --git a/app/components/swarm/swarmController.js b/app/components/swarm/swarmController.js index 5d844c92c..99cdbf31c 100644 --- a/app/components/swarm/swarmController.js +++ b/app/components/swarm/swarmController.js @@ -1,6 +1,6 @@ angular.module('swarm', []) -.controller('SwarmController', ['$scope', 'Info', 'Version', 'Node', 'Pagination', -function ($scope, Info, Version, Node, Pagination) { +.controller('SwarmController', ['$q', '$scope', 'SystemService', 'NodeService', 'Pagination', 'Notifications', +function ($q, $scope, SystemService, NodeService, Pagination, Notifications) { $scope.state = {}; $scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes'); $scope.sortType = 'Spec.Role'; @@ -20,30 +20,6 @@ function ($scope, Info, Version, Node, Pagination) { Pagination.setPaginationCount('swarm_nodes', $scope.state.pagination_count); }; - Version.get({}, function (d) { - $scope.docker = d; - }); - - Info.get({}, function (d) { - $scope.info = d; - if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { - Node.query({}, function(d) { - $scope.nodes = d.map(function (node) { - return new NodeViewModel(node); - }); - var CPU = 0, memory = 0; - angular.forEach(d, function(node) { - CPU += node.Description.Resources.NanoCPUs; - memory += node.Description.Resources.MemoryBytes; - }); - $scope.totalCPU = CPU / 1000000000; - $scope.totalMemory = memory; - }); - } else { - extractSwarmInfo(d); - } - }); - function extractSwarmInfo(info) { // Swarm info is available in SystemStatus object var systemStatus = info.SystemStatus; @@ -84,4 +60,43 @@ function ($scope, Info, Version, Node, Pagination) { node.version = info[offset + 8][1]; $scope.swarm.Status.push(node); } + + function processTotalCPUAndMemory(nodes) { + var CPU = 0, memory = 0; + angular.forEach(nodes, function(node) { + CPU += node.CPUs; + memory += node.Memory; + }); + $scope.totalCPU = CPU / 1000000000; + $scope.totalMemory = memory; + } + + function initView() { + $('#loadingViewSpinner').show(); + var provider = $scope.applicationState.endpoint.mode.provider; + $q.all({ + version: SystemService.version(), + info: SystemService.info(), + nodes: provider !== 'DOCKER_SWARM_MODE' || NodeService.nodes() + }) + .then(function success(data) { + $scope.docker = data.version; + $scope.info = data.info; + if (provider === 'DOCKER_SWARM_MODE') { + var nodes = data.nodes; + processTotalCPUAndMemory(nodes); + $scope.nodes = nodes; + } else { + extractSwarmInfo(data.info); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve cluster details'); + }) + .finally(function final() { + $('#loadingViewSpinner').hide(); + }); + } + + initView(); }]); diff --git a/app/directives/accessManagement/por-access-management.js b/app/directives/accessManagement/por-access-management.js new file mode 100644 index 000000000..cbe8515c8 --- /dev/null +++ b/app/directives/accessManagement/por-access-management.js @@ -0,0 +1,8 @@ +angular.module('portainer').component('porAccessManagement', { + templateUrl: 'app/directives/accessManagement/porAccessManagement.html', + controller: 'porAccessManagementController', + bindings: { + accessControlledEntity: '<', + updateAccess: '&' + } +}); diff --git a/app/directives/accessManagement/porAccessManagement.html b/app/directives/accessManagement/porAccessManagement.html new file mode 100644 index 000000000..eca9696a5 --- /dev/null +++ b/app/directives/accessManagement/porAccessManagement.html @@ -0,0 +1,134 @@ +
    +
    + + +
    + Items per page: + +
    +
    + +
    + +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + +
    + + Name + + + + + + Type + + + +
    {{ user.Name }} + + {{ user.Type }} +
    Loading...
    No user or team available.
    +
    + +
    +
    +
    +
    +
    +
    + + +
    + Items per page: + +
    +
    + +
    + +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + +
    + + Name + + + + + + Type + + + +
    {{ user.Name }} + + {{ user.Type }} +
    Loading...
    No authorized user or team.
    +
    + +
    +
    +
    +
    +
    +
    diff --git a/app/directives/accessManagement/porAccessManagementController.js b/app/directives/accessManagement/porAccessManagementController.js new file mode 100644 index 000000000..1ab23879e --- /dev/null +++ b/app/directives/accessManagement/porAccessManagementController.js @@ -0,0 +1,157 @@ +angular.module('portainer') +.controller('porAccessManagementController', ['AccessService', 'Pagination', 'Notifications', +function (AccessService, Pagination, Notifications) { + var ctrl = this; + + ctrl.state = { + pagination_count_accesses: Pagination.getPaginationCount('access_management_accesses'), + pagination_count_authorizedAccesses: Pagination.getPaginationCount('access_management_AuthorizedAccesses'), + sortAccessesBy: 'Type', + sortAccessesReverse: false, + sortAuthorizedAccessesBy: 'Type', + sortAuthorizedAccessesReverse: false + }; + + ctrl.orderAccesses = function(sortBy) { + ctrl.state.sortAccessesReverse = (ctrl.state.sortAccessesBy === sortBy) ? !ctrl.state.sortAccessesReverse : false; + ctrl.state.sortAccessesBy = sortBy; + }; + + ctrl.orderAuthorizedAccesses = function(sortBy) { + ctrl.state.sortAuthorizedAccessesReverse = (ctrl.state.sortAuthorizedAccessesBy === sortBy) ? !ctrl.state.sortAuthorizedAccessesReverse : false; + ctrl.state.sortAuthorizedAccessesBy = sortBy; + }; + + ctrl.changePaginationCountAuthorizedAccesses = function() { + Pagination.setPaginationCount('access_management_AuthorizedAccesses', ctrl.state.pagination_count_authorizedAccesses); + }; + + ctrl.changePaginationCountAccesses = function() { + Pagination.setPaginationCount('access_management_accesses', ctrl.state.pagination_count_accesses); + }; + + function dispatchUserAndTeamIDs(accesses, users, teams) { + angular.forEach(accesses, function (access) { + if (access.Type === 'user') { + users.push(access.Id); + } else if (access.Type === 'team') { + teams.push(access.Id); + } + }); + } + + function processAuthorizedIDs(accesses, authorizedAccesses) { + var authorizedUserIDs = []; + var authorizedTeamIDs = []; + if (accesses) { + dispatchUserAndTeamIDs(accesses, authorizedUserIDs, authorizedTeamIDs); + } + if (authorizedAccesses) { + dispatchUserAndTeamIDs(authorizedAccesses, authorizedUserIDs, authorizedTeamIDs); + } + return { + userIDs: authorizedUserIDs, + teamIDs: authorizedTeamIDs + }; + } + + function removeFromAccesses(access, accesses) { + _.remove(accesses, function(n) { + return n.Id === access.Id; + }); + } + + function removeFromAccessIDs(accessId, accessIDs) { + _.remove(accessIDs, function(n) { + return n === accessId; + }); + } + + ctrl.authorizeAccess = function(access) { + var accessData = processAuthorizedIDs(null, ctrl.authorizedAccesses); + var authorizedUserIDs = accessData.userIDs; + var authorizedTeamIDs = accessData.teamIDs; + + if (access.Type === 'user') { + authorizedUserIDs.push(access.Id); + } else if (access.Type === 'team') { + authorizedTeamIDs.push(access.Id); + } + + ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs }) + .then(function success(data) { + removeFromAccesses(access, ctrl.accesses); + ctrl.authorizedAccesses.push(access); + Notifications.success('Accesses successfully updated'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update accesses'); + }); + }; + + ctrl.unauthorizeAccess = function(access) { + var accessData = processAuthorizedIDs(null, ctrl.authorizedAccesses); + var authorizedUserIDs = accessData.userIDs; + var authorizedTeamIDs = accessData.teamIDs; + + if (access.Type === 'user') { + removeFromAccessIDs(access.Id, authorizedUserIDs); + } else if (access.Type === 'team') { + removeFromAccessIDs(access.Id, authorizedTeamIDs); + } + + ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs }) + .then(function success(data) { + removeFromAccesses(access, ctrl.authorizedAccesses); + ctrl.accesses.push(access); + Notifications.success('Accesses successfully updated'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update accesses'); + }); + }; + + ctrl.unauthorizeAllAccesses = function() { + ctrl.updateAccess({ userAccesses: [], teamAccesses: [] }) + .then(function success(data) { + ctrl.accesses = ctrl.accesses.concat(ctrl.authorizedAccesses); + ctrl.authorizedAccesses = []; + Notifications.success('Accesses successfully updated'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update accesses'); + }); + }; + + ctrl.authorizeAllAccesses = function() { + var accessData = processAuthorizedIDs(ctrl.accesses, ctrl.authorizedAccesses); + var authorizedUserIDs = accessData.userIDs; + var authorizedTeamIDs = accessData.teamIDs; + + ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs }) + .then(function success(data) { + ctrl.authorizedAccesses = ctrl.authorizedAccesses.concat(ctrl.accesses); + ctrl.accesses = []; + Notifications.success('Accesses successfully updated'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update accesses'); + }); + }; + + function initComponent() { + var entity = ctrl.accessControlledEntity; + AccessService.accesses(entity.AuthorizedUsers, entity.AuthorizedTeams) + .then(function success(data) { + ctrl.accesses = data.accesses; + ctrl.authorizedAccesses = data.authorizedAccesses; + }) + .catch(function error(err) { + ctrl.accesses = []; + ctrl.authorizedAccesses = []; + Notifications.error('Failure', err, 'Unable to retrieve accesses'); + }); + } + + initComponent(); +}]); diff --git a/app/directives/imageRegistry/por-image-registry.js b/app/directives/imageRegistry/por-image-registry.js new file mode 100644 index 000000000..f8d004967 --- /dev/null +++ b/app/directives/imageRegistry/por-image-registry.js @@ -0,0 +1,8 @@ +angular.module('portainer').component('porImageRegistry', { + templateUrl: 'app/directives/imageRegistry/porImageRegistry.html', + controller: 'porImageRegistryController', + bindings: { + 'image': '=', + 'registry': '=' + } +}); diff --git a/app/directives/imageRegistry/porImageRegistry.html b/app/directives/imageRegistry/porImageRegistry.html new file mode 100644 index 000000000..3f1dbcd43 --- /dev/null +++ b/app/directives/imageRegistry/porImageRegistry.html @@ -0,0 +1,12 @@ +
    + +
    + +
    + +
    + +
    +
    diff --git a/app/directives/imageRegistry/porImageRegistryController.js b/app/directives/imageRegistry/porImageRegistryController.js new file mode 100644 index 000000000..3eeb3d0bb --- /dev/null +++ b/app/directives/imageRegistry/porImageRegistryController.js @@ -0,0 +1,23 @@ +angular.module('portainer') +.controller('porImageRegistryController', ['$q', 'RegistryService', 'DockerHubService', 'Notifications', +function ($q, RegistryService, DockerHubService, Notifications) { + var ctrl = this; + + function initComponent() { + $q.all({ + registries: RegistryService.registries(), + dockerhub: DockerHubService.dockerhub() + }) + .then(function success(data) { + var dockerhub = data.dockerhub; + var registries = data.registries; + ctrl.availableRegistries = [dockerhub].concat(registries); + ctrl.registry = dockerhub; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve registries'); + }); + } + + initComponent(); +}]); diff --git a/app/helpers/imageHelper.js b/app/helpers/imageHelper.js index 03cb21efa..7c3012fbc 100644 --- a/app/helpers/imageHelper.js +++ b/app/helpers/imageHelper.js @@ -4,15 +4,14 @@ angular.module('portainer.helpers') var helper = {}; - helper.extractImageAndRegistryFromTag = function(tag) { - var slashCount = _.countBy(tag)['/']; + helper.extractImageAndRegistryFromRepository = function(repository) { + var slashCount = _.countBy(repository)['/']; var registry = null; - var image = tag; + var image = repository; if (slashCount > 1) { // assume something/some/thing[/...] - var registryAndImage = _.split(tag, '/'); - registry = registryAndImage[0]; - image = registryAndImage[1]; + registry = repository.substr(0, repository.indexOf('/')); + image = repository.substr(repository.indexOf('/') + 1); } return { diff --git a/app/helpers/registryHelper.js b/app/helpers/registryHelper.js new file mode 100644 index 000000000..90a582b2a --- /dev/null +++ b/app/helpers/registryHelper.js @@ -0,0 +1,18 @@ +angular.module('portainer.helpers') +.factory('RegistryHelper', [function RegistryHelperFactory() { + 'use strict'; + + var helper = {}; + + helper.getRegistryByURL = function(registries, url) { + for (var i = 0; i < registries.length; i++) { + if (registries[i].URL === url) { + return registries[i]; + } + } + + return null; + }; + + return helper; +}]); diff --git a/app/models/api/endpointAccess.js b/app/models/api/access.js similarity index 61% rename from app/models/api/endpointAccess.js rename to app/models/api/access.js index d592522f4..b66a93240 100644 --- a/app/models/api/endpointAccess.js +++ b/app/models/api/access.js @@ -1,10 +1,10 @@ -function EndpointAccessUserViewModel(data) { +function UserAccessViewModel(data) { this.Id = data.Id; this.Name = data.Username; this.Type = 'user'; } -function EndpointAccessTeamViewModel(data) { +function TeamAccessViewModel(data) { this.Id = data.Id; this.Name = data.Name; this.Type = 'team'; diff --git a/app/models/api/dockerhub.js b/app/models/api/dockerhub.js new file mode 100644 index 000000000..ee97fb579 --- /dev/null +++ b/app/models/api/dockerhub.js @@ -0,0 +1,7 @@ +function DockerHubViewModel(data) { + this.Name = 'DockerHub'; + this.URL = ''; + this.Authentication = data.Authentication; + this.Username = data.Username; + this.Password = data.Password; +} diff --git a/app/models/api/registry.js b/app/models/api/registry.js new file mode 100644 index 000000000..44c3051ac --- /dev/null +++ b/app/models/api/registry.js @@ -0,0 +1,11 @@ +function RegistryViewModel(data) { + this.Id = data.Id; + this.Name = data.Name; + this.URL = data.URL; + this.Authentication = data.Authentication; + this.Username = data.Username; + this.Password = data.Password; + this.AuthorizedUsers = data.AuthorizedUsers; + this.AuthorizedTeams = data.AuthorizedTeams; + this.Checked = false; +} diff --git a/app/rest/api/dockerhub.js b/app/rest/api/dockerhub.js new file mode 100644 index 000000000..3a07d4aa2 --- /dev/null +++ b/app/rest/api/dockerhub.js @@ -0,0 +1,8 @@ +angular.module('portainer.rest') +.factory('DockerHub', ['$resource', 'DOCKERHUB_ENDPOINT', function DockerHubFactory($resource, DOCKERHUB_ENDPOINT) { + 'use strict'; + return $resource(DOCKERHUB_ENDPOINT, {}, { + get: { method: 'GET' }, + update: { method: 'PUT' } + }); +}]); diff --git a/app/rest/api/registry.js b/app/rest/api/registry.js new file mode 100644 index 000000000..9ba68ee46 --- /dev/null +++ b/app/rest/api/registry.js @@ -0,0 +1,12 @@ +angular.module('portainer.rest') +.factory('Registries', ['$resource', 'REGISTRIES_ENDPOINT', function RegistriesFactory($resource, REGISTRIES_ENDPOINT) { + 'use strict'; + return $resource(REGISTRIES_ENDPOINT + '/:id/:action', {}, { + create: { method: 'POST' }, + query: { method: 'GET', isArray: true }, + get: { method: 'GET', params: { id: '@id' } }, + update: { method: 'PUT', params: { id: '@id' } }, + updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } }, + remove: { method: 'DELETE', params: { id: '@id'} } + }); +}]); diff --git a/app/rest/docker/event.js b/app/rest/docker/event.js deleted file mode 100644 index 468b8e0f4..000000000 --- a/app/rest/docker/event.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('portainer.rest') -.factory('Events', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function EventFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { - 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/events', { - endpointId: EndpointProvider.endpointID - }, - { - query: { - method: 'GET', params: {since: '@since', until: '@until'}, - isArray: true, transformResponse: jsonObjectsToArrayHandler - } - }); -}]); diff --git a/app/rest/docker/image.js b/app/rest/docker/image.js index e9f58d190..c9ddd14bf 100644 --- a/app/rest/docker/image.js +++ b/app/rest/docker/image.js @@ -1,6 +1,7 @@ angular.module('portainer.rest') -.factory('Image', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ImageFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Image', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', 'HttpRequestHelper', function ImageFactory($resource, DOCKER_ENDPOINT, EndpointProvider, HttpRequestHelper) { 'use strict'; + return $resource(DOCKER_ENDPOINT + '/:endpointId/images/:id/:action', { endpointId: EndpointProvider.endpointID }, @@ -14,11 +15,13 @@ angular.module('portainer.rest') inspect: {method: 'GET', params: {id: '@id', action: 'json'}}, push: { method: 'POST', params: {action: 'push', id: '@tag'}, - isArray: true, transformResponse: jsonObjectsToArrayHandler + isArray: true, transformResponse: jsonObjectsToArrayHandler, + headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader } }, create: { method: 'POST', params: {action: 'create', fromImage: '@fromImage', tag: '@tag'}, - isArray: true, transformResponse: jsonObjectsToArrayHandler + isArray: true, transformResponse: jsonObjectsToArrayHandler, + headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader } }, remove: { method: 'DELETE', params: {id: '@id', force: '@force'}, diff --git a/app/rest/docker/info.js b/app/rest/docker/info.js deleted file mode 100644 index 9f9b0d2f2..000000000 --- a/app/rest/docker/info.js +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('portainer.rest') -.factory('Info', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function InfoFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { - 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/info', { - endpointId: EndpointProvider.endpointID - }); -}]); diff --git a/app/rest/docker/service.js b/app/rest/docker/service.js index cd0e377b4..721b55a9c 100644 --- a/app/rest/docker/service.js +++ b/app/rest/docker/service.js @@ -1,5 +1,5 @@ angular.module('portainer.rest') -.factory('Service', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ServiceFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { +.factory('Service', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', 'HttpRequestHelper' ,function ServiceFactory($resource, DOCKER_ENDPOINT, EndpointProvider, HttpRequestHelper) { 'use strict'; return $resource(DOCKER_ENDPOINT + '/:endpointId/services/:id/:action', { endpointId: EndpointProvider.endpointID @@ -7,7 +7,10 @@ angular.module('portainer.rest') { get: { method: 'GET', params: {id: '@id'} }, query: { method: 'GET', isArray: true }, - create: { method: 'POST', params: {action: 'create'} }, + create: { + method: 'POST', params: {action: 'create'}, + headers: { 'X-Registry-Auth': HttpRequestHelper.registryAuthenticationHeader } + }, update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} }, remove: { method: 'DELETE', params: {id: '@id'} } }); diff --git a/app/rest/docker/system.js b/app/rest/docker/system.js new file mode 100644 index 000000000..ccc3e61e2 --- /dev/null +++ b/app/rest/docker/system.js @@ -0,0 +1,17 @@ +angular.module('portainer.rest') +.factory('System', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function SystemFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { + 'use strict'; + return $resource(DOCKER_ENDPOINT + '/:endpointId/:action', { + name: '@name', + endpointId: EndpointProvider.endpointID + }, + { + info: { method: 'GET', params: { action: 'info' } }, + version: { method: 'GET', params: { action: 'version' } }, + events: { + method: 'GET', params: { action: 'events', since: '@since', until: '@until' }, + isArray: true, transformResponse: jsonObjectsToArrayHandler + }, + auth: { method: 'POST', params: { action: 'auth' } } + }); +}]); diff --git a/app/rest/docker/version.js b/app/rest/docker/version.js deleted file mode 100644 index 01e904ec4..000000000 --- a/app/rest/docker/version.js +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('portainer.rest') -.factory('Version', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function VersionFactory($resource, DOCKER_ENDPOINT, EndpointProvider) { - 'use strict'; - return $resource(DOCKER_ENDPOINT + '/:endpointId/version', { - endpointId: EndpointProvider.endpointID - }); -}]); diff --git a/app/services/api/accessService.js b/app/services/api/accessService.js new file mode 100644 index 000000000..2182a8b21 --- /dev/null +++ b/app/services/api/accessService.js @@ -0,0 +1,58 @@ +angular.module('portainer.services') +.factory('AccessService', ['$q', 'UserService', 'TeamService', function AccessServiceFactory($q, UserService, TeamService) { + 'use strict'; + var service = {}; + + function mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs) { + var accesses = []; + var authorizedAccesses = []; + + angular.forEach(userAccesses, function(access) { + if (_.includes(authorizedUserIDs, access.Id)) { + authorizedAccesses.push(access); + } else { + accesses.push(access); + } + }); + + angular.forEach(teamAccesses, function(access) { + if (_.includes(authorizedTeamIDs, access.Id)) { + authorizedAccesses.push(access); + } else { + accesses.push(access); + } + }); + + return { + accesses: accesses, + authorizedAccesses: authorizedAccesses + }; + } + + service.accesses = function(authorizedUserIDs, authorizedTeamIDs) { + var deferred = $q.defer(); + + $q.all({ + users: UserService.users(false), + teams: TeamService.teams() + }) + .then(function success(data) { + var userAccesses = data.users.map(function (user) { + return new UserAccessViewModel(user); + }); + var teamAccesses = data.teams.map(function (team) { + return new TeamAccessViewModel(team); + }); + + var accessData = mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs); + deferred.resolve(accessData); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve users and teams', err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/services/api/dockerhubService.js b/app/services/api/dockerhubService.js new file mode 100644 index 000000000..10f213d68 --- /dev/null +++ b/app/services/api/dockerhubService.js @@ -0,0 +1,26 @@ +angular.module('portainer.services') +.factory('DockerHubService', ['$q', 'DockerHub', function DockerHubServiceFactory($q, DockerHub) { + 'use strict'; + var service = {}; + + service.dockerhub = function() { + var deferred = $q.defer(); + + DockerHub.get().$promise + .then(function success(data) { + var dockerhub = new DockerHubViewModel(data); + deferred.resolve(dockerhub); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve DockerHub details', err: err }); + }); + + return deferred.promise; + }; + + service.update = function(dockerhub) { + return DockerHub.update({}, dockerhub).$promise; + }; + + return service; +}]); diff --git a/app/services/api/registryService.js b/app/services/api/registryService.js new file mode 100644 index 000000000..a71d02403 --- /dev/null +++ b/app/services/api/registryService.js @@ -0,0 +1,92 @@ +angular.module('portainer.services') +.factory('RegistryService', ['$q', 'Registries', 'DockerHubService', 'RegistryHelper', 'ImageHelper', function RegistryServiceFactory($q, Registries, DockerHubService, RegistryHelper, ImageHelper) { + 'use strict'; + var service = {}; + + service.registries = function() { + var deferred = $q.defer(); + + Registries.query().$promise + .then(function success(data) { + var registries = data.map(function (item) { + return new RegistryViewModel(item); + }); + deferred.resolve(registries); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to retrieve registries', err: err}); + }); + + return deferred.promise; + }; + + service.registry = function(id) { + var deferred = $q.defer(); + + Registries.get({id: id}).$promise + .then(function success(data) { + var registry = new RegistryViewModel(data); + deferred.resolve(registry); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to retrieve registry details', err: err}); + }); + + return deferred.promise; + }; + + service.encodedCredentials = function(registry) { + var credentials = { + username: registry.Username, + password: registry.Password, + serveraddress: registry.URL + }; + return btoa(JSON.stringify(credentials)); + }; + + service.updateAccess = function(id, authorizedUserIDs, authorizedTeamIDs) { + return Registries.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise; + }; + + service.deleteRegistry = function(id) { + return Registries.remove({id: id}).$promise; + }; + + service.updateRegistry = function(registry) { + return Registries.update({ id: registry.Id }, registry).$promise; + }; + + service.createRegistry = function(name, URL, authentication, username, password) { + var payload = { + Name: name, + URL: URL, + Authentication: authentication + }; + if (authentication) { + payload.Username = username; + payload.Password = password; + } + return Registries.create({}, payload).$promise; + }; + + service.retrieveRegistryFromRepository = function(repository) { + var deferred = $q.defer(); + + var imageDetails = ImageHelper.extractImageAndRegistryFromRepository(repository); + $q.when(imageDetails.registry ? service.registries() : DockerHubService.dockerhub()) + .then(function success(data) { + var registry = data; + if (imageDetails.registry) { + registry = RegistryHelper.getRegistryByURL(data, imageDetails.registry); + } + deferred.resolve(registry); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve the registry associated to the repository', err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/services/api/settingsService.js b/app/services/api/settingsService.js index 6c9718ce1..f467ff844 100644 --- a/app/services/api/settingsService.js +++ b/app/services/api/settingsService.js @@ -8,8 +8,8 @@ angular.module('portainer.services') Settings.get().$promise .then(function success(data) { - var status = new SettingsViewModel(data); - deferred.resolve(status); + var settings = new SettingsViewModel(data); + deferred.resolve(settings); }) .catch(function error(err) { deferred.reject({ msg: 'Unable to retrieve application settings', err: err }); diff --git a/app/services/docker/imageService.js b/app/services/docker/imageService.js index a43f0c53f..415125f07 100644 --- a/app/services/docker/imageService.js +++ b/app/services/docker/imageService.js @@ -1,5 +1,5 @@ angular.module('portainer.services') -.factory('ImageService', ['$q', 'Image', 'ImageHelper', function ImageServiceFactory($q, Image, ImageHelper) { +.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper) { 'use strict'; var service = {}; @@ -35,10 +35,35 @@ angular.module('portainer.services') return deferred.promise; }; + service.pushImage = function(tag, registry) { + var deferred = $q.defer(); + + var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : ''; + HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); + Image.push({tag: tag}).$promise + .then(function success(data) { + if (data[data.length - 1].error) { + deferred.reject({ msg: data[data.length - 1].error }); + } else { + deferred.resolve(); + } + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to push image tag', err: err }); + }); + return deferred.promise; + }; + + service.pullImage = function(image, registry) { var deferred = $q.defer(); - var imageConfiguration = ImageHelper.createImageConfigForContainer(image, registry); - Image.create(imageConfiguration).$promise + + var imageDetails = ImageHelper.extractImageAndRegistryFromRepository(image); + var imageConfiguration = ImageHelper.createImageConfigForContainer(imageDetails.image, registry.URL); + var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : ''; + HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); + + Image.create({}, imageConfiguration).$promise .then(function success(data) { var err = data.length > 0 && data[data.length - 1].hasOwnProperty('message'); if (err) { @@ -51,12 +76,8 @@ angular.module('portainer.services') .catch(function error(err) { deferred.reject({ msg: 'Unable to pull image', err: err }); }); - return deferred.promise; - }; - service.pullTag = function(tag) { - var imageAndRegistry = ImageHelper.extractImageAndRegistryFromTag(tag); - return service.pullImage(imageAndRegistry.image, imageAndRegistry.registry); + return deferred.promise; }; service.tagImage = function(id, image, registry) { @@ -80,21 +101,5 @@ angular.module('portainer.services') return deferred.promise; }; - service.pushImage = function(tag) { - var deferred = $q.defer(); - Image.push({tag: tag}).$promise - .then(function success(data) { - if (data[data.length - 1].error) { - deferred.reject({ msg: data[data.length - 1].error }); - } else { - deferred.resolve(); - } - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to push image tag', err: err }); - }); - return deferred.promise; - }; - return service; }]); diff --git a/app/services/docker/infoService.js b/app/services/docker/infoService.js deleted file mode 100644 index 1b5f0fd4a..000000000 --- a/app/services/docker/infoService.js +++ /dev/null @@ -1,20 +0,0 @@ -angular.module('portainer.services') -.factory('InfoService', ['$q', 'Info', function InfoServiceFactory($q, Info) { - 'use strict'; - var service = {}; - - service.getVolumePlugins = function() { - var deferred = $q.defer(); - Info.get({}).$promise - .then(function success(data) { - var plugins = data.Plugins.Volume; - deferred.resolve(plugins); - }) - .catch(function error(err) { - deferred.reject({msg: 'Unable to retrieve volume plugin information', err: err}); - }); - return deferred.promise; - }; - - return service; -}]); diff --git a/app/services/docker/systemService.js b/app/services/docker/systemService.js new file mode 100644 index 000000000..4b4bf85b2 --- /dev/null +++ b/app/services/docker/systemService.js @@ -0,0 +1,45 @@ +angular.module('portainer.services') +.factory('SystemService', ['$q', 'System', function SystemServiceFactory($q, System) { + 'use strict'; + var service = {}; + + service.getVolumePlugins = function() { + var deferred = $q.defer(); + System.info({}).$promise + .then(function success(data) { + var plugins = data.Plugins.Volume; + deferred.resolve(plugins); + }) + .catch(function error(err) { + deferred.reject({msg: 'Unable to retrieve volume plugin information', err: err}); + }); + return deferred.promise; + }; + + service.info = function() { + return System.info({}).$promise; + }; + + service.version = function() { + return System.version({}).$promise; + }; + + service.events = function(from, to) { + var deferred = $q.defer(); + + System.events({since: from, until: to}).$promise + .then(function success(data) { + var events = data.map(function (item) { + return new EventViewModel(item); + }); + deferred.resolve(events); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve engine events', err: err }); + }); + + return deferred.promise; + }; + + return service; +}]); diff --git a/app/services/httpRequestHelper.js b/app/services/httpRequestHelper.js new file mode 100644 index 000000000..f6582b5a0 --- /dev/null +++ b/app/services/httpRequestHelper.js @@ -0,0 +1,17 @@ +angular.module('portainer.services') +.factory('HttpRequestHelper', [function HttpRequestHelper() { + 'use strict'; + + var service = {}; + var headers = {}; + + service.registryAuthenticationHeader = function() { + return headers.registryAuthentication; + }; + + service.setRegistryAuthenticationHeader = function(headerValue) { + headers.registryAuthentication = headerValue; + }; + + return service; +}]); diff --git a/app/services/notifications.js b/app/services/notifications.js index 20b943a4b..af2be044d 100644 --- a/app/services/notifications.js +++ b/app/services/notifications.js @@ -15,8 +15,12 @@ angular.module('portainer.services') msg = e.message; } else if (e.data && e.data.length > 0 && e.data[0].message) { msg = e.data[0].message; + } else if (e.err && e.err.data && e.err.data.length > 0 && e.err.data[0].message) { + msg = e.err.data[0].message; } else if (e.msg) { msg = e.msg; + } else if (e.data && e.data.err) { + msg = e.data.err; } toastr.error($sanitize(msg), $sanitize(title), {timeOut: 6000}); }; diff --git a/app/services/stateManager.js b/app/services/stateManager.js index d18e544b6..ae7e7838e 100644 --- a/app/services/stateManager.js +++ b/app/services/stateManager.js @@ -1,5 +1,5 @@ angular.module('portainer.services') -.factory('StateManager', ['$q', 'Info', 'InfoHelper', 'Version', 'LocalStorage', 'SettingsService', 'StatusService', function StateManagerFactory($q, Info, InfoHelper, Version, LocalStorage, SettingsService, StatusService) { +.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService) { 'use strict'; var manager = {}; @@ -75,19 +75,25 @@ angular.module('portainer.services') if (loading) { state.loading = true; } - $q.all([Info.get({}).$promise, Version.get({}).$promise]) + $q.all({ + info: SystemService.info(), + version: SystemService.version() + }) .then(function success(data) { - var endpointMode = InfoHelper.determineEndpointMode(data[0]); - var endpointAPIVersion = parseFloat(data[1].ApiVersion); + var endpointMode = InfoHelper.determineEndpointMode(data.info); + var endpointAPIVersion = parseFloat(data.version.ApiVersion); state.endpoint.mode = endpointMode; state.endpoint.apiVersion = endpointAPIVersion; LocalStorage.storeEndpointState(state.endpoint); - state.loading = false; deferred.resolve(); - }, function error(err) { - state.loading = false; + }) + .catch(function error(err) { deferred.reject({msg: 'Unable to connect to the Docker endpoint', err: err}); + }) + .finally(function final() { + state.loading = false; }); + return deferred.promise; }; diff --git a/assets/css/app.css b/assets/css/app.css index db204b5e2..adbfa66a7 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -395,32 +395,32 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { box-shadow: inset 0 0 1px rgba(0,0,0,.5), inset 0 0 40px #337ab7; } -.ownership_wrapper { +.boxselector_wrapper { display: flex; flex-flow: row wrap; margin: 0.5rem; } -.ownership_wrapper > div { +.boxselector_wrapper > div { flex: 1; padding: 0.5rem; } -.ownership_wrapper .ownership_header { +.boxselector_wrapper .boxselector_header { font-size: 14px; margin-bottom: 5px; font-weight: bold; } -.ownership_wrapper input[type="radio"] { +.boxselector_wrapper input[type="radio"] { display: none; } -.ownership_wrapper input[type="radio"]:not(:disabled) ~ label { +.boxselector_wrapper input[type="radio"]:not(:disabled) ~ label { cursor: pointer; } -.ownership_wrapper label { +.boxselector_wrapper label { font-weight: normal; font-size: 12px; display: block; @@ -433,14 +433,14 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { position: relative; } -.ownership_wrapper input[type="radio"]:checked + label { +.boxselector_wrapper input[type="radio"]:checked + label { background: #337ab7; color: white; padding-top: 2rem; border-color: #337ab7; } -.ownership_wrapper input[type="radio"]:checked + label::after { +.boxselector_wrapper input[type="radio"]:checked + label::after { color: #337ab7; font-family: FontAwesome; border: 2px solid #337ab7; @@ -461,7 +461,7 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { } @media only screen and (max-width: 700px) { - .ownership_wrapper { + .boxselector_wrapper { flex-direction: column; } } diff --git a/gruntfile.js b/gruntfile.js index 0fa157722..3c24f3062 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -215,7 +215,7 @@ module.exports = function (grunt) { 'assets/js/legend.js' // Not a bower package ], html: ['index.html'], - tpl: ['app/components/**/*.html'], + tpl: ['app/components/**/*.html', 'app/directives/**/*.html'], css: ['assets/css/app.css'], cssVendor: [ 'bower_components/bootstrap/dist/css/bootstrap.css', From 1e686f0428687127977f45d6d4b5cfefbe825705 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 20 Jun 2017 13:07:24 +0200 Subject: [PATCH 08/10] =?UTF-8?q?feat(state):=20persist=20application=20st?= =?UTF-8?q?ate=20in=20localstorage=20instead=20of=20ses=E2=80=A6=20(#940)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/app.js b/app/app.js index ab86566f4..c9a359971 100644 --- a/app/app.js +++ b/app/app.js @@ -73,7 +73,6 @@ angular.module('portainer', [ } localStorageServiceProvider - .setStorageType('sessionStorage') .setPrefix('portainer'); jwtOptionsProvider.config({ From dae4893fe1f6d2d0ad5be102666f2af60f58337f Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 20 Jun 2017 13:18:08 +0200 Subject: [PATCH 09/10] feat(endpoint): remove the active endpoint edition restriction (#941) --- app/components/endpoint/endpoint.html | 2 +- app/components/endpoints/endpoints.html | 7 +------ app/components/endpoints/endpointsController.js | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/components/endpoint/endpoint.html b/app/components/endpoint/endpoint.html index b5d95a714..543bc7062 100644 --- a/app/components/endpoint/endpoint.html +++ b/app/components/endpoint/endpoint.html @@ -38,7 +38,7 @@
    - +
    diff --git a/app/components/endpoints/endpoints.html b/app/components/endpoints/endpoints.html index 52755893a..fd6660b66 100644 --- a/app/components/endpoints/endpoints.html +++ b/app/components/endpoints/endpoints.html @@ -183,12 +183,7 @@ {{ endpoint.URL | stripprotocol }} - - Edit - - - You cannot edit the active endpoint - + Edit Manage access diff --git a/app/components/endpoints/endpointsController.js b/app/components/endpoints/endpointsController.js index 3e11a2f49..c7b488b3c 100644 --- a/app/components/endpoints/endpointsController.js +++ b/app/components/endpoints/endpointsController.js @@ -101,7 +101,6 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi EndpointService.endpoints() .then(function success(data) { $scope.endpoints = data; - $scope.activeEndpointID = EndpointProvider.endpointID(); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoints'); From c82cd50d87ef7aa64c06415ffdec6615e6b8cf47 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 20 Jun 2017 13:21:06 +0200 Subject: [PATCH 10/10] chore(version): bump version number --- api/portainer.go | 2 +- app/app.js | 1 - bower.json | 2 +- package.json | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/api/portainer.go b/api/portainer.go index 4e3f757d2..3bb862c98 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -305,7 +305,7 @@ type ( const ( // APIVersion is the version number of the Portainer API. - APIVersion = "1.13.2" + APIVersion = "1.13.3" // DBVersion is the version number of the Portainer database. DBVersion = 2 // DefaultTemplatesURL represents the default URL for the templates definitions. diff --git a/app/app.js b/app/app.js index c9a359971..bba4ad835 100644 --- a/app/app.js +++ b/app/app.js @@ -747,4 +747,3 @@ angular.module('portainer', [ .constant('TEMPLATES_ENDPOINT', 'api/templates') .constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json') .constant('PAGINATION_MAX_ITEMS', 10); - // .constant('UI_VERSION', 'v1.13.2'); diff --git a/bower.json b/bower.json index c4fd69c3c..c4563ea27 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "portainer", - "version": "1.13.2", + "version": "1.13.3", "homepage": "https://github.com/portainer/portainer", "authors": [ "Anthony Lapenna " diff --git a/package.json b/package.json index 40af843db..206efa37e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "1.13.2", + "version": "1.13.3", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git"