diff --git a/.gitignore b/.gitignore index aef04ade6..42c695629 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ node_modules bower_components dist portainer-checksum.txt -api/cmd/portainer/portainer-* +api/cmd/portainer/portainer* diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 2f02cd12a..d0321db21 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -1,8 +1,9 @@ package bolt import ( - "github.com/boltdb/bolt" "time" + + "github.com/boltdb/bolt" ) // Store defines the implementation of portainer.DataStore using @@ -12,23 +13,28 @@ type Store struct { Path string // Services - UserService *UserService + UserService *UserService + EndpointService *EndpointService db *bolt.DB } const ( - databaseFileName = "portainer.db" - userBucketName = "users" + databaseFileName = "portainer.db" + userBucketName = "users" + endpointBucketName = "endpoints" + activeEndpointBucketName = "activeEndpoint" ) // NewStore initializes a new Store and the associated services func NewStore(storePath string) *Store { store := &Store{ - Path: storePath, - UserService: &UserService{}, + Path: storePath, + UserService: &UserService{}, + EndpointService: &EndpointService{}, } store.UserService.store = store + store.EndpointService.store = store return store } @@ -45,6 +51,14 @@ func (store *Store) Open() error { if err != nil { return err } + _, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName)) + if err != nil { + return err + } + _, err = tx.CreateBucketIfNotExists([]byte(activeEndpointBucketName)) + if err != nil { + return err + } return nil }) } diff --git a/api/bolt/endpoint_service.go b/api/bolt/endpoint_service.go new file mode 100644 index 000000000..9046bf30f --- /dev/null +++ b/api/bolt/endpoint_service.go @@ -0,0 +1,162 @@ +package bolt + +import ( + "github.com/portainer/portainer" + "github.com/portainer/portainer/bolt/internal" + + "github.com/boltdb/bolt" +) + +// EndpointService represents a service for managing users. +type EndpointService struct { + store *Store +} + +const ( + activeEndpointID = 0 +) + +// Endpoint returns an endpoint by ID. +func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) { + var data []byte + err := service.store.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(endpointBucketName)) + value := bucket.Get(internal.Itob(int(ID))) + if value == nil { + return portainer.ErrEndpointNotFound + } + + data = make([]byte, len(value)) + copy(data, value) + return nil + }) + if err != nil { + return nil, err + } + + var endpoint portainer.Endpoint + err = internal.UnmarshalEndpoint(data, &endpoint) + if err != nil { + return nil, err + } + return &endpoint, nil +} + +// Endpoints return an array containing all the endpoints. +func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) { + var endpoints []portainer.Endpoint + err := service.store.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(endpointBucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var endpoint portainer.Endpoint + err := internal.UnmarshalEndpoint(v, &endpoint) + if err != nil { + return err + } + endpoints = append(endpoints, endpoint) + } + + return nil + }) + if err != nil { + return nil, err + } + + return endpoints, nil +} + +// CreateEndpoint assign an ID to a new endpoint and saves it. +func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(endpointBucketName)) + + id, _ := bucket.NextSequence() + endpoint.ID = portainer.EndpointID(id) + + data, err := internal.MarshalEndpoint(endpoint) + if err != nil { + return err + } + + err = bucket.Put(internal.Itob(int(endpoint.ID)), data) + if err != nil { + return err + } + return nil + }) +} + +// UpdateEndpoint updates an endpoint. +func (service *EndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error { + data, err := internal.MarshalEndpoint(endpoint) + if err != nil { + return err + } + + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(endpointBucketName)) + err = bucket.Put(internal.Itob(int(ID)), data) + if err != nil { + return err + } + return nil + }) +} + +// DeleteEndpoint deletes an endpoint. +func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(endpointBucketName)) + err := bucket.Delete(internal.Itob(int(ID))) + if err != nil { + return err + } + return nil + }) +} + +// GetActive returns the active endpoint. +func (service *EndpointService) GetActive() (*portainer.Endpoint, error) { + var data []byte + err := service.store.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(activeEndpointBucketName)) + value := bucket.Get(internal.Itob(activeEndpointID)) + if value == nil { + return portainer.ErrEndpointNotFound + } + + data = make([]byte, len(value)) + copy(data, value) + return nil + }) + if err != nil { + return nil, err + } + + var endpoint portainer.Endpoint + err = internal.UnmarshalEndpoint(data, &endpoint) + if err != nil { + return nil, err + } + return &endpoint, nil +} + +// SetActive saves an endpoint as active. +func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(activeEndpointBucketName)) + + data, err := internal.MarshalEndpoint(endpoint) + if err != nil { + return err + } + + err = bucket.Put(internal.Itob(activeEndpointID), data) + if err != nil { + return err + } + return nil + }) +} diff --git a/api/bolt/internal/internal.go b/api/bolt/internal/internal.go index db11bb9f4..e9d6416eb 100644 --- a/api/bolt/internal/internal.go +++ b/api/bolt/internal/internal.go @@ -3,6 +3,7 @@ package internal import ( "github.com/portainer/portainer" + "encoding/binary" "encoding/json" ) @@ -15,3 +16,22 @@ func MarshalUser(user *portainer.User) ([]byte, error) { func UnmarshalUser(data []byte, user *portainer.User) error { return json.Unmarshal(data, user) } + +// MarshalEndpoint encodes an endpoint to binary format. +func MarshalEndpoint(endpoint *portainer.Endpoint) ([]byte, error) { + return json.Marshal(endpoint) +} + +// UnmarshalEndpoint decodes an endpoint from a binary data. +func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error { + return json.Unmarshal(data, endpoint) +} + +// Itob returns an 8-byte big endian representation of v. +// This function is typically used for encoding integer IDs to byte slices +// so that they can be used as BoltDB keys. +func Itob(v int) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, uint64(v)) + return b +} diff --git a/api/cli/cli.go b/api/cli/cli.go index 6dcea483a..ccd9c1f9a 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -3,9 +3,10 @@ package cli import ( "github.com/portainer/portainer" - "gopkg.in/alecthomas/kingpin.v2" "os" "strings" + + "gopkg.in/alecthomas/kingpin.v2" ) // Service implements the CLIService interface @@ -21,13 +22,12 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { kingpin.Version(version) flags := &portainer.CLIFlags{ + Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(), + Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(), + Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')), Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String(), Assets: kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String(), - Endpoint: kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String(), - Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')), - Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(), - Swarm: kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool(), Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String(), TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default("false").Bool(), TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String(), @@ -41,17 +41,19 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { // ValidateFlags validates the values of the flags. func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { - if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") { - return errInvalidEnpointProtocol - } + if *flags.Endpoint != "" { + if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") { + return errInvalidEnpointProtocol + } - if strings.HasPrefix(*flags.Endpoint, "unix://") { - socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://") - if _, err := os.Stat(socketPath); err != nil { - if os.IsNotExist(err) { - return errSocketNotFound + if strings.HasPrefix(*flags.Endpoint, "unix://") { + socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://") + if _, err := os.Stat(socketPath); err != nil { + if os.IsNotExist(err) { + return errSocketNotFound + } + return err } - return err } } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 48cb6fa72..c11f9b34f 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -5,6 +5,7 @@ import ( "github.com/portainer/portainer/bolt" "github.com/portainer/portainer/cli" "github.com/portainer/portainer/crypto" + "github.com/portainer/portainer/file" "github.com/portainer/portainer/http" "github.com/portainer/portainer/jwt" @@ -24,7 +25,6 @@ func main() { } settings := &portainer.Settings{ - Swarm: *flags.Swarm, HiddenLabels: *flags.Labels, Logo: *flags.Logo, } @@ -41,25 +41,47 @@ func main() { log.Fatal(err) } + fileService, err := file.NewService(*flags.Data) + if err != nil { + log.Fatal(err) + } + var cryptoService portainer.CryptoService = &crypto.Service{} - endpointConfiguration := &portainer.EndpointConfiguration{ - Endpoint: *flags.Endpoint, - TLS: *flags.TLSVerify, - TLSCACertPath: *flags.TLSCacert, - TLSCertPath: *flags.TLSCert, - TLSKeyPath: *flags.TLSKey, + // Initialize the active endpoint from the CLI only if there is no + // active endpoint defined yet. + var activeEndpoint *portainer.Endpoint + if *flags.Endpoint != "" { + activeEndpoint, err = store.EndpointService.GetActive() + if err == portainer.ErrEndpointNotFound { + activeEndpoint = &portainer.Endpoint{ + Name: "primary", + URL: *flags.Endpoint, + TLS: *flags.TLSVerify, + TLSCACertPath: *flags.TLSCacert, + TLSCertPath: *flags.TLSCert, + TLSKeyPath: *flags.TLSKey, + } + err = store.EndpointService.CreateEndpoint(activeEndpoint) + if err != nil { + log.Fatal(err) + } + } else if err != nil { + log.Fatal(err) + } } var server portainer.Server = &http.Server{ - BindAddress: *flags.Addr, - AssetsPath: *flags.Assets, - Settings: settings, - TemplatesURL: *flags.Templates, - UserService: store.UserService, - CryptoService: cryptoService, - JWTService: jwtService, - EndpointConfig: endpointConfiguration, + BindAddress: *flags.Addr, + AssetsPath: *flags.Assets, + Settings: settings, + TemplatesURL: *flags.Templates, + UserService: store.UserService, + EndpointService: store.EndpointService, + CryptoService: cryptoService, + JWTService: jwtService, + FileService: fileService, + ActiveEndpoint: activeEndpoint, } log.Printf("Starting Portainer on %s", *flags.Addr) diff --git a/api/errors.go b/api/errors.go index fa59de7f2..8fcd44758 100644 --- a/api/errors.go +++ b/api/errors.go @@ -10,6 +10,12 @@ const ( ErrUserNotFound = Error("User not found") ) +// Endpoint errors. +const ( + ErrEndpointNotFound = Error("Endpoint not found") + ErrNoActiveEndpoint = Error("Undefined Docker endpoint") +) + // Crypto errors. const ( ErrCryptoHashFailure = Error("Unable to hash data") @@ -21,6 +27,11 @@ const ( ErrInvalidJWTToken = Error("Invalid JWT token") ) +// File errors. +const ( + ErrUndefinedTLSFileType = Error("Undefined TLS file type") +) + // Error represents an application error. type Error string diff --git a/api/file/file.go b/api/file/file.go new file mode 100644 index 000000000..cd76acec9 --- /dev/null +++ b/api/file/file.go @@ -0,0 +1,125 @@ +package file + +import ( + "strconv" + + "github.com/portainer/portainer" + + "io" + "os" + "path" +) + +const ( + // TLSStorePath represents the subfolder where TLS files are stored in the file store folder. + TLSStorePath = "tls" + // TLSCACertFile represents the name on disk for a TLS CA file. + TLSCACertFile = "ca.pem" + // TLSCertFile represents the name on disk for a TLS certificate file. + TLSCertFile = "cert.pem" + // TLSKeyFile represents the name on disk for a TLS key file. + TLSKeyFile = "key.pem" +) + +// Service represents a service for managing files. +type Service struct { + fileStorePath string +} + +// NewService initializes a new service. +func NewService(fileStorePath string) (*Service, error) { + service := &Service{ + fileStorePath: fileStorePath, + } + + err := service.createFolderInStoreIfNotExist(TLSStorePath) + if err != nil { + return nil, err + } + + return service, nil +} + +// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r. +func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error { + ID := strconv.Itoa(int(endpointID)) + endpointStorePath := path.Join(TLSStorePath, ID) + err := service.createFolderInStoreIfNotExist(endpointStorePath) + if err != nil { + return err + } + + var fileName string + switch fileType { + case portainer.TLSFileCA: + fileName = TLSCACertFile + case portainer.TLSFileCert: + fileName = TLSCertFile + case portainer.TLSFileKey: + fileName = TLSKeyFile + default: + return portainer.ErrUndefinedTLSFileType + } + + tlsFilePath := path.Join(endpointStorePath, fileName) + err = service.createFileInStore(tlsFilePath, r) + if err != nil { + return err + } + return nil +} + +// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint. +func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType) (string, error) { + var fileName string + switch fileType { + case portainer.TLSFileCA: + fileName = TLSCACertFile + case portainer.TLSFileCert: + fileName = TLSCertFile + case portainer.TLSFileKey: + fileName = TLSKeyFile + default: + return "", portainer.ErrUndefinedTLSFileType + } + ID := strconv.Itoa(int(endpointID)) + return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil +} + +// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint. +func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error { + ID := strconv.Itoa(int(endpointID)) + endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID) + err := os.RemoveAll(endpointPath) + if err != nil { + return err + } + return nil +} + +// createFolderInStoreIfNotExist creates a new folder in the file store if it doesn't exists on the file system. +func (service *Service) createFolderInStoreIfNotExist(name string) error { + path := path.Join(service.fileStorePath, name) + _, err := os.Stat(path) + if os.IsNotExist(err) { + os.Mkdir(path, 0600) + } else if err != nil { + return err + } + return nil +} + +// createFile creates a new file in the file store with the content from r. +func (service *Service) createFileInStore(filePath string, r io.Reader) error { + path := path.Join(service.fileStorePath, filePath) + out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer out.Close() + _, err = io.Copy(out, r) + if err != nil { + return err + } + return nil +} diff --git a/api/http/auth_handler.go b/api/http/auth_handler.go index e63412a15..29c19201d 100644 --- a/api/http/auth_handler.go +++ b/api/http/auth_handler.go @@ -4,11 +4,12 @@ import ( "github.com/portainer/portainer" "encoding/json" - "github.com/asaskevich/govalidator" - "github.com/gorilla/mux" "log" "net/http" "os" + + "github.com/asaskevich/govalidator" + "github.com/gorilla/mux" ) // AuthHandler represents an HTTP API handler for managing authentication. @@ -27,7 +28,7 @@ const ( ErrInvalidCredentials = portainer.Error("Invalid credentials") ) -// NewAuthHandler returns a new instance of DialHandler. +// NewAuthHandler returns a new instance of AuthHandler. func NewAuthHandler() *AuthHandler { h := &AuthHandler{ Router: mux.NewRouter(), @@ -38,8 +39,8 @@ func NewAuthHandler() *AuthHandler { } func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - handleNotAllowed(w, []string{"POST"}) + if r.Method != http.MethodPost { + handleNotAllowed(w, []string{http.MethodPost}) return } diff --git a/api/http/docker_handler.go b/api/http/docker_handler.go index f15b4386e..4894b797c 100644 --- a/api/http/docker_handler.go +++ b/api/http/docker_handler.go @@ -3,7 +3,6 @@ package http import ( "github.com/portainer/portainer" - "github.com/gorilla/mux" "io" "log" "net" @@ -12,6 +11,8 @@ import ( "net/url" "os" "strings" + + "github.com/gorilla/mux" ) // DockerHandler represents an HTTP API handler for proxying requests to the Docker API. @@ -36,18 +37,22 @@ func NewDockerHandler(middleWareService *middleWareService) *DockerHandler { } func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) { - handler.proxy.ServeHTTP(w, r) + if handler.proxy != nil { + handler.proxy.ServeHTTP(w, r) + } else { + Error(w, portainer.ErrNoActiveEndpoint, http.StatusNotFound, handler.Logger) + } } -func (handler *DockerHandler) setupProxy(config *portainer.EndpointConfiguration) error { +func (handler *DockerHandler) setupProxy(endpoint *portainer.Endpoint) error { var proxy http.Handler - endpointURL, err := url.Parse(config.Endpoint) + endpointURL, err := url.Parse(endpoint.URL) if err != nil { return err } if endpointURL.Scheme == "tcp" { - if config.TLS { - proxy, err = newHTTPSProxy(endpointURL, config) + if endpoint.TLS { + proxy, err = newHTTPSProxy(endpointURL, endpoint) if err != nil { return err } @@ -65,7 +70,6 @@ func (handler *DockerHandler) setupProxy(config *portainer.EndpointConfiguration // singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go // included here for use in NewSingleHostReverseProxyWithHostHeader // because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go - func singleJoiningSlash(a, b string) string { aslash := strings.HasSuffix(a, "/") bslash := strings.HasPrefix(b, "/") @@ -81,7 +85,6 @@ func singleJoiningSlash(a, b string) string { // NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy // from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host // HTTP header, which NewSingleHostReverseProxy deliberately preserves - func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy { targetQuery := target.RawQuery director := func(req *http.Request) { @@ -107,10 +110,10 @@ func newHTTPProxy(u *url.URL) http.Handler { return NewSingleHostReverseProxyWithHostHeader(u) } -func newHTTPSProxy(u *url.URL, endpointConfig *portainer.EndpointConfiguration) (http.Handler, error) { +func newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) { u.Scheme = "https" proxy := NewSingleHostReverseProxyWithHostHeader(u) - config, err := createTLSConfiguration(endpointConfig.TLSCACertPath, endpointConfig.TLSCertPath, endpointConfig.TLSKeyPath) + config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath) if err != nil { return nil, err } diff --git a/api/http/endpoint_handler.go b/api/http/endpoint_handler.go new file mode 100644 index 000000000..42a77cbec --- /dev/null +++ b/api/http/endpoint_handler.go @@ -0,0 +1,294 @@ +package http + +import ( + "github.com/portainer/portainer" + + "encoding/json" + "log" + "net/http" + "os" + "strconv" + + "github.com/asaskevich/govalidator" + "github.com/gorilla/mux" +) + +// EndpointHandler represents an HTTP API handler for managing Docker endpoints. +type EndpointHandler struct { + *mux.Router + Logger *log.Logger + EndpointService portainer.EndpointService + FileService portainer.FileService + server *Server + middleWareService *middleWareService +} + +// NewEndpointHandler returns a new instance of EndpointHandler. +func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler { + h := &EndpointHandler{ + Router: mux.NewRouter(), + Logger: log.New(os.Stderr, "", log.LstdFlags), + middleWareService: middleWareService, + } + h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.handlePostEndpoints(w, r) + }))).Methods(http.MethodPost) + h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.handleGetEndpoints(w, r) + }))).Methods(http.MethodGet) + h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.handleGetEndpoint(w, r) + }))).Methods(http.MethodGet) + h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.handlePutEndpoint(w, r) + }))).Methods(http.MethodPut) + h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.handleDeleteEndpoint(w, r) + }))).Methods(http.MethodDelete) + h.Handle("/endpoints/{id}/active", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.handlePostEndpoint(w, r) + }))).Methods(http.MethodPost) + return h +} + +// handleGetEndpoints handles GET requests on /endpoints +func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) { + endpoints, err := handler.EndpointService.Endpoints() + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + encodeJSON(w, endpoints, handler.Logger) +} + +// handlePostEndpoints handles POST requests on /endpoints +// if the active URL parameter is specified, will also define the new endpoint as the active endpoint. +// /endpoints(?active=true|false) +func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) { + var req postEndpointsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) + return + } + + _, err := govalidator.ValidateStruct(req) + if err != nil { + Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + + endpoint := &portainer.Endpoint{ + Name: req.Name, + URL: req.URL, + TLS: req.TLS, + } + + err = handler.EndpointService.CreateEndpoint(endpoint) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + if req.TLS { + caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA) + endpoint.TLSCACertPath = caCertPath + certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert) + endpoint.TLSCertPath = certPath + keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey) + endpoint.TLSKeyPath = keyPath + err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } + + activeEndpointParameter := r.FormValue("active") + if activeEndpointParameter != "" { + active, err := strconv.ParseBool(activeEndpointParameter) + if err != nil { + Error(w, err, http.StatusBadRequest, handler.Logger) + return + } + if active == true { + err = handler.server.updateActiveEndpoint(endpoint) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } + } + + encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger) +} + +type postEndpointsRequest struct { + Name string `valid:"required"` + URL string `valid:"required"` + TLS bool +} + +type postEndpointsResponse struct { + ID int `json:"Id"` +} + +// handleGetEndpoint handles GET requests on /endpoints/:id +// GET /endpoints/0 returns active endpoint +func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + endpointID, err := strconv.Atoi(id) + if err != nil { + Error(w, err, http.StatusBadRequest, handler.Logger) + return + } + + var endpoint *portainer.Endpoint + if id == "0" { + endpoint, err = handler.EndpointService.GetActive() + if err == portainer.ErrEndpointNotFound { + Error(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + if handler.server.ActiveEndpoint == nil { + err = handler.server.updateActiveEndpoint(endpoint) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } + } else { + endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrEndpointNotFound { + Error(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } + + encodeJSON(w, endpoint, handler.Logger) +} + +// handlePostEndpoint handles POST requests on /endpoints/:id/active +func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + endpointID, err := strconv.Atoi(id) + if err != nil { + Error(w, err, http.StatusBadRequest, handler.Logger) + return + } + + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrEndpointNotFound { + Error(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + err = handler.server.updateActiveEndpoint(endpoint) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + } +} + +// handlePutEndpoint handles PUT requests on /endpoints/:id +func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + endpointID, err := strconv.Atoi(id) + if err != nil { + Error(w, err, http.StatusBadRequest, handler.Logger) + return + } + + var req putEndpointsRequest + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) + return + } + + _, err = govalidator.ValidateStruct(req) + if err != nil { + Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger) + return + } + + endpoint := &portainer.Endpoint{ + ID: portainer.EndpointID(endpointID), + Name: req.Name, + URL: req.URL, + TLS: req.TLS, + } + + if req.TLS { + caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA) + endpoint.TLSCACertPath = caCertPath + certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert) + endpoint.TLSCertPath = certPath + keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey) + endpoint.TLSKeyPath = keyPath + } else { + err = handler.FileService.DeleteTLSFiles(endpoint.ID) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + } + + err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } +} + +type putEndpointsRequest struct { + Name string `valid:"required"` + URL string `valid:"required"` + TLS bool +} + +// handleDeleteEndpoint handles DELETE requests on /endpoints/:id +func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + + endpointID, err := strconv.Atoi(id) + if err != nil { + Error(w, err, http.StatusBadRequest, handler.Logger) + return + } + + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrEndpointNotFound { + Error(w, err, http.StatusNotFound, handler.Logger) + return + } else if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID)) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + if endpoint.TLS { + err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID)) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + } + } +} diff --git a/api/http/handler.go b/api/http/handler.go index 3bcfdff54..ca4b15ede 100644 --- a/api/http/handler.go +++ b/api/http/handler.go @@ -13,10 +13,12 @@ import ( type Handler struct { AuthHandler *AuthHandler UserHandler *UserHandler + EndpointHandler *EndpointHandler SettingsHandler *SettingsHandler TemplatesHandler *TemplatesHandler DockerHandler *DockerHandler WebSocketHandler *WebSocketHandler + UploadHandler *UploadHandler FileHandler http.Handler } @@ -33,10 +35,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r) } else if strings.HasPrefix(r.URL.Path, "/api/users") { http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r) + } else if strings.HasPrefix(r.URL.Path, "/api/endpoints") { + http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) } else if strings.HasPrefix(r.URL.Path, "/api/settings") { http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r) } else if strings.HasPrefix(r.URL.Path, "/api/templates") { http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r) + } else if strings.HasPrefix(r.URL.Path, "/api/upload") { + http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r) } else if strings.HasPrefix(r.URL.Path, "/api/websocket") { http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r) } else if strings.HasPrefix(r.URL.Path, "/api/docker") { diff --git a/api/http/server.go b/api/http/server.go index 4f7d4efa1..cd376883b 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -8,14 +8,33 @@ import ( // Server implements the portainer.Server interface type Server struct { - BindAddress string - AssetsPath string - UserService portainer.UserService - CryptoService portainer.CryptoService - JWTService portainer.JWTService - Settings *portainer.Settings - TemplatesURL string - EndpointConfig *portainer.EndpointConfiguration + BindAddress string + AssetsPath string + UserService portainer.UserService + EndpointService portainer.EndpointService + CryptoService portainer.CryptoService + JWTService portainer.JWTService + FileService portainer.FileService + Settings *portainer.Settings + TemplatesURL string + ActiveEndpoint *portainer.Endpoint + Handler *Handler +} + +func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error { + if endpoint != nil { + server.ActiveEndpoint = endpoint + server.Handler.WebSocketHandler.endpoint = endpoint + err := server.Handler.DockerHandler.setupProxy(endpoint) + if err != nil { + return err + } + err = server.EndpointService.SetActive(endpoint) + if err != nil { + return err + } + } + return nil } // Start starts the HTTP server @@ -23,6 +42,7 @@ func (server *Server) Start() error { middleWareService := &middleWareService{ jwtService: server.JWTService, } + var authHandler = NewAuthHandler() authHandler.UserService = server.UserService authHandler.CryptoService = server.CryptoService @@ -35,19 +55,31 @@ func (server *Server) Start() error { var templatesHandler = NewTemplatesHandler(middleWareService) templatesHandler.templatesURL = server.TemplatesURL var dockerHandler = NewDockerHandler(middleWareService) - dockerHandler.setupProxy(server.EndpointConfig) var websocketHandler = NewWebSocketHandler() - websocketHandler.endpointConfiguration = server.EndpointConfig + // EndpointHandler requires a reference to the server to be able to update the active endpoint. + var endpointHandler = NewEndpointHandler(middleWareService) + endpointHandler.EndpointService = server.EndpointService + endpointHandler.FileService = server.FileService + endpointHandler.server = server + var uploadHandler = NewUploadHandler(middleWareService) + uploadHandler.FileService = server.FileService var fileHandler = http.FileServer(http.Dir(server.AssetsPath)) - handler := &Handler{ + server.Handler = &Handler{ AuthHandler: authHandler, UserHandler: userHandler, + EndpointHandler: endpointHandler, SettingsHandler: settingsHandler, TemplatesHandler: templatesHandler, DockerHandler: dockerHandler, WebSocketHandler: websocketHandler, FileHandler: fileHandler, + UploadHandler: uploadHandler, } - return http.ListenAndServe(server.BindAddress, handler) + err := server.updateActiveEndpoint(server.ActiveEndpoint) + if err != nil { + return err + } + + return http.ListenAndServe(server.BindAddress, server.Handler) } diff --git a/api/http/settings_handler.go b/api/http/settings_handler.go index 4768e1a05..fab77aaa7 100644 --- a/api/http/settings_handler.go +++ b/api/http/settings_handler.go @@ -3,10 +3,11 @@ package http import ( "github.com/portainer/portainer" - "github.com/gorilla/mux" "log" "net/http" "os" + + "github.com/gorilla/mux" ) // SettingsHandler represents an HTTP API handler for managing settings. @@ -30,8 +31,8 @@ func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler { // handleGetSettings handles GET requests on /settings func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - handleNotAllowed(w, []string{"GET"}) + if r.Method != http.MethodGet { + handleNotAllowed(w, []string{http.MethodGet}) return } diff --git a/api/http/templates_handler.go b/api/http/templates_handler.go index b690012fb..867891291 100644 --- a/api/http/templates_handler.go +++ b/api/http/templates_handler.go @@ -2,11 +2,12 @@ package http import ( "fmt" - "github.com/gorilla/mux" "io/ioutil" "log" "net/http" "os" + + "github.com/gorilla/mux" ) // TemplatesHandler represents an HTTP API handler for managing templates. @@ -32,8 +33,8 @@ func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler // handleGetTemplates handles GET requests on /templates func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - handleNotAllowed(w, []string{"GET"}) + if r.Method != http.MethodGet { + handleNotAllowed(w, []string{http.MethodGet}) return } diff --git a/api/http/upload_handler.go b/api/http/upload_handler.go new file mode 100644 index 000000000..24b992392 --- /dev/null +++ b/api/http/upload_handler.go @@ -0,0 +1,74 @@ +package http + +import ( + "github.com/portainer/portainer" + + "log" + "net/http" + "os" + "strconv" + + "github.com/gorilla/mux" +) + +// UploadHandler represents an HTTP API handler for managing file uploads. +type UploadHandler struct { + *mux.Router + Logger *log.Logger + FileService portainer.FileService + middleWareService *middleWareService +} + +// NewUploadHandler returns a new instance of UploadHandler. +func NewUploadHandler(middleWareService *middleWareService) *UploadHandler { + h := &UploadHandler{ + Router: mux.NewRouter(), + Logger: log.New(os.Stderr, "", log.LstdFlags), + middleWareService: middleWareService, + } + h.Handle("/upload/tls/{endpointID}/{certificate:(ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.handlePostUploadTLS(w, r) + }))) + return h +} + +func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + handleNotAllowed(w, []string{http.MethodPost}) + return + } + + vars := mux.Vars(r) + endpointID := vars["endpointID"] + certificate := vars["certificate"] + ID, err := strconv.Atoi(endpointID) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + file, _, err := r.FormFile("file") + defer file.Close() + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + return + } + + var fileType portainer.TLSFileType + switch certificate { + case "ca": + fileType = portainer.TLSFileCA + case "cert": + fileType = portainer.TLSFileCert + case "key": + fileType = portainer.TLSFileKey + default: + Error(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger) + return + } + + err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file) + if err != nil { + Error(w, err, http.StatusInternalServerError, handler.Logger) + } +} diff --git a/api/http/user_handler.go b/api/http/user_handler.go index 461130e76..b47cda7ca 100644 --- a/api/http/user_handler.go +++ b/api/http/user_handler.go @@ -4,11 +4,12 @@ import ( "github.com/portainer/portainer" "encoding/json" - "github.com/asaskevich/govalidator" - "github.com/gorilla/mux" "log" "net/http" "os" + + "github.com/asaskevich/govalidator" + "github.com/gorilla/mux" ) // UserHandler represents an HTTP API handler for managing users. @@ -32,10 +33,10 @@ func NewUserHandler(middleWareService *middleWareService) *UserHandler { }))) h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.handleGetUser(w, r) - }))).Methods("GET") + }))).Methods(http.MethodGet) h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.handlePutUser(w, r) - }))).Methods("PUT") + }))).Methods(http.MethodPut) h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h.handlePostUserPasswd(w, r) }))) @@ -46,8 +47,8 @@ func NewUserHandler(middleWareService *middleWareService) *UserHandler { // handlePostUsers handles POST requests on /users func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - handleNotAllowed(w, []string{"POST"}) + if r.Method != http.MethodPost { + handleNotAllowed(w, []string{http.MethodPost}) return } @@ -86,8 +87,8 @@ type postUsersRequest struct { // handlePostUserPasswd handles POST requests on /users/:username/passwd func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - handleNotAllowed(w, []string{"POST"}) + if r.Method != http.MethodPost { + handleNotAllowed(w, []string{http.MethodPost}) return } @@ -189,8 +190,8 @@ type putUserRequest struct { // handlePostAdminInit handles GET requests on /users/admin/check func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - handleNotAllowed(w, []string{"GET"}) + if r.Method != http.MethodGet { + handleNotAllowed(w, []string{http.MethodGet}) return } @@ -209,8 +210,8 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R // handlePostAdminInit handles POST requests on /users/admin/init func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - handleNotAllowed(w, []string{"POST"}) + if r.Method != http.MethodPost { + handleNotAllowed(w, []string{http.MethodPost}) return } diff --git a/api/http/websocket_handler.go b/api/http/websocket_handler.go index 365a1ccd1..c217404a0 100644 --- a/api/http/websocket_handler.go +++ b/api/http/websocket_handler.go @@ -7,8 +7,6 @@ import ( "crypto/tls" "encoding/json" "fmt" - "github.com/gorilla/mux" - "golang.org/x/net/websocket" "io" "log" "net" @@ -17,14 +15,17 @@ import ( "net/url" "os" "time" + + "github.com/gorilla/mux" + "golang.org/x/net/websocket" ) // WebSocketHandler represents an HTTP API handler for proxying requests to a web socket. type WebSocketHandler struct { *mux.Router - Logger *log.Logger - middleWareService *middleWareService - endpointConfiguration *portainer.EndpointConfiguration + Logger *log.Logger + middleWareService *middleWareService + endpoint *portainer.Endpoint } // NewWebSocketHandler returns a new instance of WebSocketHandler. @@ -42,7 +43,7 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) { execID := qry.Get("id") // Should not be managed here - endpoint, err := url.Parse(handler.endpointConfiguration.Endpoint) + endpoint, err := url.Parse(handler.endpoint.URL) if err != nil { log.Fatalf("Unable to parse endpoint URL: %s", err) return @@ -57,10 +58,10 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) { // Should not be managed here var tlsConfig *tls.Config - if handler.endpointConfiguration.TLS { - tlsConfig, err = createTLSConfiguration(handler.endpointConfiguration.TLSCACertPath, - handler.endpointConfiguration.TLSCertPath, - handler.endpointConfiguration.TLSKeyPath) + if handler.endpoint.TLS { + tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath, + handler.endpoint.TLSCertPath, + handler.endpoint.TLSKeyPath) if err != nil { log.Fatalf("Unable to create TLS configuration: %s", err) return diff --git a/api/portainer.go b/api/portainer.go index d9038c630..60703300f 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1,5 +1,9 @@ package portainer +import ( + "io" +) + type ( // Pair defines a key/value string pair Pair struct { @@ -15,7 +19,6 @@ type ( Endpoint *string Labels *[]Pair Logo *string - Swarm *bool Templates *string TLSVerify *bool TLSCacert *string @@ -25,15 +28,14 @@ type ( // Settings represents Portainer settings. Settings struct { - Swarm bool `json:"swarm"` HiddenLabels []Pair `json:"hiddenLabels"` Logo string `json:"logo"` } // User represent a user account. User struct { - Username string `json:"username"` - Password string `json:"password,omitempty"` + Username string `json:"Username"` + Password string `json:"Password,omitempty"` } // TokenData represents the data embedded in a JWT token. @@ -41,15 +43,25 @@ type ( Username string } - // EndpointConfiguration represents the data required to connect to a Docker API endpoint. - EndpointConfiguration struct { - Endpoint string - TLS bool - TLSCACertPath string - TLSCertPath string - TLSKeyPath string + // EndpointID represents an endpoint identifier. + EndpointID int + + // Endpoint represents a Docker endpoint with all the info required + // to connect to it. + Endpoint struct { + ID EndpointID `json:"Id"` + Name string `json:"Name"` + URL string `json:"URL"` + TLS bool `json:"TLS"` + TLSCACertPath string `json:"TLSCACert,omitempty"` + TLSCertPath string `json:"TLSCert,omitempty"` + TLSKeyPath string `json:"TLSKey,omitempty"` } + // TLSFileType represents a type of TLS file required to connect to a Docker endpoint. + // It can be either a TLS CA file, a TLS certificate file or a TLS key file. + TLSFileType int + // CLIService represents a service for managing CLI. CLIService interface { ParseFlags(version string) (*CLIFlags, error) @@ -73,6 +85,17 @@ type ( UpdateUser(user *User) error } + // EndpointService represents a service for managing endpoints. + EndpointService interface { + Endpoint(ID EndpointID) (*Endpoint, error) + Endpoints() ([]Endpoint, error) + CreateEndpoint(endpoint *Endpoint) error + UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error + DeleteEndpoint(ID EndpointID) error + GetActive() (*Endpoint, error) + SetActive(endpoint *Endpoint) error + } + // CryptoService represents a service for encrypting/hashing data. CryptoService interface { Hash(data string) (string, error) @@ -84,9 +107,25 @@ type ( GenerateToken(data *TokenData) (string, error) VerifyToken(token string) error } + + // FileService represents a service for managing files. + FileService interface { + StoreTLSFile(endpointID EndpointID, fileType TLSFileType, r io.Reader) error + GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error) + DeleteTLSFiles(endpointID EndpointID) error + } ) const ( // APIVersion is the version number of portainer API. APIVersion = "1.10.2" ) + +const ( + // TLSFileCA represents a TLS CA certificate file. + TLSFileCA TLSFileType = iota + // TLSFileCert represents a TLS certificate file. + TLSFileCert + // TLSFileKey represents a TLS key file. + TLSFileKey +) diff --git a/app/app.js b/app/app.js index dc0aa5024..e8171d909 100644 --- a/app/app.js +++ b/app/app.js @@ -5,6 +5,7 @@ angular.module('portainer', [ 'ui.select', 'ngCookies', 'ngSanitize', + 'ngFileUpload', 'angularUtils.directives.dirPagination', 'LocalStorageModule', 'angular-jwt', @@ -19,6 +20,9 @@ angular.module('portainer', [ 'containers', 'createContainer', 'docker', + 'endpoint', + 'endpointInit', + 'endpoints', 'events', 'images', 'image', @@ -270,6 +274,50 @@ angular.module('portainer', [ requiresLogin: true } }) + .state('endpoints', { + url: '/endpoints/', + views: { + "content": { + templateUrl: 'app/components/endpoints/endpoints.html', + controller: 'EndpointsController' + }, + "sidebar": { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + }, + data: { + requiresLogin: true + } + }) + .state('endpoint', { + url: '^/endpoints/:id', + views: { + "content": { + templateUrl: 'app/components/endpoint/endpoint.html', + controller: 'EndpointController' + }, + "sidebar": { + templateUrl: 'app/components/sidebar/sidebar.html', + controller: 'SidebarController' + } + }, + data: { + requiresLogin: true + } + }) + .state('endpointInit', { + url: '/init/endpoint', + views: { + "content": { + templateUrl: 'app/components/endpointInit/endpointInit.html', + controller: 'EndpointInitController' + } + }, + data: { + requiresLogin: true + } + }) .state('events', { url: '/events/', views: { @@ -491,18 +539,19 @@ angular.module('portainer', [ }); $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) { - if ((fromState.name === 'auth' || fromState.name === '') && Authentication.isAuthenticated()) { + if (toState.name !== 'endpointInit' && (fromState.name === 'auth' || fromState.name === '' || fromState.name === 'endpointInit') && Authentication.isAuthenticated()) { EndpointMode.determineEndpointMode(); } }); }]) // This is your docker url that the api will use to make requests // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 - .constant('DOCKER_ENDPOINT', '/api/docker') .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243 - .constant('CONFIG_ENDPOINT', '/api/settings') + .constant('DOCKER_ENDPOINT', '/api/docker') + .constant('CONFIG_ENDPOINT', '/api/settings') .constant('AUTH_ENDPOINT', '/api/auth') .constant('USERS_ENDPOINT', '/api/users') + .constant('ENDPOINTS_ENDPOINT', '/api/endpoints') .constant('TEMPLATES_ENDPOINT', '/api/templates') .constant('PAGINATION_MAX_ITEMS', 10) .constant('UI_VERSION', 'v1.10.2'); diff --git a/app/components/auth/auth.html b/app/components/auth/auth.html index f335c52ef..8668d328a 100644 --- a/app/components/auth/auth.html +++ b/app/components/auth/auth.html @@ -1,11 +1,11 @@ -
+
-