diff --git a/api/archive/tar.go b/api/archive/tar.go
new file mode 100644
index 000000000..4040a9ec7
--- /dev/null
+++ b/api/archive/tar.go
@@ -0,0 +1,36 @@
+package archive
+
+import (
+ "archive/tar"
+ "bytes"
+)
+
+// TarFileInBuffer will create a tar archive containing a single file named via fileName and using the content
+// specified in fileContent. Returns the archive as a byte array.
+func TarFileInBuffer(fileContent []byte, fileName string) ([]byte, error) {
+ var buffer bytes.Buffer
+ tarWriter := tar.NewWriter(&buffer)
+
+ header := &tar.Header{
+ Name: fileName,
+ Mode: 0600,
+ Size: int64(len(fileContent)),
+ }
+
+ err := tarWriter.WriteHeader(header)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = tarWriter.Write(fileContent)
+ if err != nil {
+ return nil, err
+ }
+
+ err = tarWriter.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ return buffer.Bytes(), nil
+}
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/exec/stack_manager.go b/api/exec/stack_manager.go
index 9b709ea28..b7cf6df86 100644
--- a/api/exec/stack_manager.go
+++ b/api/exec/stack_manager.go
@@ -28,13 +28,13 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
- runCommandAndCaptureStdErr(command, registryArgs, nil)
+ runCommandAndCaptureStdErr(command, registryArgs, nil, "")
}
}
if dockerhub.Authentication {
dockerhubArgs := append(args, "login", "--username", dockerhub.Username, "--password", dockerhub.Password)
- runCommandAndCaptureStdErr(command, dockerhubArgs, nil)
+ runCommandAndCaptureStdErr(command, dockerhubArgs, nil, "")
}
}
@@ -42,7 +42,7 @@ func (manager *StackManager) Login(dockerhub *portainer.DockerHub, registries []
func (manager *StackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
args = append(args, "logout")
- return runCommandAndCaptureStdErr(command, args, nil)
+ return runCommandAndCaptureStdErr(command, args, nil, "")
}
// Deploy executes the docker stack deploy command.
@@ -61,20 +61,21 @@ func (manager *StackManager) Deploy(stack *portainer.Stack, prune bool, endpoint
env = append(env, envvar.Name+"="+envvar.Value)
}
- return runCommandAndCaptureStdErr(command, args, env)
+ return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
}
// Remove executes the docker stack rm command.
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
args = append(args, "stack", "rm", stack.Name)
- return runCommandAndCaptureStdErr(command, args, nil)
+ return runCommandAndCaptureStdErr(command, args, nil, "")
}
-func runCommandAndCaptureStdErr(command string, args []string, env []string) error {
+func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
+ cmd.Dir = workingDir
if env != nil {
cmd.Env = os.Environ()
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/build.go b/api/http/proxy/build.go
new file mode 100644
index 000000000..0deab93b9
--- /dev/null
+++ b/api/http/proxy/build.go
@@ -0,0 +1,56 @@
+package proxy
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "github.com/portainer/portainer/archive"
+)
+
+type postDockerfileRequest struct {
+ Content string
+}
+
+// buildOperation inspects the "Content-Type" header to determine if it needs to alter the request.
+// If the value of the header is empty, it means that a Dockerfile is posted via upload, the function
+// will extract the file content from the request body, tar it, and rewrite the body.
+// If the value of the header contains "application/json", it means that the content of a Dockerfile is posted
+// in the request payload as JSON, the function will create a new file called Dockerfile inside a tar archive and
+// rewrite the body of the request.
+// In any other case, it will leave the request unaltered.
+func buildOperation(request *http.Request) error {
+ contentTypeHeader := request.Header.Get("Content-Type")
+ if contentTypeHeader != "" && !strings.Contains(contentTypeHeader, "application/json") {
+ return nil
+ }
+
+ var dockerfileContent []byte
+
+ if contentTypeHeader == "" {
+ body, err := ioutil.ReadAll(request.Body)
+ if err != nil {
+ return err
+ }
+ dockerfileContent = body
+ } else {
+ var req postDockerfileRequest
+ if err := json.NewDecoder(request.Body).Decode(&req); err != nil {
+ return err
+ }
+ dockerfileContent = []byte(req.Content)
+ }
+
+ buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile")
+ if err != nil {
+ return err
+ }
+
+ request.Body = ioutil.NopCloser(bytes.NewReader(buffer))
+ request.ContentLength = int64(len(buffer))
+ request.Header.Set("Content-Type", "application/x-tar")
+
+ return nil
+}
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/proxy/transport.go b/api/http/proxy/transport.go
index 83edcaf37..3b0b1fa3c 100644
--- a/api/http/proxy/transport.go
+++ b/api/http/proxy/transport.go
@@ -27,6 +27,7 @@ type (
labelBlackList []portainer.Pair
}
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
+ operationRequest func(*http.Request) error
)
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
@@ -59,6 +60,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
return p.proxyNodeRequest(request)
case strings.HasPrefix(path, "/tasks"):
return p.proxyTaskRequest(request)
+ case strings.HasPrefix(path, "/build"):
+ return p.proxyBuildRequest(request)
default:
return p.executeDockerRequest(request)
}
@@ -228,6 +231,10 @@ func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response
}
}
+func (p *proxyTransport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
+ return p.interceptAndRewriteRequest(request, buildOperation)
+}
+
// restrictedOperation ensures that the current user has the required authorizations
// before executing the original request.
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
@@ -300,6 +307,15 @@ func (p *proxyTransport) rewriteOperation(request *http.Request, operation restr
return p.executeRequestAndRewriteResponse(request, operation, executor)
}
+func (p *proxyTransport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) {
+ err := operation(request)
+ if err != nil {
+ return nil, err
+ }
+
+ return p.executeDockerRequest(request)
+}
+
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
response, err := p.executeDockerRequest(request)
if err != nil {
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/security/bouncer.go b/api/http/security/bouncer.go
index e6a8fc962..76b47aaea 100644
--- a/api/http/security/bouncer.go
+++ b/api/http/security/bouncer.go
@@ -12,6 +12,7 @@ type (
// RequestBouncer represents an entity that manages API request accesses
RequestBouncer struct {
jwtService portainer.JWTService
+ userService portainer.UserService
teamMembershipService portainer.TeamMembershipService
authDisabled bool
}
@@ -27,9 +28,10 @@ type (
)
// NewRequestBouncer initializes a new RequestBouncer
-func NewRequestBouncer(jwtService portainer.JWTService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
+func NewRequestBouncer(jwtService portainer.JWTService, userService portainer.UserService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
return &RequestBouncer{
jwtService: jwtService,
+ userService: userService,
teamMembershipService: teamMembershipService,
authDisabled: authDisabled,
}
@@ -136,6 +138,15 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
httperror.WriteErrorResponse(w, err, http.StatusUnauthorized, nil)
return
}
+
+ _, err = bouncer.userService.User(tokenData.ID)
+ if err != nil && err == portainer.ErrUserNotFound {
+ httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
+ return
+ } else if err != nil {
+ httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
+ return
+ }
} else {
tokenData = &portainer.TokenData{
Role: portainer.AdministratorRole,
diff --git a/api/http/server.go b/api/http/server.go
index d0402bed4..fc5f08972 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"
@@ -40,7 +41,7 @@ type Server struct {
// Start starts the HTTP server
func (server *Server) Start() error {
- requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled)
+ requestBouncer := security.NewRequestBouncer(server.JWTService, server.UserService, server.TeamMembershipService, server.AuthDisabled)
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService)
var fileHandler = handler.NewFileHandler(filepath.Join(server.AssetsPath, "public"))
@@ -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..291f75059 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
@@ -389,9 +400,9 @@ type (
const (
// APIVersion is the version number of the Portainer API.
- APIVersion = "1.16.2"
+ APIVersion = "1.16.3"
// 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/api/swagger.yaml b/api/swagger.yaml
index ab7cf1c7d..bec349046 100644
--- a/api/swagger.yaml
+++ b/api/swagger.yaml
@@ -56,7 +56,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
- version: "1.16.2"
+ version: "1.16.3"
title: "Portainer API"
contact:
email: "info@portainer.io"
@@ -2143,7 +2143,7 @@ definitions:
description: "Is analytics enabled"
Version:
type: "string"
- example: "1.16.2"
+ example: "1.16.3"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"
diff --git a/app/__module.js b/app/__module.js
index 15371bdf6..98591b7e6 100644
--- a/app/__module.js
+++ b/app/__module.js
@@ -13,6 +13,8 @@ angular.module('portainer', [
'angular-google-analytics',
'angular-json-tree',
'angular-loading-bar',
+ 'angular-clipboard',
+ 'luegg.directives',
'portainer.templates',
'portainer.app',
'portainer.docker',
diff --git a/app/docker/__module.js b/app/docker/__module.js
index 1e15df46e..0ede8298d 100644
--- a/app/docker/__module.js
+++ b/app/docker/__module.js
@@ -179,6 +179,17 @@ angular.module('portainer.docker', ['portainer.app'])
}
};
+ var imageBuild = {
+ name: 'docker.images.build',
+ url: '/build',
+ views: {
+ 'content@': {
+ templateUrl: 'app/docker/views/images/build/buildimage.html',
+ controller: 'BuildImageController'
+ }
+ }
+ };
+
var networks = {
name: 'docker.networks',
url: '/networks',
@@ -378,6 +389,17 @@ angular.module('portainer.docker', ['portainer.app'])
}
};
+ var taskLogs = {
+ name: 'docker.tasks.task.logs',
+ url: '/logs',
+ views: {
+ 'content@': {
+ templateUrl: 'app/docker/views/tasks/logs/tasklogs.html',
+ controller: 'TaskLogsController'
+ }
+ }
+ };
+
var templates = {
name: 'docker.templates',
url: '/templates',
@@ -457,6 +479,7 @@ angular.module('portainer.docker', ['portainer.app'])
$stateRegistryProvider.register(events);
$stateRegistryProvider.register(images);
$stateRegistryProvider.register(image);
+ $stateRegistryProvider.register(imageBuild);
$stateRegistryProvider.register(networks);
$stateRegistryProvider.register(network);
$stateRegistryProvider.register(networkCreation);
@@ -476,6 +499,7 @@ angular.module('portainer.docker', ['portainer.app'])
$stateRegistryProvider.register(swarmVisualizer);
$stateRegistryProvider.register(tasks);
$stateRegistryProvider.register(task);
+ $stateRegistryProvider.register(taskLogs);
$stateRegistryProvider.register(templates);
$stateRegistryProvider.register(templatesLinuxServer);
$stateRegistryProvider.register(volumes);
diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html
index 3662455d8..f9e82b812 100644
--- a/app/docker/components/datatables/containers-datatable/containersDatatable.html
+++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html
@@ -195,10 +195,10 @@
|
{{ item.StackName ? item.StackName : '-' }} |
diff --git a/app/docker/components/datatables/images-datatable/imagesDatatable.html b/app/docker/components/datatables/images-datatable/imagesDatatable.html
index a22ffc57c..89d916dab 100644
--- a/app/docker/components/datatables/images-datatable/imagesDatatable.html
+++ b/app/docker/components/datatables/images-datatable/imagesDatatable.html
@@ -25,6 +25,9 @@
Force Remove
+
diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html
index 3b39ba144..71e830cd4 100644
--- a/app/docker/components/datatables/services-datatable/servicesDatatable.html
+++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html
@@ -114,7 +114,7 @@
-
+
{{ p.PublishedPort }}:{{ p.TargetPort }}
-
diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.js b/app/docker/components/datatables/services-datatable/servicesDatatable.js
index 93c127267..e87726251 100644
--- a/app/docker/components/datatables/services-datatable/servicesDatatable.js
+++ b/app/docker/components/datatables/services-datatable/servicesDatatable.js
@@ -12,7 +12,7 @@ angular.module('portainer.docker').component('servicesDatatable', {
showOwnershipColumn: '<',
removeAction: '<',
scaleAction: '<',
- swarmManagerIp: '<',
+ publicUrl: '<',
forceUpdateAction: '<',
showForceUpdateButton: '<'
}
diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatable.html b/app/docker/components/datatables/tasks-datatable/tasksDatatable.html
index 654ee3a22..ff8adfbf8 100644
--- a/app/docker/components/datatables/tasks-datatable/tasksDatatable.html
+++ b/app/docker/components/datatables/tasks-datatable/tasksDatatable.html
@@ -54,6 +54,7 @@
+ | Actions |
@@ -63,6 +64,11 @@
{{ item.Slot ? item.Slot : '-' }} |
{{ item.NodeId | tasknodename: $ctrl.nodes }} |
{{ item.Updated | getisodate }} |
+
+
+ View logs
+
+ |
Loading... |
diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatable.js b/app/docker/components/datatables/tasks-datatable/tasksDatatable.js
index c8bae7d68..b560bdade 100644
--- a/app/docker/components/datatables/tasks-datatable/tasksDatatable.js
+++ b/app/docker/components/datatables/tasks-datatable/tasksDatatable.js
@@ -10,6 +10,7 @@ angular.module('portainer.docker').component('tasksDatatable', {
reverseOrder: '<',
nodes: '<',
showTextFilter: '<',
- showSlotColumn: '<'
+ showSlotColumn: '<',
+ showLogsButton: '<'
}
});
diff --git a/app/docker/components/log-viewer/log-viewer.js b/app/docker/components/log-viewer/log-viewer.js
new file mode 100644
index 000000000..5c7dc6d6c
--- /dev/null
+++ b/app/docker/components/log-viewer/log-viewer.js
@@ -0,0 +1,8 @@
+angular.module('portainer.docker').component('logViewer', {
+ templateUrl: 'app/docker/components/log-viewer/logViewer.html',
+ controller: 'LogViewerController',
+ bindings: {
+ data: '=',
+ logCollectionChange: '<'
+ }
+});
diff --git a/app/docker/components/log-viewer/logViewer.html b/app/docker/components/log-viewer/logViewer.html
new file mode 100644
index 000000000..e61923b38
--- /dev/null
+++ b/app/docker/components/log-viewer/logViewer.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+ No log line matching the '{{ $ctrl.state.search }}' filter
+
+
+
+
diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js
new file mode 100644
index 000000000..10b544c58
--- /dev/null
+++ b/app/docker/components/log-viewer/logViewerController.js
@@ -0,0 +1,35 @@
+angular.module('portainer.docker')
+.controller('LogViewerController', ['clipboard',
+function (clipboard) {
+ var ctrl = this;
+
+ this.state = {
+ copySupported: clipboard.supported,
+ logCollection: true,
+ autoScroll: true,
+ search: '',
+ filteredLogs: [],
+ selectedLines: []
+ };
+
+ this.copy = function() {
+ clipboard.copyText(this.state.filteredLogs);
+ $('#refreshRateChange').show();
+ $('#refreshRateChange').fadeOut(1500);
+ };
+
+ this.copySelection = function() {
+ clipboard.copyText(this.state.selectedLines);
+ $('#refreshRateChange').show();
+ $('#refreshRateChange').fadeOut(1500);
+ };
+
+ this.selectLine = function(line) {
+ var idx = this.state.selectedLines.indexOf(line);
+ if (idx === -1) {
+ this.state.selectedLines.push(line);
+ } else {
+ this.state.selectedLines.splice(idx, 1);
+ }
+ };
+}]);
diff --git a/app/docker/filters/filters.js b/app/docker/filters/filters.js
index 17c3efb68..7e03b5e7d 100644
--- a/app/docker/filters/filters.js
+++ b/app/docker/filters/filters.js
@@ -4,6 +4,22 @@ function includeString(text, values) {
});
}
+function strToHash(str) {
+ var hash = 0;
+ for (var i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ return hash;
+}
+
+function hashToHexColor(hash) {
+ var color = '#';
+ for (var i = 0; i < 3;) {
+ color += ('00' + ((hash >> i++ * 8) & 0xFF).toString(16)).slice(-2);
+ }
+ return color;
+}
+
angular.module('portainer.docker')
.filter('visualizerTask', function () {
'use strict';
@@ -19,6 +35,14 @@ angular.module('portainer.docker')
return 'running';
};
})
+.filter('visualizerTaskBorderColor', function () {
+ 'use strict';
+ return function (str) {
+ var hash = strToHash(str);
+ var color = hashToHexColor(hash);
+ return color;
+ };
+})
.filter('taskstatusbadge', function () {
'use strict';
return function (text) {
@@ -42,11 +66,11 @@ angular.module('portainer.docker')
'use strict';
return function (text) {
var status = _.toLower(text);
- if (includeString(status, ['paused', 'starting'])) {
+ if (includeString(status, ['paused', 'starting', 'unhealthy'])) {
return 'warning';
} else if (includeString(status, ['created'])) {
return 'info';
- } else if (includeString(status, ['stopped', 'unhealthy', 'dead', 'exited'])) {
+ } else if (includeString(status, ['stopped', 'dead', 'exited'])) {
return 'danger';
}
return 'success';
diff --git a/app/docker/helpers/nodeHelper.js b/app/docker/helpers/nodeHelper.js
index 3e79915fe..ba02dfdd1 100644
--- a/app/docker/helpers/nodeHelper.js
+++ b/app/docker/helpers/nodeHelper.js
@@ -9,16 +9,6 @@ angular.module('portainer.docker')
Labels: node.Spec.Labels,
Availability: node.Spec.Availability
};
- },
- getManagerIP: function(nodes) {
- var managerIp;
- for (var n in nodes) {
- if (undefined === nodes[n].ManagerStatus || nodes[n].ManagerStatus.Reachability !== 'reachable') {
- continue;
- }
- managerIp = nodes[n].ManagerStatus.Addr.split(':')[0];
- }
- return managerIp;
}
};
}]);
diff --git a/app/docker/models/container.js b/app/docker/models/container.js
index c37f2fe93..64d4d7047 100644
--- a/app/docker/models/container.js
+++ b/app/docker/models/container.js
@@ -36,3 +36,35 @@ function ContainerViewModel(data) {
}
}
}
+
+function ContainerStatsViewModel(data) {
+ this.Date = data.read;
+ this.MemoryUsage = data.memory_stats.usage;
+ this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
+ this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
+ this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
+ this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
+ if (data.cpu_stats.cpu_usage.percpu_usage) {
+ this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
+ }
+ this.Networks = _.values(data.networks);
+}
+
+function ContainerDetailsViewModel(data) {
+ this.Model = data;
+ this.Id = data.Id;
+ this.State = data.State;
+ this.Created = data.Created;
+ this.Name = data.Name;
+ this.NetworkSettings = data.NetworkSettings;
+ this.Args = data.Args;
+ this.Image = data.Image;
+ this.Config = data.Config;
+ this.HostConfig = data.HostConfig;
+ this.Mounts = data.Mounts;
+ if (data.Portainer) {
+ if (data.Portainer.ResourceControl) {
+ this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
+ }
+ }
+}
diff --git a/app/docker/models/containerDetails.js b/app/docker/models/containerDetails.js
deleted file mode 100644
index eae58c105..000000000
--- a/app/docker/models/containerDetails.js
+++ /dev/null
@@ -1,18 +0,0 @@
-function ContainerDetailsViewModel(data) {
- this.Model = data;
- this.Id = data.Id;
- this.State = data.State;
- this.Created = data.Created;
- this.Name = data.Name;
- this.NetworkSettings = data.NetworkSettings;
- this.Args = data.Args;
- this.Image = data.Image;
- this.Config = data.Config;
- this.HostConfig = data.HostConfig;
- this.Mounts = data.Mounts;
- if (data.Portainer) {
- if (data.Portainer.ResourceControl) {
- this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
- }
- }
-}
diff --git a/app/docker/models/containerStats.js b/app/docker/models/containerStats.js
deleted file mode 100644
index aad3b48b3..000000000
--- a/app/docker/models/containerStats.js
+++ /dev/null
@@ -1,12 +0,0 @@
-function ContainerStatsViewModel(data) {
- this.Date = data.read;
- this.MemoryUsage = data.memory_stats.usage;
- this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
- this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
- this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
- this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
- if (data.cpu_stats.cpu_usage.percpu_usage) {
- this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
- }
- this.Networks = _.values(data.networks);
-}
diff --git a/app/docker/models/event.js b/app/docker/models/event.js
index 6ce3647ca..dc49c63b5 100644
--- a/app/docker/models/event.js
+++ b/app/docker/models/event.js
@@ -37,6 +37,33 @@ function createEventDetails(event) {
case 'attach':
details = 'Container ' + eventAttr.name + ' attached';
break;
+ case 'detach':
+ details = 'Container ' + eventAttr.name + ' detached';
+ break;
+ case 'copy':
+ details = 'Container ' + eventAttr.name + ' copied';
+ break;
+ case 'export':
+ details = 'Container ' + eventAttr.name + ' exported';
+ break;
+ case 'health_status':
+ details = 'Container ' + eventAttr.name + ' executed health status';
+ break;
+ case 'oom':
+ details = 'Container ' + eventAttr.name + ' goes in out of memory';
+ break;
+ case 'rename':
+ details = 'Container ' + eventAttr.name + ' renamed';
+ break;
+ case 'resize':
+ details = 'Container ' + eventAttr.name + ' resized';
+ break;
+ case 'top':
+ details = 'Showed running processes for container ' + eventAttr.name;
+ break;
+ case 'update':
+ details = 'Container ' + eventAttr.name + ' updated';
+ break;
default:
if (event.Action.indexOf('exec_create') === 0) {
details = 'Exec instance created';
@@ -52,15 +79,27 @@ function createEventDetails(event) {
case 'delete':
details = 'Image deleted';
break;
+ case 'import':
+ details = 'Image ' + event.Actor.ID + ' imported';
+ break;
+ case 'load':
+ details = 'Image ' + event.Actor.ID + ' loaded';
+ break;
case 'tag':
details = 'New tag created for ' + eventAttr.name;
break;
case 'untag':
details = 'Image untagged';
break;
+ case 'save':
+ details = 'Image ' + event.Actor.ID + ' saved';
+ break;
case 'pull':
details = 'Image ' + event.Actor.ID + ' pulled';
break;
+ case 'push':
+ details = 'Image ' + event.Actor.ID + ' pushed';
+ break;
default:
details = 'Unsupported event';
}
@@ -73,6 +112,9 @@ function createEventDetails(event) {
case 'destroy':
details = 'Network ' + eventAttr.name + ' deleted';
break;
+ case 'remove':
+ details = 'Network ' + eventAttr.name + ' removed';
+ break;
case 'connect':
details = 'Container connected to ' + eventAttr.name + ' network';
break;
diff --git a/app/docker/models/image.js b/app/docker/models/image.js
index 5dd073800..60e7b0c11 100644
--- a/app/docker/models/image.js
+++ b/app/docker/models/image.js
@@ -8,3 +8,24 @@ function ImageViewModel(data) {
this.VirtualSize = data.VirtualSize;
this.ContainerCount = data.ContainerCount;
}
+
+function ImageBuildModel(data) {
+ this.hasError = false;
+ var buildLogs = [];
+
+ for (var i = 0; i < data.length; i++) {
+ var line = data[i];
+
+ if (line.stream) {
+ line = line.stream.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
+ buildLogs.push(line);
+ }
+
+ if (line.errorDetail) {
+ buildLogs.push(line.errorDetail.message);
+ this.hasError = true;
+ }
+ }
+
+ this.buildLogs = buildLogs;
+}
diff --git a/app/docker/models/stackTemplate.js b/app/docker/models/stackTemplate.js
index 728d98421..7e96321e9 100644
--- a/app/docker/models/stackTemplate.js
+++ b/app/docker/models/stackTemplate.js
@@ -1,5 +1,6 @@
function StackTemplateViewModel(data) {
this.Type = data.type;
+ this.Name = data.name;
this.Title = data.title;
this.Description = data.description;
this.Note = data.note;
diff --git a/app/docker/models/template.js b/app/docker/models/template.js
index c4d8c3b44..dcc00c843 100644
--- a/app/docker/models/template.js
+++ b/app/docker/models/template.js
@@ -1,5 +1,6 @@
function TemplateViewModel(data) {
this.Type = data.type;
+ this.Name = data.name;
this.Title = data.title;
this.Description = data.description;
this.Note = data.note;
@@ -45,5 +46,5 @@ function TemplateViewModel(data) {
};
});
}
- this.Hosts = data.hosts ? data.hosts : [];
+ this.Hosts = data.hosts ? data.hosts : [];
}
diff --git a/app/docker/rest/build.js b/app/docker/rest/build.js
new file mode 100644
index 000000000..565c35f4f
--- /dev/null
+++ b/app/docker/rest/build.js
@@ -0,0 +1,18 @@
+angular.module('portainer.docker')
+.factory('Build', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BuildFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
+ 'use strict';
+ return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/build', {
+ endpointId: EndpointProvider.endpointID
+ },
+ {
+ buildImage: {
+ method: 'POST', ignoreLoadingBar: true,
+ transformResponse: jsonObjectsToArrayHandler, isArray: true,
+ headers: { 'Content-Type': 'application/x-tar' }
+ },
+ buildImageOverride: {
+ method: 'POST', ignoreLoadingBar: true,
+ transformResponse: jsonObjectsToArrayHandler, isArray: true
+ }
+ });
+}]);
diff --git a/app/docker/rest/commit.js b/app/docker/rest/commit.js
new file mode 100644
index 000000000..f2cf6dc03
--- /dev/null
+++ b/app/docker/rest/commit.js
@@ -0,0 +1,10 @@
+angular.module('portainer.docker')
+.factory('Commit', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function CommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
+ 'use strict';
+ return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', {
+ endpointId: EndpointProvider.endpointID
+ },
+ {
+ commitContainer: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}, ignoreLoadingBar: true}
+ });
+}]);
diff --git a/app/docker/rest/container.js b/app/docker/rest/container.js
index cfa0a0587..b2acedd59 100644
--- a/app/docker/rest/container.js
+++ b/app/docker/rest/container.js
@@ -13,6 +13,11 @@ angular.module('portainer.docker')
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
+ logs: {
+ method: 'GET', params: { id: '@id', action: 'logs' },
+ timeout: 4500, ignoreLoadingBar: true,
+ transformResponse: logsHandler, isArray: true
+ },
stats: {
method: 'GET', params: { id: '@id', stream: false, action: 'stats' },
timeout: 4500, ignoreLoadingBar: true
diff --git a/app/docker/rest/containerCommit.js b/app/docker/rest/containerCommit.js
deleted file mode 100644
index 6b3233d9d..000000000
--- a/app/docker/rest/containerCommit.js
+++ /dev/null
@@ -1,10 +0,0 @@
-angular.module('portainer.docker')
-.factory('ContainerCommit', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerCommitFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
- 'use strict';
- return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/commit', {
- endpointId: EndpointProvider.endpointID
- },
- {
- commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}, ignoreLoadingBar: true}
- });
-}]);
diff --git a/app/docker/rest/containerLogs.js b/app/docker/rest/containerLogs.js
deleted file mode 100644
index f4520f3b0..000000000
--- a/app/docker/rest/containerLogs.js
+++ /dev/null
@@ -1,21 +0,0 @@
-angular.module('portainer.docker')
-.factory('ContainerLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ContainerLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
- 'use strict';
- return {
- get: function (id, params, callback) {
- $http({
- method: 'GET',
- url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/containers/' + id + '/logs',
- params: {
- 'stdout': params.stdout || 0,
- 'stderr': params.stderr || 0,
- 'timestamps': params.timestamps || 0,
- 'tail': params.tail || 'all'
- },
- ignoreLoadingBar: true
- }).success(callback).error(function (data, status, headers, config) {
- console.log(data);
- });
- }
- };
-}]);
diff --git a/app/docker/rest/response/handlers.js b/app/docker/rest/response/handlers.js
index 03aa6c1d1..53e660c39 100644
--- a/app/docker/rest/response/handlers.js
+++ b/app/docker/rest/response/handlers.js
@@ -44,6 +44,18 @@ function genericHandler(data) {
return response;
}
+// The Docker API returns the logs as a single string.
+// This handler will return an array with each line being an entry.
+// It will also strip the 8 first characters of each line and remove any ANSI code related character sequences.
+function logsHandler(data) {
+ var logs = data;
+ logs = logs.substring(8);
+ logs = logs.replace(/\n(.{8})/g, '\n\r');
+ logs = logs.replace(
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
+ return logs.split('\n');
+}
+
// Image delete API returns an array on success (Docker 1.9 -> Docker 1.12).
// On error, it returns either an error message as a string (Docker < 1.12) or a JSON object with the field message
// container the error (Docker = 1.12).
diff --git a/app/docker/rest/service.js b/app/docker/rest/service.js
index 66e19468c..3af88acd2 100644
--- a/app/docker/rest/service.js
+++ b/app/docker/rest/service.js
@@ -13,6 +13,11 @@ angular.module('portainer.docker')
ignoreLoadingBar: true
},
update: { method: 'POST', params: {id: '@id', action: 'update', version: '@version'} },
- remove: { method: 'DELETE', params: {id: '@id'} }
+ remove: { method: 'DELETE', params: {id: '@id'} },
+ logs: {
+ method: 'GET', params: { id: '@id', action: 'logs' },
+ timeout: 4500, ignoreLoadingBar: true,
+ transformResponse: logsHandler, isArray: true
+ }
});
}]);
diff --git a/app/docker/rest/serviceLogs.js b/app/docker/rest/serviceLogs.js
deleted file mode 100644
index fbd25ec66..000000000
--- a/app/docker/rest/serviceLogs.js
+++ /dev/null
@@ -1,20 +0,0 @@
-angular.module('portainer.docker')
-.factory('ServiceLogs', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ServiceLogsFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
- 'use strict';
- return {
- get: function (id, params, callback) {
- $http({
- method: 'GET',
- url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/services/' + id + '/logs',
- params: {
- 'stdout': params.stdout || 0,
- 'stderr': params.stderr || 0,
- 'timestamps': params.timestamps || 0,
- 'tail': params.tail || 'all'
- }
- }).success(callback).error(function (data, status, headers, config) {
- console.log(data);
- });
- }
- };
-}]);
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/rest/task.js b/app/docker/rest/task.js
index 73032ae12..9683b05ba 100644
--- a/app/docker/rest/task.js
+++ b/app/docker/rest/task.js
@@ -1,11 +1,16 @@
angular.module('portainer.docker')
.factory('Task', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function TaskFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
- return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id', {
+ return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/tasks/:id/:action', {
endpointId: EndpointProvider.endpointID
},
{
get: { method: 'GET', params: {id: '@id'} },
- query: { method: 'GET', isArray: true, params: {filters: '@filters'} }
+ query: { method: 'GET', isArray: true, params: {filters: '@filters'} },
+ logs: {
+ method: 'GET', params: { id: '@id', action: 'logs' },
+ timeout: 4500, ignoreLoadingBar: true,
+ transformResponse: logsHandler, isArray: true
+ }
});
}]);
diff --git a/app/docker/services/buildService.js b/app/docker/services/buildService.js
new file mode 100644
index 000000000..60e7383cd
--- /dev/null
+++ b/app/docker/services/buildService.js
@@ -0,0 +1,65 @@
+angular.module('portainer.docker')
+.factory('BuildService', ['$q', 'Build', 'FileUploadService', function BuildServiceFactory($q, Build, FileUploadService) {
+ 'use strict';
+ var service = {};
+
+ service.buildImageFromUpload = function(names, file, path) {
+ var deferred = $q.defer();
+
+ FileUploadService.buildImage(names, file, path)
+ .then(function success(response) {
+ var model = new ImageBuildModel(response.data);
+ deferred.resolve(model);
+ })
+ .catch(function error(err) {
+ deferred.reject(err);
+ });
+
+ return deferred.promise;
+ };
+
+ service.buildImageFromURL = function(names, url, path) {
+ var params = {
+ t: names,
+ remote: url,
+ dockerfile: path
+ };
+
+ var deferred = $q.defer();
+
+ Build.buildImage(params, {}).$promise
+ .then(function success(data) {
+ var model = new ImageBuildModel(data);
+ deferred.resolve(model);
+ })
+ .catch(function error(err) {
+ deferred.reject(err);
+ });
+
+ return deferred.promise;
+ };
+
+ service.buildImageFromDockerfileContent = function(names, content) {
+ var params = {
+ t: names
+ };
+ var payload = {
+ content: content
+ };
+
+ var deferred = $q.defer();
+
+ Build.buildImageOverride(params, payload).$promise
+ .then(function success(data) {
+ var model = new ImageBuildModel(data);
+ deferred.resolve(model);
+ })
+ .catch(function error(err) {
+ deferred.reject(err);
+ });
+
+ return deferred.promise;
+ };
+
+ return service;
+}]);
diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js
index 204dbe545..51efd1b8e 100644
--- a/app/docker/services/containerService.js
+++ b/app/docker/services/containerService.js
@@ -131,6 +131,18 @@ angular.module('portainer.docker')
return deferred.promise;
};
+ service.logs = function(id, stdout, stderr, timestamps, tail) {
+ var parameters = {
+ id: id,
+ stdout: stdout || 0,
+ stderr: stderr || 0,
+ timestamps: timestamps || 0,
+ tail: tail || 'all'
+ };
+
+ return Container.logs(parameters).$promise;
+ };
+
service.containerStats = function(id) {
var deferred = $q.defer();
diff --git a/app/docker/services/serviceService.js b/app/docker/services/serviceService.js
index 7f136b6ab..1e28b75f9 100644
--- a/app/docker/services/serviceService.js
+++ b/app/docker/services/serviceService.js
@@ -58,5 +58,17 @@ angular.module('portainer.docker')
return Service.update({ id: service.Id, version: service.Version }, config).$promise;
};
+ service.logs = function(id, stdout, stderr, timestamps, tail) {
+ var parameters = {
+ id: id,
+ stdout: stdout || 0,
+ stderr: stderr || 0,
+ timestamps: timestamps || 0,
+ tail: tail || 'all'
+ };
+
+ return Service.logs(parameters).$promise;
+ };
+
return service;
}]);
diff --git a/app/docker/services/taskService.js b/app/docker/services/taskService.js
index 3280c5c15..4fdbe4331 100644
--- a/app/docker/services/taskService.js
+++ b/app/docker/services/taskService.js
@@ -35,5 +35,17 @@ angular.module('portainer.docker')
return deferred.promise;
};
+ service.logs = function(id, stdout, stderr, timestamps, tail) {
+ var parameters = {
+ id: id,
+ stdout: stdout || 0,
+ stderr: stderr || 0,
+ timestamps: timestamps || 0,
+ tail: tail || 'all'
+ };
+
+ return Task.logs(parameters).$promise;
+ };
+
return service;
}]);
diff --git a/app/docker/views/configs/create/createConfigController.js b/app/docker/views/configs/create/createConfigController.js
index 19a698b0b..07868dd3c 100644
--- a/app/docker/views/configs/create/createConfigController.js
+++ b/app/docker/views/configs/create/createConfigController.js
@@ -1,11 +1,12 @@
angular.module('portainer.docker')
-.controller('CreateConfigController', ['$scope', '$state', '$document', 'Notifications', 'ConfigService', 'Authentication', 'FormValidator', 'ResourceControlService', 'CodeMirrorService',
-function ($scope, $state, $document, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService, CodeMirrorService) {
+.controller('CreateConfigController', ['$scope', '$state', 'Notifications', 'ConfigService', 'Authentication', 'FormValidator', 'ResourceControlService',
+function ($scope, $state, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {
$scope.formValues = {
Name: '',
Labels: [],
- AccessControlData: new AccessControlFormData()
+ AccessControlData: new AccessControlFormData(),
+ ConfigContent: ''
};
$scope.state = {
@@ -31,9 +32,7 @@ function ($scope, $state, $document, Notifications, ConfigService, Authenticatio
}
function prepareConfigData(config) {
- // The codemirror editor does not work with ng-model so we need to retrieve
- // the value directly from the editor.
- var configData = $scope.editor.getValue();
+ var configData = $scope.formValues.ConfigContent;
config.Data = btoa(unescape(encodeURIComponent(configData)));
}
@@ -62,6 +61,11 @@ function ($scope, $state, $document, Notifications, ConfigService, Authenticatio
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1;
+ if ($scope.formValues.ConfigContent === '') {
+ $scope.state.formValidationError = 'Config content must not be empty';
+ return;
+ }
+
if (!validateForm(accessControlData, isAdmin)) {
return;
}
@@ -83,14 +87,7 @@ function ($scope, $state, $document, Notifications, ConfigService, Authenticatio
});
};
- function initView() {
- $document.ready(function() {
- var webEditorElement = $document[0].getElementById('config-editor', false);
- if (webEditorElement) {
- $scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement, false, false);
- }
- });
- }
-
- initView();
+ $scope.editorUpdate = function(cm) {
+ $scope.formValues.ConfigContent = cm.getValue();
+ };
}]);
diff --git a/app/docker/views/configs/create/createconfig.html b/app/docker/views/configs/create/createconfig.html
index 0f7972ad0..d55343dcb 100644
--- a/app/docker/views/configs/create/createconfig.html
+++ b/app/docker/views/configs/create/createconfig.html
@@ -21,7 +21,12 @@
@@ -62,6 +67,7 @@
diff --git a/app/docker/views/configs/edit/config.html b/app/docker/views/configs/edit/config.html
index 2b84b6d54..32475db4f 100644
--- a/app/docker/views/configs/edit/config.html
+++ b/app/docker/views/configs/edit/config.html
@@ -70,7 +70,12 @@
diff --git a/app/docker/views/configs/edit/configController.js b/app/docker/views/configs/edit/configController.js
index 72c1ea86b..3e0ad0178 100644
--- a/app/docker/views/configs/edit/configController.js
+++ b/app/docker/views/configs/edit/configController.js
@@ -1,6 +1,6 @@
angular.module('portainer.docker')
-.controller('ConfigController', ['$scope', '$transition$', '$state', '$document', 'ConfigService', 'Notifications', 'CodeMirrorService',
-function ($scope, $transition$, $state, $document, ConfigService, Notifications, CodeMirrorService) {
+.controller('ConfigController', ['$scope', '$transition$', '$state', 'ConfigService', 'Notifications',
+function ($scope, $transition$, $state, ConfigService, Notifications) {
$scope.removeConfig = function removeConfig(configId) {
ConfigService.remove(configId)
@@ -13,20 +13,10 @@ function ($scope, $transition$, $state, $document, ConfigService, Notifications,
});
};
- function initEditor() {
- $document.ready(function() {
- var webEditorElement = $document[0].getElementById('config-editor');
- if (webEditorElement) {
- $scope.editor = CodeMirrorService.applyCodeMirrorOnElement(webEditorElement, false, true);
- }
- });
- }
-
function initView() {
ConfigService.config($transition$.params().id)
.then(function success(data) {
$scope.config = data;
- initEditor();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve config details');
diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html
index 58fa5f819..e384da895 100644
--- a/app/docker/views/containers/edit/container.html
+++ b/app/docker/views/containers/edit/container.html
@@ -85,7 +85,7 @@
diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js
index d89f3dfa4..c96578c7b 100644
--- a/app/docker/views/containers/edit/containerController.js
+++ b/app/docker/views/containers/edit/containerController.js
@@ -1,6 +1,6 @@
angular.module('portainer.docker')
-.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
-function ($q, $scope, $state, $transition$, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService) {
+.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
+function ($q, $scope, $state, $transition$, $filter, Container, Commit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService) {
$scope.activityTime = 0;
$scope.portBindings = [];
@@ -80,7 +80,7 @@ function ($q, $scope, $state, $transition$, $filter, Container, ContainerCommit,
var image = $scope.config.Image;
var registry = $scope.config.Registry;
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
- ContainerCommit.commit({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
+ Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
update();
Notifications.success('Container commited', $transition$.params().id);
}, function (e) {
diff --git a/app/docker/views/containers/logs/containerLogsController.js b/app/docker/views/containers/logs/containerLogsController.js
index dc7169164..c874d2db8 100644
--- a/app/docker/views/containers/logs/containerLogsController.js
+++ b/app/docker/views/containers/logs/containerLogsController.js
@@ -1,70 +1,71 @@
angular.module('portainer.docker')
-.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container', 'Notifications',
-function ($scope, $transition$, $anchorScroll, ContainerLogs, Container, Notifications) {
- $scope.state = {};
- $scope.state.displayTimestampsOut = false;
- $scope.state.displayTimestampsErr = false;
- $scope.stdout = '';
- $scope.stderr = '';
- $scope.tailLines = 2000;
+.controller('ContainerLogsController', ['$scope', '$transition$', '$interval', 'ContainerService', 'Notifications',
+function ($scope, $transition$, $interval, ContainerService, Notifications) {
+ $scope.state = {
+ refreshRate: 3,
+ lineCount: 2000
+ };
- Container.get({id: $transition$.params().id}, function (d) {
- $scope.container = d;
- }, function (e) {
- Notifications.error('Failure', e, 'Unable to retrieve container info');
+ $scope.changeLogCollection = function(logCollectionStatus) {
+ if (!logCollectionStatus) {
+ stopRepeater();
+ } else {
+ setUpdateRepeater();
+ }
+ };
+
+ $scope.$on('$destroy', function() {
+ stopRepeater();
});
- function getLogs() {
- getLogsStdout();
- getLogsStderr();
+ function stopRepeater() {
+ var repeater = $scope.repeater;
+ if (angular.isDefined(repeater)) {
+ $interval.cancel(repeater);
+ repeater = null;
+ }
}
- function getLogsStderr() {
- ContainerLogs.get($transition$.params().id, {
- stdout: 0,
- stderr: 1,
- timestamps: $scope.state.displayTimestampsErr,
- tail: $scope.tailLines
- }, function (data, status, headers, config) {
- // Replace carriage returns with newlines to clean up output
- data = data.replace(/[\r]/g, '\n');
- // Strip 8 byte header from each line of output
- data = data.substring(8);
- data = data.replace(/\n(.{8})/g, '\n');
- $scope.stderr = data;
+ function update(logs) {
+ $scope.logs = logs;
+ }
+
+ function setUpdateRepeater() {
+ var refreshRate = $scope.state.refreshRate;
+ $scope.repeater = $interval(function() {
+ ContainerService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
+ .then(function success(data) {
+ $scope.logs = data;
+ })
+ .catch(function error(err) {
+ stopRepeater();
+ Notifications.error('Failure', err, 'Unable to retrieve container logs');
+ });
+ }, refreshRate * 1000);
+ }
+
+ function startLogPolling() {
+ ContainerService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
+ .then(function success(data) {
+ $scope.logs = data;
+ setUpdateRepeater();
+ })
+ .catch(function error(err) {
+ stopRepeater();
+ Notifications.error('Failure', err, 'Unable to retrieve container logs');
});
}
- function getLogsStdout() {
- ContainerLogs.get($transition$.params().id, {
- stdout: 1,
- stderr: 0,
- timestamps: $scope.state.displayTimestampsOut,
- tail: $scope.tailLines
- }, function (data, status, headers, config) {
- // Replace carriage returns with newlines to clean up output
- data = data.replace(/[\r]/g, '\n');
- // Strip 8 byte header from each line of output
- data = data.substring(8);
- data = data.replace(/\n(.{8})/g, '\n');
- $scope.stdout = data;
+ function initView() {
+ ContainerService.container($transition$.params().id)
+ .then(function success(data) {
+ $scope.container = data;
+ startLogPolling();
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to retrieve container information');
});
}
- // initial call
- getLogs();
- var logIntervalId = window.setInterval(getLogs, 5000);
-
- $scope.$on('$destroy', function () {
- // clearing interval when view changes
- clearInterval(logIntervalId);
- });
-
- $scope.toggleTimestampsOut = function () {
- getLogsStdout();
- };
-
- $scope.toggleTimestampsErr = function () {
- getLogsStderr();
- };
+ initView();
}]);
diff --git a/app/docker/views/containers/logs/containerlogs.html b/app/docker/views/containers/logs/containerlogs.html
index 86ce31b13..48d0ef68e 100644
--- a/app/docker/views/containers/logs/containerlogs.html
+++ b/app/docker/views/containers/logs/containerlogs.html
@@ -5,50 +5,6 @@
-
-
-
-
-
-
-
- {{ container.Name|trimcontainername }}
-
-
-
-
-
-
-
-
-
+
diff --git a/app/docker/views/images/build/buildImageController.js b/app/docker/views/images/build/buildImageController.js
new file mode 100644
index 000000000..4b28b4492
--- /dev/null
+++ b/app/docker/views/images/build/buildImageController.js
@@ -0,0 +1,90 @@
+angular.module('portainer.docker')
+.controller('BuildImageController', ['$scope', '$state', 'BuildService', 'Notifications',
+function ($scope, $state, BuildService, Notifications) {
+
+ $scope.state = {
+ BuildType: 'editor',
+ actionInProgress: false,
+ activeTab: 0
+ };
+
+ $scope.formValues = {
+ ImageNames: [{ Name: '' }],
+ UploadFile: null,
+ DockerFileContent: '',
+ URL: '',
+ Path: 'Dockerfile'
+ };
+
+ $scope.addImageName = function() {
+ $scope.formValues.ImageNames.push({ Name: '' });
+ };
+
+ $scope.removeImageName = function(index) {
+ $scope.formValues.ImageNames.splice(index, 1);
+ };
+
+ function buildImageBasedOnBuildType(method, names) {
+ var buildType = $scope.state.BuildType;
+ var dockerfilePath = $scope.formValues.Path;
+
+ if (buildType === 'upload') {
+ var file = $scope.formValues.UploadFile;
+ return BuildService.buildImageFromUpload(names, file, dockerfilePath);
+ } else if (buildType === 'url') {
+ var URL = $scope.formValues.URL;
+ return BuildService.buildImageFromURL(names, URL, dockerfilePath);
+ } else {
+ var dockerfileContent = $scope.formValues.DockerFileContent;
+ return BuildService.buildImageFromDockerfileContent(names, dockerfileContent);
+ }
+ }
+
+ $scope.buildImage = function() {
+ var buildType = $scope.state.BuildType;
+
+ if (buildType === 'editor' && $scope.formValues.DockerFileContent === '') {
+ $scope.state.formValidationError = 'Dockerfile content must not be empty';
+ return;
+ }
+
+ $scope.state.actionInProgress = true;
+
+ var imageNames = $scope.formValues.ImageNames.filter(function filterNull(x) {
+ return x.Name;
+ }).map(function getNames(x) {
+ return x.Name;
+ });
+
+ buildImageBasedOnBuildType(buildType, imageNames)
+ .then(function success(data) {
+ $scope.buildLogs = data.buildLogs;
+ $scope.state.activeTab = 1;
+ if (data.hasError) {
+ Notifications.error('An error occured during build', { msg: 'Please check build logs output' });
+ } else {
+ Notifications.success('Image successfully built');
+ }
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to build image');
+ })
+ .finally(function final() {
+ $scope.state.actionInProgress = false;
+ });
+ };
+
+ $scope.validImageNames = function() {
+ for (var i = 0; i < $scope.formValues.ImageNames.length; i++) {
+ var item = $scope.formValues.ImageNames[i];
+ if (item.Name !== '') {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ $scope.editorUpdate = function(cm) {
+ $scope.formValues.DockerFileContent = cm.getValue();
+ };
+}]);
diff --git a/app/docker/views/images/build/buildimage.html b/app/docker/views/images/build/buildimage.html
new file mode 100644
index 000000000..cf930f0a5
--- /dev/null
+++ b/app/docker/views/images/build/buildimage.html
@@ -0,0 +1,231 @@
+
+
+
+ Images > Build image
+
+
+
+
+
+
+
+
+
+
+ Builder
+
+
+
+
+
+ Output
+
+
+
+ No build output available.
+
+
+
+
+
+
+
diff --git a/app/docker/views/services/edit/includes/tasks.html b/app/docker/views/services/edit/includes/tasks.html
index 2cf7fdbaf..369276b9a 100644
--- a/app/docker/views/services/edit/includes/tasks.html
+++ b/app/docker/views/services/edit/includes/tasks.html
@@ -6,5 +6,6 @@
nodes="nodes"
show-text-filter="true"
show-slot-column="service.Mode !== 'global'"
+ show-logs-button="applicationState.endpoint.apiVersion >= 1.30"
>
diff --git a/app/docker/views/services/edit/service.html b/app/docker/views/services/edit/service.html
index 059da3dca..c2d86925c 100644
--- a/app/docker/views/services/edit/service.html
+++ b/app/docker/views/services/edit/service.html
@@ -73,7 +73,7 @@
|
- Service logs
+ Service logs
|
+
+ Task logs |
+
diff --git a/app/docker/views/tasks/edit/taskController.js b/app/docker/views/tasks/edit/taskController.js
index adc3d032c..4b4ec6616 100644
--- a/app/docker/views/tasks/edit/taskController.js
+++ b/app/docker/views/tasks/edit/taskController.js
@@ -1,16 +1,16 @@
angular.module('portainer.docker')
-.controller('TaskController', ['$scope', '$transition$', 'TaskService', 'Service', 'Notifications',
-function ($scope, $transition$, TaskService, Service, Notifications) {
+.controller('TaskController', ['$scope', '$transition$', 'TaskService', 'ServiceService', 'Notifications',
+function ($scope, $transition$, TaskService, ServiceService, Notifications) {
function initView() {
TaskService.task($transition$.params().id)
.then(function success(data) {
var task = data;
$scope.task = task;
- return Service.get({ id: task.ServiceId }).$promise;
+ return ServiceService.service(task.ServiceId);
})
.then(function success(data) {
- var service = new ServiceViewModel(data);
+ var service = data;
$scope.service = service;
})
.catch(function error(err) {
diff --git a/app/docker/views/tasks/logs/taskLogsController.js b/app/docker/views/tasks/logs/taskLogsController.js
new file mode 100644
index 000000000..e578c5d7a
--- /dev/null
+++ b/app/docker/views/tasks/logs/taskLogsController.js
@@ -0,0 +1,73 @@
+angular.module('portainer.docker')
+.controller('TaskLogsController', ['$scope', '$transition$', '$interval', 'TaskService', 'ServiceService', 'Notifications',
+function ($scope, $transition$, $interval, TaskService, ServiceService, Notifications) {
+ $scope.state = {
+ refreshRate: 3,
+ lineCount: 2000
+ };
+
+ $scope.changeLogCollection = function(logCollectionStatus) {
+ if (!logCollectionStatus) {
+ stopRepeater();
+ } else {
+ setUpdateRepeater();
+ }
+ };
+
+ $scope.$on('$destroy', function() {
+ stopRepeater();
+ });
+
+ function stopRepeater() {
+ var repeater = $scope.repeater;
+ if (angular.isDefined(repeater)) {
+ $interval.cancel(repeater);
+ repeater = null;
+ }
+ }
+
+ function setUpdateRepeater() {
+ var refreshRate = $scope.state.refreshRate;
+ $scope.repeater = $interval(function() {
+ TaskService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
+ .then(function success(data) {
+ $scope.logs = data;
+ })
+ .catch(function error(err) {
+ stopRepeater();
+ Notifications.error('Failure', err, 'Unable to retrieve task logs');
+ });
+ }, refreshRate * 1000);
+ }
+
+ function startLogPolling() {
+ TaskService.logs($transition$.params().id, 1, 1, 0, $scope.state.lineCount)
+ .then(function success(data) {
+ $scope.logs = data;
+ setUpdateRepeater();
+ })
+ .catch(function error(err) {
+ stopRepeater();
+ Notifications.error('Failure', err, 'Unable to retrieve task logs');
+ });
+ }
+
+ function initView() {
+ TaskService.task($transition$.params().id)
+ .then(function success(data) {
+ var task = data;
+ $scope.task = task;
+ return ServiceService.service(task.ServiceId);
+ })
+ .then(function success(data) {
+ var service = data;
+ $scope.service = service;
+ startLogPolling();
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to retrieve task details');
+ });
+ }
+
+ initView();
+}]);
diff --git a/app/docker/views/tasks/logs/tasklogs.html b/app/docker/views/tasks/logs/tasklogs.html
new file mode 100644
index 000000000..d86db0f92
--- /dev/null
+++ b/app/docker/views/tasks/logs/tasklogs.html
@@ -0,0 +1,10 @@
+
+
+
+ Services > {{ service.Name }} > {{ task.Id }} > Logs
+
+
+
+
diff --git a/app/docker/views/templates/templatesController.js b/app/docker/views/templates/templatesController.js
index c6128d456..d634b940c 100644
--- a/app/docker/views/templates/templatesController.js
+++ b/app/docker/views/templates/templatesController.js
@@ -178,6 +178,12 @@ function ($scope, $q, $state, $transition$, $anchorScroll, $filter, ContainerSer
$scope.formValues.network = _.find($scope.availableNetworks, function(o) { return o.Name === 'bridge'; });
}
+ if (selectedTemplate.Name) {
+ $scope.formValues.name = selectedTemplate.Name;
+ } else {
+ $scope.formValues.name = '';
+ }
+
$anchorScroll('view-top');
}
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/components/code-editor/code-editor.js b/app/portainer/components/code-editor/code-editor.js
new file mode 100644
index 000000000..97d7d583b
--- /dev/null
+++ b/app/portainer/components/code-editor/code-editor.js
@@ -0,0 +1,12 @@
+angular.module('portainer.app').component('codeEditor', {
+ templateUrl: 'app/portainer/components/code-editor/codeEditor.html',
+ controller: 'CodeEditorController',
+ bindings: {
+ identifier: '@',
+ placeholder: '@',
+ yml: '<',
+ readOnly: '<',
+ onChange: '<',
+ value: '<'
+ }
+});
diff --git a/app/portainer/components/code-editor/codeEditor.html b/app/portainer/components/code-editor/codeEditor.html
new file mode 100644
index 000000000..54d2a0bbf
--- /dev/null
+++ b/app/portainer/components/code-editor/codeEditor.html
@@ -0,0 +1 @@
+
diff --git a/app/portainer/components/code-editor/codeEditorController.js b/app/portainer/components/code-editor/codeEditorController.js
new file mode 100644
index 000000000..82f022ba6
--- /dev/null
+++ b/app/portainer/components/code-editor/codeEditorController.js
@@ -0,0 +1,18 @@
+angular.module('portainer.app')
+.controller('CodeEditorController', ['$document', 'CodeMirrorService',
+function ($document, CodeMirrorService) {
+ var ctrl = this;
+
+ this.$onInit = function() {
+ $document.ready(function() {
+ var editorElement = $document[0].getElementById(ctrl.identifier);
+ ctrl.editor = CodeMirrorService.applyCodeMirrorOnElement(editorElement, ctrl.yml, ctrl.readOnly);
+ if (ctrl.onChange) {
+ ctrl.editor.on('change', ctrl.onChange);
+ }
+ if (ctrl.value) {
+ ctrl.editor.setValue(ctrl.value);
+ }
+ });
+ };
+}]);
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/codeMirror.js b/app/portainer/services/codeMirror.js
index 38bb10259..a6e43f11e 100644
--- a/app/portainer/services/codeMirror.js
+++ b/app/portainer/services/codeMirror.js
@@ -2,6 +2,8 @@ angular.module('portainer.app')
.factory('CodeMirrorService', function CodeMirrorService() {
'use strict';
+ var service = {};
+
var codeMirrorGenericOptions = {
lineNumbers: true
};
@@ -12,8 +14,6 @@ angular.module('portainer.app')
lint: true
};
- var service = {};
-
service.applyCodeMirrorOnElement = function(element, yamlLint, readOnly) {
var options = angular.copy(codeMirrorGenericOptions);
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/fileUpload.js b/app/portainer/services/fileUpload.js
index 5ac738884..bcfb73328 100644
--- a/app/portainer/services/fileUpload.js
+++ b/app/portainer/services/fileUpload.js
@@ -8,6 +8,26 @@ angular.module('portainer.app')
return Upload.upload({ url: url, data: { file: file }});
}
+ service.buildImage = function(names, file, path) {
+ var endpointID = EndpointProvider.endpointID();
+ Upload.setDefaults({ ngfMinSize: 10 });
+ return Upload.http({
+ url: 'api/endpoints/' + endpointID + '/docker/build',
+ headers : {
+ 'Content-Type': file.type
+ },
+ data: file,
+ params: {
+ t: names,
+ dockerfile: path
+ },
+ ignoreLoadingBar: true,
+ transformResponse: function(data, headers) {
+ return jsonObjectsToArrayHandler(data);
+ }
+ });
+ };
+
service.createStack = function(stackName, swarmId, file, env) {
var endpointID = EndpointProvider.endpointID();
return Upload.upload({
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..3b9d843dd 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,23 @@ angular.module('portainer.app')
return deferred.promise;
};
- manager.updateEndpointState = function(loading) {
+
+ function assignExtensions(endpointExtensions) {
+ 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 +137,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..5c8b76e43 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
@@ -30,10 +30,26 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
+ var endpointId;
$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) {
+ endpointId = data.Id;
+ var currentEndpointId = EndpointProvider.endpointID();
+ EndpointProvider.setEndpointID(endpointId);
+ ExtensionManager.initEndpointExtensions(endpointId)
+ .then(function success(data) {
+ Notifications.success('Endpoint created', name);
+ $state.reload();
+ })
+ .catch(function error(err) {
+ Notifications.error('Failure', err, 'Unable to create endpoint');
+ EndpointService.deleteEndpoint(endpointId);
+ })
+ .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/registries/edit/registry.html b/app/portainer/views/registries/edit/registry.html
index 24f519ac9..9e794548c 100644
--- a/app/portainer/views/registries/edit/registry.html
+++ b/app/portainer/views/registries/edit/registry.html
@@ -56,7 +56,7 @@
@@ -64,7 +64,7 @@