diff --git a/api/bolt/endpoint_service.go b/api/bolt/endpoint_service.go index f8bbdd795..cb8349076 100644 --- a/api/bolt/endpoint_service.go +++ b/api/bolt/endpoint_service.go @@ -67,20 +67,41 @@ func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) { return endpoints, nil } +// Synchronize creates, updates and deletes endpoints inside a single transaction. +func (service *EndpointService) Synchronize(toCreate, toUpdate, toDelete []*portainer.Endpoint) error { + return service.store.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(endpointBucketName)) + + for _, endpoint := range toCreate { + err := storeNewEndpoint(endpoint, bucket) + if err != nil { + return err + } + } + + for _, endpoint := range toUpdate { + err := marshalAndStoreEndpoint(endpoint, bucket) + if err != nil { + return err + } + } + + for _, endpoint := range toDelete { + err := bucket.Delete(internal.Itob(int(endpoint.ID))) + if err != nil { + return err + } + } + + return 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) + err := storeNewEndpoint(endpoint, bucket) if err != nil { return err } @@ -172,3 +193,23 @@ func (service *EndpointService) DeleteActive() error { return nil }) } + +func marshalAndStoreEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error { + 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 +} + +func storeNewEndpoint(endpoint *portainer.Endpoint, bucket *bolt.Bucket) error { + id, _ := bucket.NextSequence() + endpoint.ID = portainer.EndpointID(id) + + return marshalAndStoreEndpoint(endpoint, bucket) +} diff --git a/api/cli/cli.go b/api/cli/cli.go index d77d6249b..6148fdc95 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -1,6 +1,8 @@ package cli import ( + "time" + "github.com/portainer/portainer" "os" @@ -13,8 +15,11 @@ import ( type Service struct{} const ( - errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://") - errSocketNotFound = portainer.Error("Unable to locate Unix socket") + errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://") + errSocketNotFound = portainer.Error("Unable to locate Unix socket") + errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file") + errInvalidSyncInterval = portainer.Error("Invalid synchronization interval") + errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints") ) // ParseFlags parse the CLI flags and return a portainer.Flags struct @@ -22,19 +27,21 @@ 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(defaultBindAddress).Short('p').String(), - Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), - Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), - Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(), - NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(), - NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(), - TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(), - TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(), - TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(), - TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(), + 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')), + ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(), + SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(), + Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(), + Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), + Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), + Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(), + NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(), + NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(), + TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(), + TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(), + TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(), + TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(), } kingpin.Parse() @@ -43,13 +50,37 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { // ValidateFlags validates the values of the flags. func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { - if *flags.Endpoint != "" { - if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") { + + if *flags.Endpoint != "" && *flags.ExternalEndpoints != "" { + return errEndpointExcludeExternal + } + + err := validateEndpoint(*flags.Endpoint) + if err != nil { + return err + } + + err = validateExternalEndpoints(*flags.ExternalEndpoints) + if err != nil { + return err + } + + err = validateSyncInterval(*flags.SyncInterval) + if err != nil { + return err + } + + return nil +} + +func validateEndpoint(endpoint string) error { + if endpoint != "" { + if !strings.HasPrefix(endpoint, "unix://") && !strings.HasPrefix(endpoint, "tcp://") { return errInvalidEnpointProtocol } - if strings.HasPrefix(*flags.Endpoint, "unix://") { - socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://") + if strings.HasPrefix(endpoint, "unix://") { + socketPath := strings.TrimPrefix(endpoint, "unix://") if _, err := os.Stat(socketPath); err != nil { if os.IsNotExist(err) { return errSocketNotFound @@ -58,6 +89,27 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { } } } - + return nil +} + +func validateExternalEndpoints(externalEndpoints string) error { + if externalEndpoints != "" { + if _, err := os.Stat(externalEndpoints); err != nil { + if os.IsNotExist(err) { + return errEndpointsFileNotFound + } + return err + } + } + return nil +} + +func validateSyncInterval(syncInterval string) error { + if syncInterval != defaultSyncInterval { + _, err := time.ParseDuration(syncInterval) + if err != nil { + return errInvalidSyncInterval + } + } return nil } diff --git a/api/cli/defaults.go b/api/cli/defaults.go index 7d281575f..160b74808 100644 --- a/api/cli/defaults.go +++ b/api/cli/defaults.go @@ -13,4 +13,5 @@ const ( defaultTLSCACertPath = "/certs/ca.pem" defaultTLSCertPath = "/certs/cert.pem" defaultTLSKeyPath = "/certs/key.pem" + defaultSyncInterval = "60s" ) diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go index f8408dcdc..cbd0555a8 100644 --- a/api/cli/defaults_windows.go +++ b/api/cli/defaults_windows.go @@ -11,4 +11,5 @@ const ( defaultTLSCACertPath = "C:\\certs\\ca.pem" defaultTLSCertPath = "C:\\certs\\cert.pem" defaultTLSKeyPath = "C:\\certs\\key.pem" + defaultSyncInterval = "60s" ) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 13d0342c2..12b778c11 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -4,6 +4,7 @@ import ( "github.com/portainer/portainer" "github.com/portainer/portainer/bolt" "github.com/portainer/portainer/cli" + "github.com/portainer/portainer/cron" "github.com/portainer/portainer/crypto" "github.com/portainer/portainer/file" "github.com/portainer/portainer/http" @@ -12,7 +13,7 @@ import ( "log" ) -func main() { +func initCLI() *portainer.CLIFlags { var cli portainer.CLIService = &cli.Service{} flags, err := cli.ParseFlags(portainer.APIVersion) if err != nil { @@ -23,42 +24,77 @@ func main() { if err != nil { log.Fatal(err) } + return flags +} - settings := &portainer.Settings{ - HiddenLabels: *flags.Labels, - Logo: *flags.Logo, - Authentication: !*flags.NoAuth, - Analytics: !*flags.NoAnalytics, - } - - fileService, err := file.NewService(*flags.Data, "") +func initFileService(dataStorePath string) portainer.FileService { + fileService, err := file.NewService(dataStorePath, "") if err != nil { log.Fatal(err) } + return fileService +} - var store = bolt.NewStore(*flags.Data) - err = store.Open() +func initStore(dataStorePath string) *bolt.Store { + var store = bolt.NewStore(dataStorePath) + err := store.Open() if err != nil { log.Fatal(err) } - defer store.Close() + return store +} - var jwtService portainer.JWTService - if !*flags.NoAuth { - jwtService, err = jwt.NewService() +func initJWTService(authenticationEnabled bool) portainer.JWTService { + if authenticationEnabled { + jwtService, err := jwt.NewService() + if err != nil { + log.Fatal(err) + } + return jwtService + } + return nil +} + +func initCryptoService() portainer.CryptoService { + return &crypto.Service{} +} + +func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool { + authorizeEndpointMgmt := true + if externalEnpointFile != "" { + authorizeEndpointMgmt = false + log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.") + endpointWatcher := cron.NewWatcher(endpointService, syncInterval) + err := endpointWatcher.WatchEndpointFile(externalEnpointFile) if err != nil { log.Fatal(err) } } + return authorizeEndpointMgmt +} - var cryptoService portainer.CryptoService = &crypto.Service{} +func initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Settings { + return &portainer.Settings{ + HiddenLabels: *flags.Labels, + Logo: *flags.Logo, + Analytics: !*flags.NoAnalytics, + Authentication: !*flags.NoAuth, + EndpointManagement: authorizeEndpointMgmt, + } +} - // 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 { +func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint { + endpoints, err := endpointService.Endpoints() + if err != nil { + log.Fatal(err) + } + return &endpoints[0] +} + +func initActiveEndpoint(endpointService portainer.EndpointService, flags *portainer.CLIFlags) *portainer.Endpoint { + activeEndpoint, err := endpointService.GetActive() + if err == portainer.ErrEndpointNotFound { + if *flags.Endpoint != "" { activeEndpoint = &portainer.Endpoint{ Name: "primary", URL: *flags.Endpoint, @@ -67,31 +103,54 @@ func main() { TLSCertPath: *flags.TLSCert, TLSKeyPath: *flags.TLSKey, } - err = store.EndpointService.CreateEndpoint(activeEndpoint) + err = endpointService.CreateEndpoint(activeEndpoint) if err != nil { log.Fatal(err) } - } else if err != nil { - log.Fatal(err) + } else if *flags.ExternalEndpoints != "" { + activeEndpoint = retrieveFirstEndpointFromDatabase(endpointService) } + } else if err != nil { + log.Fatal(err) } + return activeEndpoint +} + +func main() { + flags := initCLI() + + fileService := initFileService(*flags.Data) + + store := initStore(*flags.Data) + defer store.Close() + + jwtService := initJWTService(!*flags.NoAuth) + + cryptoService := initCryptoService() + + authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval) + + settings := initSettings(authorizeEndpointMgmt, flags) + + activeEndpoint := initActiveEndpoint(store.EndpointService, flags) var server portainer.Server = &http.Server{ - BindAddress: *flags.Addr, - AssetsPath: *flags.Assets, - Settings: settings, - TemplatesURL: *flags.Templates, - AuthDisabled: *flags.NoAuth, - UserService: store.UserService, - EndpointService: store.EndpointService, - CryptoService: cryptoService, - JWTService: jwtService, - FileService: fileService, - ActiveEndpoint: activeEndpoint, + BindAddress: *flags.Addr, + AssetsPath: *flags.Assets, + Settings: settings, + TemplatesURL: *flags.Templates, + AuthDisabled: *flags.NoAuth, + EndpointManagement: authorizeEndpointMgmt, + UserService: store.UserService, + EndpointService: store.EndpointService, + CryptoService: cryptoService, + JWTService: jwtService, + FileService: fileService, + ActiveEndpoint: activeEndpoint, } log.Printf("Starting Portainer on %s", *flags.Addr) - err = server.Start() + err := server.Start() if err != nil { log.Fatal(err) } diff --git a/api/cron/endpoint_sync.go b/api/cron/endpoint_sync.go new file mode 100644 index 000000000..9dcd4e290 --- /dev/null +++ b/api/cron/endpoint_sync.go @@ -0,0 +1,171 @@ +package cron + +import ( + "encoding/json" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/portainer/portainer" +) + +type ( + endpointSyncJob struct { + logger *log.Logger + endpointService portainer.EndpointService + endpointFilePath string + } + + synchronization struct { + endpointsToCreate []*portainer.Endpoint + endpointsToUpdate []*portainer.Endpoint + endpointsToDelete []*portainer.Endpoint + } +) + +const ( + // ErrEmptyEndpointArray is an error raised when the external endpoint source array is empty. + ErrEmptyEndpointArray = portainer.Error("External endpoint source is empty") +) + +func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob { + return endpointSyncJob{ + logger: log.New(os.Stderr, "", log.LstdFlags), + endpointService: endpointService, + endpointFilePath: endpointFilePath, + } +} + +func endpointSyncError(err error, logger *log.Logger) bool { + if err != nil { + logger.Printf("Endpoint synchronization error: %s", err) + return true + } + return false +} + +func isValidEndpoint(endpoint *portainer.Endpoint) bool { + if endpoint.Name != "" && endpoint.URL != "" { + if !strings.HasPrefix(endpoint.URL, "unix://") && !strings.HasPrefix(endpoint.URL, "tcp://") { + return false + } + return true + } + return false +} + +func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int { + for idx, v := range endpoints { + if endpoint.Name == v.Name && isValidEndpoint(&v) { + return idx + } + } + return -1 +} + +func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint { + var endpoint *portainer.Endpoint + if original.URL != updated.URL || original.TLS != updated.TLS { + endpoint = original + endpoint.URL = updated.URL + if updated.TLS { + endpoint.TLS = true + endpoint.TLSCACertPath = updated.TLSCACertPath + endpoint.TLSCertPath = updated.TLSCertPath + endpoint.TLSKeyPath = updated.TLSKeyPath + } else { + endpoint.TLS = false + endpoint.TLSCACertPath = "" + endpoint.TLSCertPath = "" + endpoint.TLSKeyPath = "" + } + } + return endpoint +} + +func (sync synchronization) requireSync() bool { + if len(sync.endpointsToCreate) != 0 || len(sync.endpointsToUpdate) != 0 || len(sync.endpointsToDelete) != 0 { + return true + } + return false +} + +// TMP: endpointSyncJob method to access logger, should be generic +func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization { + endpointsToCreate := make([]*portainer.Endpoint, 0) + endpointsToUpdate := make([]*portainer.Endpoint, 0) + endpointsToDelete := make([]*portainer.Endpoint, 0) + + for idx := range storedEndpoints { + fidx := endpointExists(&storedEndpoints[idx], fileEndpoints) + if fidx != -1 { + endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx]) + if endpoint != nil { + job.logger.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL) + endpointsToUpdate = append(endpointsToUpdate, endpoint) + } else { + job.logger.Printf("No change detected for a stored endpoint. [name: %v] [url: %v]\n", storedEndpoints[idx].Name, storedEndpoints[idx].URL) + } + } else { + job.logger.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL) + endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx]) + } + } + + for idx, endpoint := range fileEndpoints { + if endpoint.Name == "" || endpoint.URL == "" { + job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL) + continue + } + sidx := endpointExists(&fileEndpoints[idx], storedEndpoints) + if sidx == -1 { + job.logger.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL) + endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx]) + } + } + + return &synchronization{ + endpointsToCreate: endpointsToCreate, + endpointsToUpdate: endpointsToUpdate, + endpointsToDelete: endpointsToDelete, + } +} + +func (job endpointSyncJob) Sync() error { + data, err := ioutil.ReadFile(job.endpointFilePath) + if endpointSyncError(err, job.logger) { + return err + } + + var fileEndpoints []portainer.Endpoint + err = json.Unmarshal(data, &fileEndpoints) + if endpointSyncError(err, job.logger) { + return err + } + + if len(fileEndpoints) == 0 { + return ErrEmptyEndpointArray + } + + storedEndpoints, err := job.endpointService.Endpoints() + if endpointSyncError(err, job.logger) { + return err + } + + sync := job.prepareSyncData(storedEndpoints, fileEndpoints) + if sync.requireSync() { + err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete) + if endpointSyncError(err, job.logger) { + return err + } + job.logger.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete)) + } + return nil +} + +func (job endpointSyncJob) Run() { + job.logger.Println("Endpoint synchronization job started.") + err := job.Sync() + endpointSyncError(err, job.logger) +} diff --git a/api/cron/watcher.go b/api/cron/watcher.go new file mode 100644 index 000000000..6b44ff5ce --- /dev/null +++ b/api/cron/watcher.go @@ -0,0 +1,40 @@ +package cron + +import ( + "github.com/portainer/portainer" + "github.com/robfig/cron" +) + +// Watcher represents a service for managing crons. +type Watcher struct { + Cron *cron.Cron + EndpointService portainer.EndpointService + syncInterval string +} + +// NewWatcher initializes a new service. +func NewWatcher(endpointService portainer.EndpointService, syncInterval string) *Watcher { + return &Watcher{ + Cron: cron.New(), + EndpointService: endpointService, + syncInterval: syncInterval, + } +} + +// WatchEndpointFile starts a cron job to synchronize the endpoints from a file +func (watcher *Watcher) WatchEndpointFile(endpointFilePath string) error { + job := newEndpointSyncJob(endpointFilePath, watcher.EndpointService) + + err := job.Sync() + if err != nil { + return err + } + + err = watcher.Cron.AddJob("@every "+watcher.syncInterval, job) + if err != nil { + return err + } + + watcher.Cron.Start() + return nil +} diff --git a/api/http/endpoint_handler.go b/api/http/endpoint_handler.go index 9f5ca31c0..7c7d0f930 100644 --- a/api/http/endpoint_handler.go +++ b/api/http/endpoint_handler.go @@ -16,13 +16,20 @@ import ( // 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 + Logger *log.Logger + authorizeEndpointManagement bool + EndpointService portainer.EndpointService + FileService portainer.FileService + server *Server + middleWareService *middleWareService } +const ( + // ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints + // when the server has been started with the --external-endpoints flag + ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled") +) + // NewEndpointHandler returns a new instance of EndpointHandler. func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler { h := &EndpointHandler{ @@ -65,6 +72,11 @@ func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *htt // 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) { + if !handler.authorizeEndpointManagement { + Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger) + return + } + var req postEndpointsRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger) @@ -203,6 +215,11 @@ func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *htt // handlePutEndpoint handles PUT requests on /endpoints/:id func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) { + if !handler.authorizeEndpointManagement { + Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger) + return + } + vars := mux.Vars(r) id := vars["id"] @@ -262,6 +279,11 @@ type putEndpointsRequest struct { // handleDeleteEndpoint handles DELETE requests on /endpoints/:id // DELETE /endpoints/0 deletes the active endpoint func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) { + if !handler.authorizeEndpointManagement { + Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger) + return + } + vars := mux.Vars(r) id := vars["id"] diff --git a/api/http/server.go b/api/http/server.go index 784edc5dc..dc09db888 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -8,18 +8,19 @@ import ( // Server implements the portainer.Server interface type Server struct { - BindAddress string - AssetsPath string - AuthDisabled bool - 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 + BindAddress string + AssetsPath string + AuthDisabled bool + EndpointManagement bool + 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 { @@ -61,6 +62,7 @@ func (server *Server) Start() error { var websocketHandler = NewWebSocketHandler() // EndpointHandler requires a reference to the server to be able to update the active endpoint. var endpointHandler = NewEndpointHandler(middleWareService) + endpointHandler.authorizeEndpointManagement = server.EndpointManagement endpointHandler.EndpointService = server.EndpointService endpointHandler.FileService = server.FileService endpointHandler.server = server diff --git a/api/portainer.go b/api/portainer.go index fc3cb1df2..5297d50c0 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -13,27 +13,30 @@ type ( // CLIFlags represents the available flags on the CLI. CLIFlags struct { - Addr *string - Assets *string - Data *string - Endpoint *string - Labels *[]Pair - Logo *string - Templates *string - NoAuth *bool - NoAnalytics *bool - TLSVerify *bool - TLSCacert *string - TLSCert *string - TLSKey *string + Addr *string + Assets *string + Data *string + ExternalEndpoints *string + SyncInterval *string + Endpoint *string + Labels *[]Pair + Logo *string + Templates *string + NoAuth *bool + NoAnalytics *bool + TLSVerify *bool + TLSCacert *string + TLSCert *string + TLSKey *string } // Settings represents Portainer settings. Settings struct { - HiddenLabels []Pair `json:"hiddenLabels"` - Logo string `json:"logo"` - Authentication bool `json:"authentication"` - Analytics bool `json:"analytics"` + HiddenLabels []Pair `json:"hiddenLabels"` + Logo string `json:"logo"` + Authentication bool `json:"authentication"` + Analytics bool `json:"analytics"` + EndpointManagement bool `json:"endpointManagement"` } // User represent a user account. @@ -99,6 +102,7 @@ type ( GetActive() (*Endpoint, error) SetActive(endpoint *Endpoint) error DeleteActive() error + Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error } // CryptoService represents a service for encrypting/hashing data. @@ -119,11 +123,16 @@ type ( GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error) DeleteTLSFiles(endpointID EndpointID) error } + + // EndpointWatcher represents a service to synchronize the endpoints via an external source. + EndpointWatcher interface { + WatchEndpointFile(endpointFilePath string) error + } ) const ( // APIVersion is the version number of portainer API. - APIVersion = "1.11.3" + APIVersion = "1.11.4" ) const ( diff --git a/app/app.js b/app/app.js index 3c31d9274..82fe85f9a 100644 --- a/app/app.js +++ b/app/app.js @@ -523,4 +523,4 @@ angular.module('portainer', [ .constant('ENDPOINTS_ENDPOINT', 'api/endpoints') .constant('TEMPLATES_ENDPOINT', 'api/templates') .constant('PAGINATION_MAX_ITEMS', 10) - .constant('UI_VERSION', 'v1.11.3'); + .constant('UI_VERSION', 'v1.11.4'); diff --git a/app/components/endpoint/endpointController.js b/app/components/endpoint/endpointController.js index 0c254b3df..e6e4eb526 100644 --- a/app/components/endpoint/endpointController.js +++ b/app/components/endpoint/endpointController.js @@ -1,6 +1,11 @@ angular.module('endpoint', []) .controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Messages', function ($scope, $state, $stateParams, $filter, EndpointService, Messages) { + + if (!$scope.applicationState.application.endpointManagement) { + $state.go('endpoints'); + } + $scope.state = { error: '', uploadInProgress: false diff --git a/app/components/endpoints/endpoints.html b/app/components/endpoints/endpoints.html index 7c6acea3e..41501eaa5 100644 --- a/app/components/endpoints/endpoints.html +++ b/app/components/endpoints/endpoints.html @@ -8,7 +8,19 @@ Endpoint management -
+
+
+ + + + + Portainer has been started using the --external-endpoints flag. Endpoint management via the UI is disabled. + + +
+
+ +
@@ -113,7 +125,7 @@
-
+
@@ -125,7 +137,7 @@ - + - + - + -
Name @@ -147,16 +159,16 @@
{{ endpoint.Name }} {{ endpoint.URL | stripprotocol }} + Edit diff --git a/app/services/stateManager.js b/app/services/stateManager.js index 8f4e41b77..c17dae338 100644 --- a/app/services/stateManager.js +++ b/app/services/stateManager.js @@ -25,6 +25,7 @@ angular.module('portainer.services') Config.$promise.then(function success(data) { state.application.authentication = data.authentication; state.application.analytics = data.analytics; + state.application.endpointManagement = data.endpointManagement; state.application.logo = data.logo; LocalStorage.storeApplicationState(state.application); state.loading = false; diff --git a/bower.json b/bower.json index 2627f8495..1c4d9514a 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "portainer", - "version": "1.11.3", + "version": "1.11.4", "homepage": "https://github.com/portainer/portainer", "authors": [ "Anthony Lapenna " diff --git a/package.json b/package.json index a24d04c33..8f879fe51 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "1.11.3", + "version": "1.11.4", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git"