From eb4357937873b8af16dcc76e8906874549d04354 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Fri, 23 Feb 2018 03:10:26 +0100 Subject: [PATCH] feat(storidge): introduce endpoint extensions and proxy Storidge API (#1661) --- api/bolt/migrate_dbversion7.go | 20 ++++ api/bolt/migrator.go | 7 ++ api/cmd/portainer/main.go | 1 + api/errors.go | 6 ++ api/http/handler/docker.go | 29 ++---- api/http/handler/endpoint.go | 2 + api/http/handler/extensions.go | 99 +++++++++++++++++++ api/http/handler/extensions/storidge.go | 97 ++++++++++++++++++ api/http/handler/handler.go | 14 ++- api/http/proxy/factory.go | 23 ++--- api/http/proxy/manager.go | 46 +++++++-- api/http/security/authorization.go | 19 ++++ api/http/server.go | 10 ++ api/portainer.go | 33 +++++-- app/docker/rest/system.js | 2 +- .../volumes/create/createVolumeController.js | 12 +-- app/extensions/storidge/rest/cluster.js | 52 ---------- app/extensions/storidge/rest/node.js | 24 ----- app/extensions/storidge/rest/profile.js | 52 ---------- app/extensions/storidge/rest/storidge.js | 20 ++++ .../storidge/services/clusterService.js | 24 ++--- app/extensions/storidge/services/manager.js | 48 --------- .../storidge/services/nodeService.js | 8 +- .../storidge/services/profileService.js | 20 ++-- .../views/cluster/clusterController.js | 30 ++---- .../views/monitor/monitorController.js | 12 +-- .../create/createProfileController.js | 12 +-- .../views/profiles/edit/profileController.js | 13 +-- .../views/profiles/profilesController.js | 12 +-- app/portainer/rest/extension.js | 10 ++ app/portainer/services/api/endpointService.js | 2 + .../services/api/extensionService.js | 17 ++++ app/portainer/services/extensionManager.js | 74 ++++++++++---- app/portainer/services/localStorage.js | 9 -- app/portainer/services/stateManager.js | 26 +++-- app/portainer/views/auth/authController.js | 2 +- .../views/endpoints/endpointsController.js | 23 ++++- .../views/init/admin/initAdminController.js | 6 +- .../init/endpoint/initEndpointController.js | 16 ++- app/portainer/views/sidebar/sidebar.html | 2 +- .../views/sidebar/sidebarController.js | 9 +- 41 files changed, 571 insertions(+), 372 deletions(-) create mode 100644 api/bolt/migrate_dbversion7.go create mode 100644 api/http/handler/extensions.go create mode 100644 api/http/handler/extensions/storidge.go delete mode 100644 app/extensions/storidge/rest/cluster.js delete mode 100644 app/extensions/storidge/rest/node.js delete mode 100644 app/extensions/storidge/rest/profile.js create mode 100644 app/extensions/storidge/rest/storidge.js delete mode 100644 app/extensions/storidge/services/manager.js create mode 100644 app/portainer/rest/extension.js create mode 100644 app/portainer/services/api/extensionService.js diff --git a/api/bolt/migrate_dbversion7.go b/api/bolt/migrate_dbversion7.go new file mode 100644 index 000000000..bcdd199f2 --- /dev/null +++ b/api/bolt/migrate_dbversion7.go @@ -0,0 +1,20 @@ +package bolt + +import "github.com/portainer/portainer" + +func (m *Migrator) updateEndpointsToVersion8() error { + legacyEndpoints, err := m.EndpointService.Endpoints() + if err != nil { + return err + } + + for _, endpoint := range legacyEndpoints { + endpoint.Extensions = []portainer.EndpointExtension{} + err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator.go b/api/bolt/migrator.go index f74a29b34..9b3aacdec 100644 --- a/api/bolt/migrator.go +++ b/api/bolt/migrator.go @@ -89,6 +89,13 @@ func (m *Migrator) Migrate() error { } } + if m.CurrentDBVersion < 8 { + err := m.updateEndpointsToVersion8() + if err != nil { + return err + } + } + err := m.VersionService.StoreDBVersion(portainer.DBVersion) if err != nil { return err diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 3c8c9f2aa..50b0591e4 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -218,6 +218,7 @@ func main() { }, AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, + Extensions: []portainer.EndpointExtension{}, } err = store.EndpointService.CreateEndpoint(endpoint) if err != nil { diff --git a/api/errors.go b/api/errors.go index ce2a9d10a..8e4c07d9e 100644 --- a/api/errors.go +++ b/api/errors.go @@ -57,6 +57,12 @@ const ( ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository") ) +// Endpoint extensions error +const ( + ErrEndpointExtensionNotSupported = Error("This extension is not supported") + ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint") +) + // Version errors. const ( ErrDBVersionNotFound = Error("DB version not found") diff --git a/api/http/handler/docker.go b/api/http/handler/docker.go index c823b3d20..5992d7065 100644 --- a/api/http/handler/docker.go +++ b/api/http/handler/docker.go @@ -35,24 +35,6 @@ func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler { return h } -func (handler *DockerHandler) checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool { - for _, authorizedUserID := range endpoint.AuthorizedUsers { - if authorizedUserID == userID { - return true - } - } - - memberships, _ := handler.TeamMembershipService.TeamMembershipsByUserID(userID) - for _, authorizedTeamID := range endpoint.AuthorizedTeams { - for _, membership := range memberships { - if membership.TeamID == authorizedTeamID { - return true - } - } - } - return false -} - func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] @@ -75,7 +57,14 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } - if tokenData.Role != portainer.AdministratorRole && !handler.checkEndpointAccessControl(endpoint, tokenData.ID) { + + memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) { httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger) return } @@ -85,7 +74,7 @@ func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r if proxy == nil { proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint) if err != nil { - httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) return } } diff --git a/api/http/handler/endpoint.go b/api/http/handler/endpoint.go index 1b052847e..cd345c1d9 100644 --- a/api/http/handler/endpoint.go +++ b/api/http/handler/endpoint.go @@ -136,6 +136,7 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht }, AuthorizedUsers: []portainer.UserID{}, AuthorizedTeams: []portainer.TeamID{}, + Extensions: []portainer.EndpointExtension{}, } err = handler.EndpointService.CreateEndpoint(endpoint) @@ -372,6 +373,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h } handler.ProxyManager.DeleteProxy(string(endpointID)) + handler.ProxyManager.DeleteExtensionProxies(string(endpointID)) err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID)) if err != nil { diff --git a/api/http/handler/extensions.go b/api/http/handler/extensions.go new file mode 100644 index 000000000..f34aab9a7 --- /dev/null +++ b/api/http/handler/extensions.go @@ -0,0 +1,99 @@ +package handler + +import ( + "encoding/json" + "strconv" + + "github.com/asaskevich/govalidator" + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/proxy" + "github.com/portainer/portainer/http/security" + + "log" + "net/http" + "os" + + "github.com/gorilla/mux" +) + +// ExtensionHandler represents an HTTP API handler for managing Settings. +type ExtensionHandler struct { + *mux.Router + Logger *log.Logger + EndpointService portainer.EndpointService + ProxyManager *proxy.Manager +} + +// NewExtensionHandler returns a new instance of ExtensionHandler. +func NewExtensionHandler(bouncer *security.RequestBouncer) *ExtensionHandler { + h := &ExtensionHandler{ + Router: mux.NewRouter(), + Logger: log.New(os.Stderr, "", log.LstdFlags), + } + h.Handle("/{endpointId}/extensions", + bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostExtensions))).Methods(http.MethodPost) + return h +} + +type ( + postExtensionRequest struct { + Type int `valid:"required"` + URL string `valid:"required"` + } +) + +func (handler *ExtensionHandler) handlePostExtensions(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["endpointId"]) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) + return + } + endpointID := portainer.EndpointID(id) + + endpoint, err := handler.EndpointService.Endpoint(endpointID) + if err == portainer.ErrEndpointNotFound { + httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + var req postExtensionRequest + 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 + } + + extensionType := portainer.EndpointExtensionType(req.Type) + + for _, extension := range endpoint.Extensions { + if extension.Type == extensionType { + httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionAlreadyAssociated, http.StatusConflict, handler.Logger) + return + } + } + + extension := portainer.EndpointExtension{ + Type: extensionType, + URL: req.URL, + } + + endpoint.Extensions = append(endpoint.Extensions, extension) + + err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + encodeJSON(w, extension, handler.Logger) +} diff --git a/api/http/handler/extensions/storidge.go b/api/http/handler/extensions/storidge.go new file mode 100644 index 000000000..1977705e5 --- /dev/null +++ b/api/http/handler/extensions/storidge.go @@ -0,0 +1,97 @@ +package extensions + +import ( + "strconv" + + "github.com/portainer/portainer" + httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/proxy" + "github.com/portainer/portainer/http/security" + + "log" + "net/http" + "os" + + "github.com/gorilla/mux" +) + +// StoridgeHandler represents an HTTP API handler for proxying requests to the Docker API. +type StoridgeHandler struct { + *mux.Router + Logger *log.Logger + EndpointService portainer.EndpointService + TeamMembershipService portainer.TeamMembershipService + ProxyManager *proxy.Manager +} + +// NewStoridgeHandler returns a new instance of StoridgeHandler. +func NewStoridgeHandler(bouncer *security.RequestBouncer) *StoridgeHandler { + h := &StoridgeHandler{ + Router: mux.NewRouter(), + Logger: log.New(os.Stderr, "", log.LstdFlags), + } + h.PathPrefix("/{id}/extensions/storidge").Handler( + bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToStoridgeAPI))) + return h +} + +func (handler *StoridgeHandler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + parsedID, err := strconv.Atoi(id) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger) + return + } + + endpointID := portainer.EndpointID(parsedID) + endpoint, err := handler.EndpointService.Endpoint(endpointID) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + if tokenData.Role != portainer.AdministratorRole && !security.AuthorizedEndpointAccess(endpoint, tokenData.ID, memberships) { + httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger) + return + } + + var storidgeExtension *portainer.EndpointExtension + for _, extension := range endpoint.Extensions { + if extension.Type == portainer.StoridgeEndpointExtension { + storidgeExtension = &extension + } + } + + if storidgeExtension == nil { + httperror.WriteErrorResponse(w, portainer.ErrEndpointExtensionNotSupported, http.StatusInternalServerError, handler.Logger) + return + } + + proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension) + + var proxy http.Handler + proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey) + if proxy == nil { + proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL) + if err != nil { + httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } + + http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r) +} diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 24367f360..ac0cc7b9f 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/portainer" httperror "github.com/portainer/portainer/http/error" + "github.com/portainer/portainer/http/handler/extensions" ) // Handler is a collection of all the service handlers. @@ -19,6 +20,8 @@ type Handler struct { EndpointHandler *EndpointHandler RegistryHandler *RegistryHandler DockerHubHandler *DockerHubHandler + ExtensionHandler *ExtensionHandler + StoridgeHandler *extensions.StoridgeHandler ResourceHandler *ResourceHandler StackHandler *StackHandler StatusHandler *StatusHandler @@ -48,11 +51,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case strings.HasPrefix(r.URL.Path, "/api/dockerhub"): http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/endpoints"): - if strings.Contains(r.URL.Path, "/docker/") { + switch { + case strings.Contains(r.URL.Path, "/docker"): http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r) - } else if strings.Contains(r.URL.Path, "/stacks") { + case strings.Contains(r.URL.Path, "/stacks"): http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r) - } else { + case strings.Contains(r.URL.Path, "/extensions/storidge"): + http.StripPrefix("/api/endpoints", h.StoridgeHandler).ServeHTTP(w, r) + case strings.Contains(r.URL.Path, "/extensions"): + http.StripPrefix("/api/endpoints", h.ExtensionHandler).ServeHTTP(w, r) + default: http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) } case strings.HasPrefix(r.URL.Path, "/api/registries"): diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index c0a69109f..1602f9d8d 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -17,14 +17,14 @@ type proxyFactory struct { SettingsService portainer.SettingsService } -func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { +func (factory *proxyFactory) newExtensionHTTPPRoxy(u *url.URL) http.Handler { u.Scheme = "http" - return factory.createReverseProxy(u) + return newSingleHostReverseProxyWithHostHeader(u) } -func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) { +func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) { u.Scheme = "https" - proxy := factory.createReverseProxy(u) + proxy := factory.createDockerReverseProxy(u) config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig) if err != nil { return nil, err @@ -34,7 +34,12 @@ func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpo return proxy, nil } -func (factory *proxyFactory) newSocketProxy(path string) http.Handler { +func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL) http.Handler { + u.Scheme = "http" + return factory.createDockerReverseProxy(u) +} + +func (factory *proxyFactory) newDockerSocketProxy(path string) http.Handler { proxy := &socketProxy{} transport := &proxyTransport{ ResourceControlService: factory.ResourceControlService, @@ -46,13 +51,13 @@ func (factory *proxyFactory) newSocketProxy(path string) http.Handler { return proxy } -func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReverseProxy { +func (factory *proxyFactory) createDockerReverseProxy(u *url.URL) *httputil.ReverseProxy { proxy := newSingleHostReverseProxyWithHostHeader(u) transport := &proxyTransport{ ResourceControlService: factory.ResourceControlService, TeamMembershipService: factory.TeamMembershipService, SettingsService: factory.SettingsService, - dockerTransport: newHTTPTransport(), + dockerTransport: &http.Transport{}, } proxy.Transport = transport return proxy @@ -65,7 +70,3 @@ func newSocketTransport(socketPath string) *http.Transport { }, } } - -func newHTTPTransport() *http.Transport { - return &http.Transport{} -} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index bdba2b216..a5b57a535 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -3,6 +3,7 @@ package proxy import ( "net/http" "net/url" + "strings" "github.com/orcaman/concurrent-map" "github.com/portainer/portainer" @@ -10,14 +11,16 @@ import ( // Manager represents a service used to manage Docker proxies. type Manager struct { - proxyFactory *proxyFactory - proxies cmap.ConcurrentMap + proxyFactory *proxyFactory + proxies cmap.ConcurrentMap + extensionProxies cmap.ConcurrentMap } // NewManager initializes a new proxy Service func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager { return &Manager{ - proxies: cmap.New(), + proxies: cmap.New(), + extensionProxies: cmap.New(), proxyFactory: &proxyFactory{ ResourceControlService: resourceControlService, TeamMembershipService: teamMembershipService, @@ -38,16 +41,16 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht if endpointURL.Scheme == "tcp" { if endpoint.TLSConfig.TLS { - proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint) + proxy, err = manager.proxyFactory.newDockerHTTPSProxy(endpointURL, endpoint) if err != nil { return nil, err } } else { - proxy = manager.proxyFactory.newHTTPProxy(endpointURL) + proxy = manager.proxyFactory.newDockerHTTPProxy(endpointURL) } } else { // Assume unix:// scheme - proxy = manager.proxyFactory.newSocketProxy(endpointURL.Path) + proxy = manager.proxyFactory.newDockerSocketProxy(endpointURL.Path) } manager.proxies.Set(string(endpoint.ID), proxy) @@ -67,3 +70,34 @@ func (manager *Manager) GetProxy(key string) http.Handler { func (manager *Manager) DeleteProxy(key string) { manager.proxies.Remove(key) } + +// CreateAndRegisterExtensionProxy creates a new HTTP reverse proxy for an extension and adds it to the registered proxies. +func (manager *Manager) CreateAndRegisterExtensionProxy(key, extensionAPIURL string) (http.Handler, error) { + + extensionURL, err := url.Parse(extensionAPIURL) + if err != nil { + return nil, err + } + + proxy := manager.proxyFactory.newExtensionHTTPPRoxy(extensionURL) + manager.extensionProxies.Set(key, proxy) + return proxy, nil +} + +// GetExtensionProxy returns the extension proxy associated to a key +func (manager *Manager) GetExtensionProxy(key string) http.Handler { + proxy, ok := manager.extensionProxies.Get(key) + if !ok { + return nil + } + return proxy.(http.Handler) +} + +// DeleteExtensionProxies deletes all the extension proxies associated to a key +func (manager *Manager) DeleteExtensionProxies(key string) { + for _, k := range manager.extensionProxies.Keys() { + if strings.Contains(k, key+"_") { + manager.extensionProxies.Remove(k) + } + } +} diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go index 30d6dfb72..976c2947f 100644 --- a/api/http/security/authorization.go +++ b/api/http/security/authorization.go @@ -121,3 +121,22 @@ func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedReques } return false } + +// AuthorizedEndpointAccess ensure that the user can access the specified endpoint. +// It will check if the user is part of the authorized users or part of a team that is +// listed in the authorized teams. +func AuthorizedEndpointAccess(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool { + for _, authorizedUserID := range endpoint.AuthorizedUsers { + if authorizedUserID == userID { + return true + } + } + for _, membership := range memberships { + for _, authorizedTeamID := range endpoint.AuthorizedTeams { + if membership.TeamID == authorizedTeamID { + return true + } + } + } + return false +} diff --git a/api/http/server.go b/api/http/server.go index d0402bed4..d429044b2 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -3,6 +3,7 @@ package http import ( "github.com/portainer/portainer" "github.com/portainer/portainer/http/handler" + "github.com/portainer/portainer/http/handler/extensions" "github.com/portainer/portainer/http/proxy" "github.com/portainer/portainer/http/security" @@ -96,6 +97,13 @@ func (server *Server) Start() error { stackHandler.GitService = server.GitService stackHandler.RegistryService = server.RegistryService stackHandler.DockerHubService = server.DockerHubService + var extensionHandler = handler.NewExtensionHandler(requestBouncer) + extensionHandler.EndpointService = server.EndpointService + extensionHandler.ProxyManager = proxyManager + var storidgeHandler = extensions.NewStoridgeHandler(requestBouncer) + storidgeHandler.EndpointService = server.EndpointService + storidgeHandler.TeamMembershipService = server.TeamMembershipService + storidgeHandler.ProxyManager = proxyManager server.Handler = &handler.Handler{ AuthHandler: authHandler, @@ -114,6 +122,8 @@ func (server *Server) Start() error { WebSocketHandler: websocketHandler, FileHandler: fileHandler, UploadHandler: uploadHandler, + ExtensionHandler: extensionHandler, + StoridgeHandler: storidgeHandler, } if server.SSL { diff --git a/api/portainer.go b/api/portainer.go index 6cfa73bb9..2ac53a726 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -171,13 +171,14 @@ type ( // Endpoint represents a Docker endpoint with all the info required // to connect to it. Endpoint struct { - ID EndpointID `json:"Id"` - Name string `json:"Name"` - URL string `json:"URL"` - PublicURL string `json:"PublicURL"` - TLSConfig TLSConfiguration `json:"TLSConfig"` - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` + ID EndpointID `json:"Id"` + Name string `json:"Name"` + URL string `json:"URL"` + PublicURL string `json:"PublicURL"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` + Extensions []EndpointExtension `json:"Extensions"` // Deprecated fields // Deprecated in DBVersion == 4 @@ -187,6 +188,16 @@ type ( TLSKeyPath string `json:"TLSKey,omitempty"` } + // EndpointExtension represents a extension associated to an endpoint. + EndpointExtension struct { + Type EndpointExtensionType `json:"Type"` + URL string `json:"URL"` + } + + // EndpointExtensionType represents the type of an endpoint extension. Only + // one extension of each type can be associated to an endpoint. + EndpointExtensionType int + // ResourceControlID represents a resource control identifier. ResourceControlID int @@ -391,7 +402,7 @@ const ( // APIVersion is the version number of the Portainer API. APIVersion = "1.16.2" // DBVersion is the version number of the Portainer database. - DBVersion = 7 + DBVersion = 8 // DefaultTemplatesURL represents the default URL for the templates definitions. DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" ) @@ -452,3 +463,9 @@ const ( // ConfigResourceControl represents a resource control associated to a Docker config ConfigResourceControl ) + +const ( + _ EndpointExtensionType = iota + // StoridgeEndpointExtension represents the Storidge extension + StoridgeEndpointExtension +) diff --git a/app/docker/rest/system.js b/app/docker/rest/system.js index e9f8c18b2..5af2ebbce 100644 --- a/app/docker/rest/system.js +++ b/app/docker/rest/system.js @@ -7,7 +7,7 @@ angular.module('portainer.docker') }, { info: { method: 'GET', params: { action: 'info' }, ignoreLoadingBar: true }, - version: { method: 'GET', params: { action: 'version' }, ignoreLoadingBar: true }, + version: { method: 'GET', params: { action: 'version' }, ignoreLoadingBar: true, timeout: 4500 }, events: { method: 'GET', params: { action: 'events', since: '@since', until: '@until' }, isArray: true, transformResponse: jsonObjectsToArrayHandler diff --git a/app/docker/views/volumes/create/createVolumeController.js b/app/docker/views/volumes/create/createVolumeController.js index 1b5b47ceb..249c05924 100644 --- a/app/docker/views/volumes/create/createVolumeController.js +++ b/app/docker/views/volumes/create/createVolumeController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', 'ExtensionManager', -function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator, ExtensionManager) { +.controller('CreateVolumeController', ['$q', '$scope', '$state', 'VolumeService', 'PluginService', 'ResourceControlService', 'Authentication', 'Notifications', 'FormValidator', +function ($q, $scope, $state, VolumeService, PluginService, ResourceControlService, Authentication, Notifications, FormValidator) { $scope.formValues = { Driver: 'local', @@ -88,11 +88,5 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi } } - ExtensionManager.init() - .then(function success(data) { - initView(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to initialize extensions'); - }); + initView(); }]); diff --git a/app/extensions/storidge/rest/cluster.js b/app/extensions/storidge/rest/cluster.js deleted file mode 100644 index 022f8620a..000000000 --- a/app/extensions/storidge/rest/cluster.js +++ /dev/null @@ -1,52 +0,0 @@ -angular.module('extension.storidge') -.factory('StoridgeCluster', ['$http', 'StoridgeManager', function StoridgeClusterFactory($http, StoridgeManager) { - 'use strict'; - - var service = {}; - - service.queryEvents = function() { - return $http({ - method: 'GET', - url: StoridgeManager.StoridgeAPIURL() + '/events', - skipAuthorization: true, - timeout: 4500, - ignoreLoadingBar: true - }); - }; - - service.queryVersion = function() { - return $http({ - method: 'GET', - url: StoridgeManager.StoridgeAPIURL() + '/version', - skipAuthorization: true - }); - }; - - service.queryInfo = function() { - return $http({ - method: 'GET', - url: StoridgeManager.StoridgeAPIURL() + '/info', - skipAuthorization: true, - timeout: 4500, - ignoreLoadingBar: true - }); - }; - - service.reboot = function() { - return $http({ - method: 'POST', - url: StoridgeManager.StoridgeAPIURL() + '/cluster/reboot', - skipAuthorization: true - }); - }; - - service.shutdown = function() { - return $http({ - method: 'POST', - url: StoridgeManager.StoridgeAPIURL() + '/cluster/shutdown', - skipAuthorization: true - }); - }; - - return service; -}]); diff --git a/app/extensions/storidge/rest/node.js b/app/extensions/storidge/rest/node.js deleted file mode 100644 index f69d66da3..000000000 --- a/app/extensions/storidge/rest/node.js +++ /dev/null @@ -1,24 +0,0 @@ -angular.module('extension.storidge') -.factory('StoridgeNodes', ['$http', 'StoridgeManager', function StoridgeNodesFactory($http, StoridgeManager) { - 'use strict'; - - var service = {}; - - service.query = function() { - return $http({ - method: 'GET', - url: StoridgeManager.StoridgeAPIURL() + '/nodes', - skipAuthorization: true - }); - }; - - service.inspect = function(id) { - return $http({ - method: 'GET', - url: StoridgeManager.StoridgeAPIURL() + '/nodes/' + id, - skipAuthorization: true - }); - }; - - return service; -}]); diff --git a/app/extensions/storidge/rest/profile.js b/app/extensions/storidge/rest/profile.js deleted file mode 100644 index f08e10122..000000000 --- a/app/extensions/storidge/rest/profile.js +++ /dev/null @@ -1,52 +0,0 @@ -angular.module('extension.storidge') -.factory('StoridgeProfiles', ['$http', 'StoridgeManager', function StoridgeProfilesFactory($http, StoridgeManager) { - 'use strict'; - - var service = {}; - - service.create = function(payload) { - return $http({ - method: 'POST', - url: StoridgeManager.StoridgeAPIURL() + '/profiles', - data: payload, - headers: { 'Content-type': 'application/json' }, - skipAuthorization: true - }); - }; - - service.update = function(id, payload) { - return $http({ - method: 'PUT', - url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id, - data: payload, - headers: { 'Content-type': 'application/json' }, - skipAuthorization: true - }); - }; - - service.query = function() { - return $http({ - method: 'GET', - url: StoridgeManager.StoridgeAPIURL() + '/profiles', - skipAuthorization: true - }); - }; - - service.inspect = function(id) { - return $http({ - method: 'GET', - url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id, - skipAuthorization: true - }); - }; - - service.delete = function(id) { - return $http({ - method: 'DELETE', - url: StoridgeManager.StoridgeAPIURL() + '/profiles/' + id, - skipAuthorization: true - }); - }; - - return service; -}]); diff --git a/app/extensions/storidge/rest/storidge.js b/app/extensions/storidge/rest/storidge.js new file mode 100644 index 000000000..5636c96cd --- /dev/null +++ b/app/extensions/storidge/rest/storidge.js @@ -0,0 +1,20 @@ +angular.module('extension.storidge') +.factory('Storidge', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function StoridgeFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/extensions/storidge/:resource/:id/:action', { + endpointId: EndpointProvider.endpointID + }, + { + rebootCluster: { method: 'POST', params: { resource: 'cluster', action: 'reboot' } }, + shutdownCluster: { method: 'POST', params: { resource: 'cluster', action: 'shutdown' } }, + queryEvents: { method: 'GET', params: { resource: 'events' }, timeout: 4500, ignoreLoadingBar: true, isArray: true }, + getVersion: { method: 'GET', params: { resource: 'version' } }, + getInfo: { method: 'GET', params: { resource: 'info' }, timeout: 4500, ignoreLoadingBar: true }, + queryNodes: { method: 'GET', params: { resource: 'nodes' } }, + queryProfiles: { method: 'GET', params: { resource: 'profiles' } }, + getProfile: { method: 'GET', params: { resource: 'profiles' } }, + createProfile: { method: 'POST', params: { resource: 'profiles' } }, + updateProfile: { method: 'PUT', params: { resource: 'profiles', id: '@name' } }, + deleteProfile: { method: 'DELETE', params: { resource: 'profiles' } } + }); +}]); diff --git a/app/extensions/storidge/services/clusterService.js b/app/extensions/storidge/services/clusterService.js index ec1aaec93..97dfa2514 100644 --- a/app/extensions/storidge/services/clusterService.js +++ b/app/extensions/storidge/services/clusterService.js @@ -1,22 +1,22 @@ angular.module('extension.storidge') -.factory('StoridgeClusterService', ['$q', 'StoridgeCluster', function StoridgeClusterServiceFactory($q, StoridgeCluster) { +.factory('StoridgeClusterService', ['$q', 'Storidge', function StoridgeClusterServiceFactory($q, Storidge) { 'use strict'; var service = {}; service.reboot = function() { - return StoridgeCluster.reboot(); + return Storidge.rebootCluster().$promise; }; service.shutdown = function() { - return StoridgeCluster.shutdown(); + return Storidge.shutdownCluster().$promise; }; service.info = function() { var deferred = $q.defer(); - StoridgeCluster.queryInfo() - .then(function success(response) { - var info = new StoridgeInfoModel(response.data); + Storidge.getInfo().$promise + .then(function success(data) { + var info = new StoridgeInfoModel(data); deferred.resolve(info); }) .catch(function error(err) { @@ -29,9 +29,9 @@ angular.module('extension.storidge') service.version = function() { var deferred = $q.defer(); - StoridgeCluster.queryVersion() - .then(function success(response) { - var version = response.data.version; + Storidge.getVersion().$promise + .then(function success(data) { + var version = data.version; deferred.resolve(version); }) .catch(function error(err) { @@ -44,9 +44,9 @@ angular.module('extension.storidge') service.events = function() { var deferred = $q.defer(); - StoridgeCluster.queryEvents() - .then(function success(response) { - var events = response.data.map(function(item) { + Storidge.queryEvents().$promise + .then(function success(data) { + var events = data.map(function(item) { return new StoridgeEventModel(item); }); deferred.resolve(events); diff --git a/app/extensions/storidge/services/manager.js b/app/extensions/storidge/services/manager.js deleted file mode 100644 index e57e7c235..000000000 --- a/app/extensions/storidge/services/manager.js +++ /dev/null @@ -1,48 +0,0 @@ -angular.module('extension.storidge') -.factory('StoridgeManager', ['$q', 'LocalStorage', 'SystemService', function StoridgeManagerFactory($q, LocalStorage, SystemService) { - 'use strict'; - var service = { - API: '' - }; - - service.init = function() { - var deferred = $q.defer(); - - var storedAPIURL = LocalStorage.getStoridgeAPIURL(); - if (storedAPIURL) { - service.API = storedAPIURL; - deferred.resolve(); - } else { - SystemService.info() - .then(function success(data) { - var endpointAddress = LocalStorage.getEndpointPublicURL(); - var storidgeAPIURL = ''; - if (endpointAddress) { - storidgeAPIURL = 'http://' + endpointAddress + ':8282'; - } else { - var managerIP = data.Swarm.NodeAddr; - storidgeAPIURL = 'http://' + managerIP + ':8282'; - } - - service.API = storidgeAPIURL; - LocalStorage.storeStoridgeAPIURL(storidgeAPIURL); - deferred.resolve(); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve Storidge API URL', err: err }); - }); - } - - return deferred.promise; - }; - - service.reset = function() { - LocalStorage.clearStoridgeAPIURL(); - }; - - service.StoridgeAPIURL = function() { - return service.API; - }; - - return service; -}]); diff --git a/app/extensions/storidge/services/nodeService.js b/app/extensions/storidge/services/nodeService.js index ea6640b24..386748ffb 100644 --- a/app/extensions/storidge/services/nodeService.js +++ b/app/extensions/storidge/services/nodeService.js @@ -1,14 +1,14 @@ angular.module('extension.storidge') -.factory('StoridgeNodeService', ['$q', 'StoridgeNodes', function StoridgeNodeServiceFactory($q, StoridgeNodes) { +.factory('StoridgeNodeService', ['$q', 'Storidge', function StoridgeNodeServiceFactory($q, Storidge) { 'use strict'; var service = {}; service.nodes = function() { var deferred = $q.defer(); - StoridgeNodes.query() - .then(function success(response) { - var nodeData = response.data.nodes; + Storidge.queryNodes().$promise + .then(function success(data) { + var nodeData = data.nodes; var nodes = []; for (var key in nodeData) { diff --git a/app/extensions/storidge/services/profileService.js b/app/extensions/storidge/services/profileService.js index e758e1938..3770ab056 100644 --- a/app/extensions/storidge/services/profileService.js +++ b/app/extensions/storidge/services/profileService.js @@ -1,28 +1,28 @@ angular.module('extension.storidge') -.factory('StoridgeProfileService', ['$q', 'StoridgeProfiles', function StoridgeProfileServiceFactory($q, StoridgeProfiles) { +.factory('StoridgeProfileService', ['$q', 'Storidge', function StoridgeProfileServiceFactory($q, Storidge) { 'use strict'; var service = {}; service.create = function(model) { var payload = new StoridgeCreateProfileRequest(model); - return StoridgeProfiles.create(payload); + return Storidge.createProfile(payload).$promise; }; service.update = function(model) { var payload = new StoridgeCreateProfileRequest(model); - return StoridgeProfiles.update(model.Name, payload); + return Storidge.updateProfile(payload).$promise; }; service.delete = function(profileName) { - return StoridgeProfiles.delete(profileName); + return Storidge.deleteProfile({ id: profileName }).$promise; }; service.profile = function(profileName) { var deferred = $q.defer(); - StoridgeProfiles.inspect(profileName) - .then(function success(response) { - var profile = new StoridgeProfileModel(profileName, response.data); + Storidge.getProfile({ id: profileName }).$promise + .then(function success(data) { + var profile = new StoridgeProfileModel(profileName, data); deferred.resolve(profile); }) .catch(function error(err) { @@ -35,9 +35,9 @@ angular.module('extension.storidge') service.profiles = function() { var deferred = $q.defer(); - StoridgeProfiles.query() - .then(function success(response) { - var profiles = response.data.profiles.map(function (item) { + Storidge.queryProfiles().$promise + .then(function success(data) { + var profiles = data.profiles.map(function (item) { return new StoridgeProfileListModel(item); }); deferred.resolve(profiles); diff --git a/app/extensions/storidge/views/cluster/clusterController.js b/app/extensions/storidge/views/cluster/clusterController.js index 5715f3625..6cf3d9f84 100644 --- a/app/extensions/storidge/views/cluster/clusterController.js +++ b/app/extensions/storidge/views/cluster/clusterController.js @@ -1,6 +1,6 @@ angular.module('extension.storidge') -.controller('StoridgeClusterController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeClusterService', 'StoridgeNodeService', 'StoridgeManager', 'ModalService', -function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNodeService, StoridgeManager, ModalService) { +.controller('StoridgeClusterController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeClusterService', 'StoridgeNodeService', 'ModalService', +function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNodeService, ModalService) { $scope.state = { shutdownInProgress: false, @@ -44,30 +44,20 @@ function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNod function shutdownCluster() { $scope.state.shutdownInProgress = true; StoridgeClusterService.shutdown() - .then(function success(data) { - Notifications.success('Cluster successfully shutdown'); - $state.go('docker.dashboard'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to shutdown cluster'); - }) .finally(function final() { $scope.state.shutdownInProgress = false; + Notifications.success('Cluster successfully shutdown'); + $state.go('docker.dashboard'); }); } function rebootCluster() { $scope.state.rebootInProgress = true; StoridgeClusterService.reboot() - .then(function success(data) { - Notifications.success('Cluster successfully rebooted'); - $state.reload(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to reboot cluster'); - }) .finally(function final() { $scope.state.rebootInProgress = false; + Notifications.success('Cluster successfully rebooted'); + $state.reload(); }); } @@ -87,11 +77,5 @@ function ($q, $scope, $state, Notifications, StoridgeClusterService, StoridgeNod }); } - StoridgeManager.init() - .then(function success() { - initView(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); - }); + initView(); }]); diff --git a/app/extensions/storidge/views/monitor/monitorController.js b/app/extensions/storidge/views/monitor/monitorController.js index 64c60b7f1..21c0f788d 100644 --- a/app/extensions/storidge/views/monitor/monitorController.js +++ b/app/extensions/storidge/views/monitor/monitorController.js @@ -1,6 +1,6 @@ angular.module('extension.storidge') -.controller('StoridgeMonitorController', ['$q', '$scope', '$interval', '$document', 'Notifications', 'StoridgeClusterService', 'StoridgeChartService', 'StoridgeManager', 'ModalService', -function ($q, $scope, $interval, $document, Notifications, StoridgeClusterService, StoridgeChartService, StoridgeManager, ModalService) { +.controller('StoridgeMonitorController', ['$q', '$scope', '$interval', '$document', 'Notifications', 'StoridgeClusterService', 'StoridgeChartService', 'ModalService', +function ($q, $scope, $interval, $document, Notifications, StoridgeClusterService, StoridgeChartService, ModalService) { $scope.$on('$destroy', function() { stopRepeater(); @@ -98,11 +98,5 @@ function ($q, $scope, $interval, $document, Notifications, StoridgeClusterServic }); } - StoridgeManager.init() - .then(function success() { - initView(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); - }); + initView(); }]); diff --git a/app/extensions/storidge/views/profiles/create/createProfileController.js b/app/extensions/storidge/views/profiles/create/createProfileController.js index 73666ce1a..6f8f05a7c 100644 --- a/app/extensions/storidge/views/profiles/create/createProfileController.js +++ b/app/extensions/storidge/views/profiles/create/createProfileController.js @@ -1,6 +1,6 @@ angular.module('extension.storidge') -.controller('StoridgeCreateProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'StoridgeManager', -function ($scope, $state, $transition$, Notifications, StoridgeProfileService, StoridgeManager) { +.controller('StoridgeCreateProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', +function ($scope, $state, $transition$, Notifications, StoridgeProfileService) { $scope.state = { NoLimit: true, @@ -62,11 +62,5 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService, S $scope.model = profile; } - StoridgeManager.init() - .then(function success() { - initView(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); - }); + initView(); }]); diff --git a/app/extensions/storidge/views/profiles/edit/profileController.js b/app/extensions/storidge/views/profiles/edit/profileController.js index d3b0ac396..87db55c95 100644 --- a/app/extensions/storidge/views/profiles/edit/profileController.js +++ b/app/extensions/storidge/views/profiles/edit/profileController.js @@ -1,6 +1,6 @@ angular.module('extension.storidge') -.controller('StoridgeProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'StoridgeManager', 'ModalService', -function ($scope, $state, $transition$, Notifications, StoridgeProfileService, StoridgeManager, ModalService) { +.controller('StoridgeProfileController', ['$scope', '$state', '$transition$', 'Notifications', 'StoridgeProfileService', 'ModalService', +function ($scope, $state, $transition$, Notifications, StoridgeProfileService, ModalService) { $scope.state = { NoLimit: false, @@ -88,11 +88,6 @@ function ($scope, $state, $transition$, Notifications, StoridgeProfileService, S }); } - StoridgeManager.init() - .then(function success() { - initView(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); - }); + initView(); + }]); diff --git a/app/extensions/storidge/views/profiles/profilesController.js b/app/extensions/storidge/views/profiles/profilesController.js index f3bd1c441..0d6ba5fc6 100644 --- a/app/extensions/storidge/views/profiles/profilesController.js +++ b/app/extensions/storidge/views/profiles/profilesController.js @@ -1,6 +1,6 @@ angular.module('extension.storidge') -.controller('StoridgeProfilesController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeProfileService', 'StoridgeManager', -function ($q, $scope, $state, Notifications, StoridgeProfileService, StoridgeManager) { +.controller('StoridgeProfilesController', ['$q', '$scope', '$state', 'Notifications', 'StoridgeProfileService', +function ($q, $scope, $state, Notifications, StoridgeProfileService) { $scope.state = { actionInProgress: false @@ -60,11 +60,5 @@ function ($q, $scope, $state, Notifications, StoridgeProfileService, StoridgeMan }); } - StoridgeManager.init() - .then(function success() { - initView(); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to communicate with Storidge API'); - }); + initView(); }]); diff --git a/app/portainer/rest/extension.js b/app/portainer/rest/extension.js new file mode 100644 index 000000000..bd00f3158 --- /dev/null +++ b/app/portainer/rest/extension.js @@ -0,0 +1,10 @@ +angular.module('portainer.app') +.factory('Extensions', ['$resource', 'EndpointProvider', 'API_ENDPOINT_ENDPOINTS', function Extensions($resource, EndpointProvider, API_ENDPOINT_ENDPOINTS) { + 'use strict'; + return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/extensions', { + endpointId: EndpointProvider.endpointID + }, + { + register: { method: 'POST', params: { endpointId: '@endpointId' } } + }); +}]); diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 2a1976e4f..3d7f43d65 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -66,6 +66,7 @@ angular.module('portainer.app') TLSSkipVerify: TLSSkipVerify, TLSSkipClientVerify: TLSSkipClientVerify }; + var deferred = $q.defer(); Endpoints.create({}, endpoint).$promise .then(function success(data) { @@ -85,6 +86,7 @@ angular.module('portainer.app') deferred.notify({upload: false}); deferred.reject({msg: 'Unable to upload TLS certs', err: err}); }); + return deferred.promise; }; diff --git a/app/portainer/services/api/extensionService.js b/app/portainer/services/api/extensionService.js new file mode 100644 index 000000000..42ae3d7dc --- /dev/null +++ b/app/portainer/services/api/extensionService.js @@ -0,0 +1,17 @@ +angular.module('portainer.app') +.factory('ExtensionService', ['Extensions', function ExtensionServiceFactory(Extensions) { + 'use strict'; + var service = {}; + + service.registerStoridgeExtension = function(endpointId, url) { + var payload = { + endpointId: endpointId, + Type: 1, + URL: url + }; + + return Extensions.register(payload).$promise; + }; + + return service; +}]); diff --git a/app/portainer/services/extensionManager.js b/app/portainer/services/extensionManager.js index 4fdbf8cc0..82a3ae1c8 100644 --- a/app/portainer/services/extensionManager.js +++ b/app/portainer/services/extensionManager.js @@ -1,35 +1,71 @@ angular.module('portainer.app') -.factory('ExtensionManager', ['$q', 'PluginService', 'StoridgeManager', function ExtensionManagerFactory($q, PluginService, StoridgeManager) { +.factory('ExtensionManager', ['$q', 'PluginService', 'SystemService', 'ExtensionService', +function ExtensionManagerFactory($q, PluginService, SystemService, ExtensionService) { 'use strict'; var service = {}; - service.init = function() { - return $q.all( - StoridgeManager.init() - ); - }; - - service.reset = function() { - StoridgeManager.reset(); - }; - - service.extensions = function() { + service.initEndpointExtensions = function(endpointId) { var deferred = $q.defer(); - var extensions = []; - PluginService.volumePlugins() + SystemService.version() .then(function success(data) { - var volumePlugins = data; - if (_.includes(volumePlugins, 'cio:latest')) { - extensions.push('storidge'); - } + var endpointAPIVersion = parseFloat(data.ApiVersion); + + return $q.all([ + endpointAPIVersion >= 1.25 ? initStoridgeExtension(endpointId): null + ]); }) - .finally(function final() { + .then(function success(data) { + var extensions = data.filter(function filterNull(x) { + return x; + }); deferred.resolve(extensions); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to connect to the Docker environment', err: err }); }); return deferred.promise; }; + function initStoridgeExtension(endpointId) { + var deferred = $q.defer(); + + PluginService.volumePlugins() + .then(function success(data) { + var volumePlugins = data; + if (_.includes(volumePlugins, 'cio:latest')) { + return registerStoridgeUsingSwarmManagerIP(endpointId); + } + }) + .then(function success(data) { + deferred.resolve(data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'An error occured during Storidge extension check', err: err }); + }); + + return deferred.promise; + } + + function registerStoridgeUsingSwarmManagerIP(endpointId) { + var deferred = $q.defer(); + + SystemService.info() + .then(function success(data) { + var managerIP = data.Swarm.NodeAddr; + var storidgeAPIURL = 'tcp://' + managerIP + ':8282'; + return ExtensionService.registerStoridgeExtension(endpointId, storidgeAPIURL); + }) + .then(function success(data) { + deferred.resolve(data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'An error occured during Storidge extension initialization', err: err }); + }); + + return deferred.promise; + } + return service; }]); diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index 24b7be1cf..27c5256ea 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -41,15 +41,6 @@ angular.module('portainer.app') getPaginationLimit: function(key) { return localStorageService.cookie.get('pagination_' + key); }, - storeStoridgeAPIURL: function(url) { - localStorageService.set('STORIDGE_API_URL', url); - }, - getStoridgeAPIURL: function() { - return localStorageService.get('STORIDGE_API_URL'); - }, - clearStoridgeAPIURL: function() { - return localStorageService.remove('STORIDGE_API_URL'); - }, getDataTableOrder: function(key) { return localStorageService.get('datatable_order_' + key); }, diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index ca9b8aebe..6780b5c62 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -1,5 +1,6 @@ angular.module('portainer.app') -.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'ExtensionManager', 'APPLICATION_CACHE_VALIDITY', function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, ExtensionManager, APPLICATION_CACHE_VALIDITY) { +.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'APPLICATION_CACHE_VALIDITY', +function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY) { 'use strict'; var manager = {}; @@ -107,8 +108,24 @@ angular.module('portainer.app') return deferred.promise; }; - manager.updateEndpointState = function(loading) { + + function assignExtensions(endpointExtensions) { + console.log(JSON.stringify(endpointExtensions, null, 4)); + var extensions = []; + + for (var i = 0; i < endpointExtensions.length; i++) { + var extension = endpointExtensions[i]; + if (extension.Type === 1) { + extensions.push('storidge'); + } + } + + return extensions; + } + + manager.updateEndpointState = function(loading, extensions) { var deferred = $q.defer(); + if (loading) { state.loading = true; } @@ -121,10 +138,7 @@ angular.module('portainer.app') var endpointAPIVersion = parseFloat(data.version.ApiVersion); state.endpoint.mode = endpointMode; state.endpoint.apiVersion = endpointAPIVersion; - return $q.when(endpointAPIVersion < 1.25 || ExtensionManager.extensions()); - }) - .then(function success(data) { - state.endpoint.extensions = data instanceof Array ? data : []; + state.endpoint.extensions = assignExtensions(extensions); LocalStorage.storeEndpointState(state.endpoint); deferred.resolve(); }) diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index e1b1164a2..757175299 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -18,7 +18,7 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica if (!endpointID) { EndpointProvider.setEndpointID(endpoint.Id); } - StateManager.updateEndpointState(true) + StateManager.updateEndpointState(true, endpoint.Extensions) .then(function success(data) { $state.go('docker.dashboard'); }) diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js index 76910cd4b..e55fbf80d 100644 --- a/app/portainer/views/endpoints/endpointsController.js +++ b/app/portainer/views/endpoints/endpointsController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications', -function ($scope, $state, $filter, EndpointService, Notifications) { +.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications', 'ExtensionManager', 'EndpointProvider', +function ($scope, $state, $filter, EndpointService, Notifications, ExtensionManager, EndpointProvider) { $scope.state = { uploadInProgress: false, actionInProgress: false @@ -31,9 +31,22 @@ function ($scope, $state, $filter, EndpointService, Notifications) { var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; $scope.state.actionInProgress = true; - EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile).then(function success(data) { - Notifications.success('Endpoint created', name); - $state.reload(); + EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + .then(function success(data) { + var currentEndpointId = EndpointProvider.endpointID(); + EndpointProvider.setEndpointID(data.Id); + ExtensionManager.initEndpointExtensions(data.Id) + .then(function success(data) { + Notifications.success('Endpoint created', name); + $state.reload(); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create endpoint'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + EndpointProvider.setEndpointID(currentEndpointId); + }); }, function error(err) { $scope.state.uploadInProgress = false; $scope.state.actionInProgress = false; diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js index c7a915f18..f89bdddca 100644 --- a/app/portainer/views/init/admin/initAdminController.js +++ b/app/portainer/views/init/admin/initAdminController.js @@ -30,9 +30,9 @@ function ($scope, $state, $sanitize, Notifications, Authentication, StateManager if (data.length === 0) { $state.go('portainer.init.endpoint'); } else { - var endpointID = data[0].Id; - EndpointProvider.setEndpointID(endpointID); - StateManager.updateEndpointState(false) + var endpoint = data[0]; + EndpointProvider.setEndpointID(endpoint.Id); + StateManager.updateEndpointState(false, endpoint.Extensions) .then(function success() { $state.go('docker.dashboard'); }) diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index e26236e69..8bf9c36b5 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', -function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications) { +.controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'ExtensionManager', +function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications, ExtensionManager) { if (!_.isEmpty($scope.applicationState.endpoint)) { $state.go('docker.dashboard'); @@ -35,7 +35,11 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif .then(function success(data) { endpointID = data.Id; EndpointProvider.setEndpointID(endpointID); - return StateManager.updateEndpointState(false); + return ExtensionManager.initEndpointExtensions(endpointID); + }) + .then(function success(data) { + var extensions = data; + return StateManager.updateEndpointState(false, extensions); }) .then(function success(data) { $state.go('docker.dashboard'); @@ -66,7 +70,11 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif .then(function success(data) { endpointID = data.Id; EndpointProvider.setEndpointID(endpointID); - return StateManager.updateEndpointState(false); + return ExtensionManager.initEndpointExtensions(endpointID); + }) + .then(function success(data) { + var extensions = data; + return StateManager.updateEndpointState(false, extensions); }) .then(function success(data) { $state.go('docker.dashboard'); diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index f1e3dfa6a..a50f6516e 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -24,7 +24,7 @@ sidebar-toggled-on="toggle" current-state="$state.current.name" > -