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 @@
--external-endpoints
flag. Endpoint management via the UI is disabled.
+
+ + | 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 |