mirror of https://github.com/portainer/portainer
feat(endpoints): add the ability to define endpoints from an external source
parent
10f7744a62
commit
dc78ec5135
|
@ -67,6 +67,49 @@ func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
|
||||||
return endpoints, nil
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range toUpdate {
|
||||||
|
data, err := internal.MarshalEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
|
||||||
|
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.
|
// CreateEndpoint assign an ID to a new endpoint and saves it.
|
||||||
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
|
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
|
||||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
"os"
|
"os"
|
||||||
|
@ -15,6 +17,8 @@ type Service struct{}
|
||||||
const (
|
const (
|
||||||
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
|
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
|
||||||
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
|
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
|
||||||
|
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
|
||||||
|
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseFlags parse the CLI flags and return a portainer.Flags struct
|
// ParseFlags parse the CLI flags and return a portainer.Flags struct
|
||||||
|
@ -22,18 +26,20 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
kingpin.Version(version)
|
kingpin.Version(version)
|
||||||
|
|
||||||
flags := &portainer.CLIFlags{
|
flags := &portainer.CLIFlags{
|
||||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").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(),
|
||||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
||||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
|
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||||
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
|
||||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
||||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||||
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
|
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()
|
kingpin.Parse()
|
||||||
|
@ -58,5 +64,21 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *flags.ExternalEndpoints != "" {
|
||||||
|
if _, err := os.Stat(*flags.ExternalEndpoints); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return errEndpointsFileNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *flags.SyncInterval != defaultSyncInterval {
|
||||||
|
_, err := time.ParseDuration(*flags.SyncInterval)
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidSyncInterval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,5 @@ const (
|
||||||
defaultTLSCACertPath = "/certs/ca.pem"
|
defaultTLSCACertPath = "/certs/ca.pem"
|
||||||
defaultTLSCertPath = "/certs/cert.pem"
|
defaultTLSCertPath = "/certs/cert.pem"
|
||||||
defaultTLSKeyPath = "/certs/key.pem"
|
defaultTLSKeyPath = "/certs/key.pem"
|
||||||
|
defaultSyncInterval = "60s"
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,4 +10,5 @@ const (
|
||||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||||
|
defaultSyncInterval = "60s"
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
"github.com/portainer/portainer/bolt"
|
"github.com/portainer/portainer/bolt"
|
||||||
"github.com/portainer/portainer/cli"
|
"github.com/portainer/portainer/cli"
|
||||||
|
"github.com/portainer/portainer/cron"
|
||||||
"github.com/portainer/portainer/crypto"
|
"github.com/portainer/portainer/crypto"
|
||||||
"github.com/portainer/portainer/file"
|
"github.com/portainer/portainer/file"
|
||||||
"github.com/portainer/portainer/http"
|
"github.com/portainer/portainer/http"
|
||||||
|
@ -24,12 +25,6 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := &portainer.Settings{
|
|
||||||
HiddenLabels: *flags.Labels,
|
|
||||||
Logo: *flags.Logo,
|
|
||||||
Authentication: !*flags.NoAuth,
|
|
||||||
}
|
|
||||||
|
|
||||||
fileService, err := file.NewService(*flags.Data, "")
|
fileService, err := file.NewService(*flags.Data, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
@ -52,6 +47,25 @@ func main() {
|
||||||
|
|
||||||
var cryptoService portainer.CryptoService = &crypto.Service{}
|
var cryptoService portainer.CryptoService = &crypto.Service{}
|
||||||
|
|
||||||
|
var endpointWatcher portainer.EndpointWatcher
|
||||||
|
authorizeEndpointMgmt := true
|
||||||
|
if *flags.ExternalEndpoints != "" {
|
||||||
|
log.Println("Using external endpoint definition. Disabling endpoint management via API.")
|
||||||
|
authorizeEndpointMgmt = false
|
||||||
|
endpointWatcher = cron.NewWatcher(store.EndpointService, *flags.SyncInterval)
|
||||||
|
err = endpointWatcher.WatchEndpointFile(*flags.ExternalEndpoints)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := &portainer.Settings{
|
||||||
|
HiddenLabels: *flags.Labels,
|
||||||
|
Logo: *flags.Logo,
|
||||||
|
Authentication: !*flags.NoAuth,
|
||||||
|
EndpointManagement: authorizeEndpointMgmt,
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the active endpoint from the CLI only if there is no
|
// Initialize the active endpoint from the CLI only if there is no
|
||||||
// active endpoint defined yet.
|
// active endpoint defined yet.
|
||||||
var activeEndpoint *portainer.Endpoint
|
var activeEndpoint *portainer.Endpoint
|
||||||
|
@ -74,19 +88,36 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if *flags.ExternalEndpoints != "" {
|
||||||
|
activeEndpoint, err = store.EndpointService.GetActive()
|
||||||
|
if err == portainer.ErrEndpointNotFound {
|
||||||
|
var endpoints []portainer.Endpoint
|
||||||
|
endpoints, err = store.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = store.EndpointService.SetActive(&endpoints[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var server portainer.Server = &http.Server{
|
var server portainer.Server = &http.Server{
|
||||||
BindAddress: *flags.Addr,
|
BindAddress: *flags.Addr,
|
||||||
AssetsPath: *flags.Assets,
|
AssetsPath: *flags.Assets,
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
TemplatesURL: *flags.Templates,
|
TemplatesURL: *flags.Templates,
|
||||||
AuthDisabled: *flags.NoAuth,
|
AuthDisabled: *flags.NoAuth,
|
||||||
UserService: store.UserService,
|
EndpointManagement: authorizeEndpointMgmt,
|
||||||
EndpointService: store.EndpointService,
|
UserService: store.UserService,
|
||||||
CryptoService: cryptoService,
|
EndpointService: store.EndpointService,
|
||||||
JWTService: jwtService,
|
CryptoService: cryptoService,
|
||||||
FileService: fileService,
|
JWTService: jwtService,
|
||||||
ActiveEndpoint: activeEndpoint,
|
FileService: fileService,
|
||||||
|
ActiveEndpoint: activeEndpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
package cron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"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 != "" {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (job endpointSyncJob) Run() {
|
||||||
|
job.logger.Printf("Endpoint synchronization job started")
|
||||||
|
err := job.Sync()
|
||||||
|
endpointSyncError(err, job.logger)
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
watcher.Cron.AddJob("@every "+watcher.syncInterval, job)
|
||||||
|
watcher.Cron.Start()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -16,13 +16,20 @@ import (
|
||||||
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
|
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
|
||||||
type EndpointHandler struct {
|
type EndpointHandler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
EndpointService portainer.EndpointService
|
authorizeEndpointManagement bool
|
||||||
FileService portainer.FileService
|
EndpointService portainer.EndpointService
|
||||||
server *Server
|
FileService portainer.FileService
|
||||||
middleWareService *middleWareService
|
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.
|
// NewEndpointHandler returns a new instance of EndpointHandler.
|
||||||
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
|
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
|
||||||
h := &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.
|
// if the active URL parameter is specified, will also define the new endpoint as the active endpoint.
|
||||||
// /endpoints(?active=true|false)
|
// /endpoints(?active=true|false)
|
||||||
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
|
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !handler.authorizeEndpointManagement {
|
||||||
|
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var req postEndpointsRequest
|
var req postEndpointsRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
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
|
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||||
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
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)
|
vars := mux.Vars(r)
|
||||||
id := vars["id"]
|
id := vars["id"]
|
||||||
|
|
||||||
|
@ -262,6 +279,11 @@ type putEndpointsRequest struct {
|
||||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||||
// DELETE /endpoints/0 deletes the active endpoint
|
// DELETE /endpoints/0 deletes the active endpoint
|
||||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
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)
|
vars := mux.Vars(r)
|
||||||
id := vars["id"]
|
id := vars["id"]
|
||||||
|
|
||||||
|
|
|
@ -8,18 +8,19 @@ import (
|
||||||
|
|
||||||
// Server implements the portainer.Server interface
|
// Server implements the portainer.Server interface
|
||||||
type Server struct {
|
type Server struct {
|
||||||
BindAddress string
|
BindAddress string
|
||||||
AssetsPath string
|
AssetsPath string
|
||||||
AuthDisabled bool
|
AuthDisabled bool
|
||||||
UserService portainer.UserService
|
EndpointManagement bool
|
||||||
EndpointService portainer.EndpointService
|
UserService portainer.UserService
|
||||||
CryptoService portainer.CryptoService
|
EndpointService portainer.EndpointService
|
||||||
JWTService portainer.JWTService
|
CryptoService portainer.CryptoService
|
||||||
FileService portainer.FileService
|
JWTService portainer.JWTService
|
||||||
Settings *portainer.Settings
|
FileService portainer.FileService
|
||||||
TemplatesURL string
|
Settings *portainer.Settings
|
||||||
ActiveEndpoint *portainer.Endpoint
|
TemplatesURL string
|
||||||
Handler *Handler
|
ActiveEndpoint *portainer.Endpoint
|
||||||
|
Handler *Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
|
func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
|
||||||
|
@ -61,6 +62,7 @@ func (server *Server) Start() error {
|
||||||
var websocketHandler = NewWebSocketHandler()
|
var websocketHandler = NewWebSocketHandler()
|
||||||
// EndpointHandler requires a reference to the server to be able to update the active endpoint.
|
// EndpointHandler requires a reference to the server to be able to update the active endpoint.
|
||||||
var endpointHandler = NewEndpointHandler(middleWareService)
|
var endpointHandler = NewEndpointHandler(middleWareService)
|
||||||
|
endpointHandler.authorizeEndpointManagement = server.EndpointManagement
|
||||||
endpointHandler.EndpointService = server.EndpointService
|
endpointHandler.EndpointService = server.EndpointService
|
||||||
endpointHandler.FileService = server.FileService
|
endpointHandler.FileService = server.FileService
|
||||||
endpointHandler.server = server
|
endpointHandler.server = server
|
||||||
|
|
|
@ -13,25 +13,28 @@ type (
|
||||||
|
|
||||||
// CLIFlags represents the available flags on the CLI.
|
// CLIFlags represents the available flags on the CLI.
|
||||||
CLIFlags struct {
|
CLIFlags struct {
|
||||||
Addr *string
|
Addr *string
|
||||||
Assets *string
|
Assets *string
|
||||||
Data *string
|
Data *string
|
||||||
Endpoint *string
|
ExternalEndpoints *string
|
||||||
Labels *[]Pair
|
SyncInterval *string
|
||||||
Logo *string
|
Endpoint *string
|
||||||
Templates *string
|
Labels *[]Pair
|
||||||
NoAuth *bool
|
Logo *string
|
||||||
TLSVerify *bool
|
Templates *string
|
||||||
TLSCacert *string
|
NoAuth *bool
|
||||||
TLSCert *string
|
TLSVerify *bool
|
||||||
TLSKey *string
|
TLSCacert *string
|
||||||
|
TLSCert *string
|
||||||
|
TLSKey *string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Settings represents Portainer settings.
|
// Settings represents Portainer settings.
|
||||||
Settings struct {
|
Settings struct {
|
||||||
HiddenLabels []Pair `json:"hiddenLabels"`
|
HiddenLabels []Pair `json:"hiddenLabels"`
|
||||||
Logo string `json:"logo"`
|
Logo string `json:"logo"`
|
||||||
Authentication bool `json:"authentication"`
|
Authentication bool `json:"authentication"`
|
||||||
|
EndpointManagement bool `json:"endpointManagement"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// User represent a user account.
|
// User represent a user account.
|
||||||
|
@ -97,6 +100,7 @@ type (
|
||||||
GetActive() (*Endpoint, error)
|
GetActive() (*Endpoint, error)
|
||||||
SetActive(endpoint *Endpoint) error
|
SetActive(endpoint *Endpoint) error
|
||||||
DeleteActive() error
|
DeleteActive() error
|
||||||
|
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// CryptoService represents a service for encrypting/hashing data.
|
// CryptoService represents a service for encrypting/hashing data.
|
||||||
|
@ -117,6 +121,11 @@ type (
|
||||||
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
|
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
|
||||||
DeleteTLSFiles(endpointID EndpointID) error
|
DeleteTLSFiles(endpointID EndpointID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
|
||||||
|
EndpointWatcher interface {
|
||||||
|
WatchEndpointFile(endpointFilePath string) error
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
angular.module('endpoint', [])
|
angular.module('endpoint', [])
|
||||||
.controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Messages',
|
.controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Messages',
|
||||||
function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
|
function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
|
||||||
|
|
||||||
|
if (!$scope.applicationState.application.endpointManagement) {
|
||||||
|
$state.go('endpoints');
|
||||||
|
}
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
error: '',
|
error: '',
|
||||||
uploadInProgress: false
|
uploadInProgress: false
|
||||||
|
|
|
@ -8,7 +8,19 @@
|
||||||
<rd-header-content>Endpoint management</rd-header-content>
|
<rd-header-content>Endpoint management</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row" ng-if="!applicationState.application.endpointManagement">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-exclamation-triangle" title="Endpoint management is not available">
|
||||||
|
</rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<span class="small text-muted">Portainer has been started using the <code>--external-endpoints</code> flag. Endpoint management via the UI is disabled.</span>
|
||||||
|
</rd-wigdet-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" ng-if="applicationState.application.endpointManagement">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-plus" title="Add a new endpoint">
|
<rd-widget-header icon="fa-plus" title="Add a new endpoint">
|
||||||
|
@ -113,7 +125,7 @@
|
||||||
</div>
|
</div>
|
||||||
</rd-widget-header>
|
</rd-widget-header>
|
||||||
<rd-widget-taskbar classes="col-lg-12">
|
<rd-widget-taskbar classes="col-lg-12">
|
||||||
<div class="pull-left">
|
<div class="pull-left" ng-if="applicationState.application.endpointManagement">
|
||||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
|
@ -125,7 +137,7 @@
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th ng-if="applicationState.application.endpointManagement"></th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="endpoints" ng-click="order('Name')">
|
<a ui-sref="endpoints" ng-click="order('Name')">
|
||||||
Name
|
Name
|
||||||
|
@ -147,16 +159,16 @@
|
||||||
<span ng-show="sortType == 'TLS' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'TLS' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th></th>
|
<th ng-if="applicationState.application.endpointManagement"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||||
<td><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
|
<td ng-if="applicationState.application.endpointManagement"><input type="checkbox" ng-model="endpoint.Checked" ng-change="selectItem(endpoint)" /></td>
|
||||||
<td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td>
|
<td><i class="fa fa-star" aria-hidden="true" ng-if="endpoint.Id === activeEndpoint.Id"></i> {{ endpoint.Name }}</td>
|
||||||
<td>{{ endpoint.URL | stripprotocol }}</td>
|
<td>{{ endpoint.URL | stripprotocol }}</td>
|
||||||
<td><i class="fa fa-shield" aria-hidden="true" ng-if="endpoint.TLS"></i></td>
|
<td><i class="fa fa-shield" aria-hidden="true" ng-if="endpoint.TLS"></i></td>
|
||||||
<td>
|
<td ng-if="applicationState.application.endpointManagement">
|
||||||
<span ng-if="endpoint.Id !== activeEndpoint.Id">
|
<span ng-if="endpoint.Id !== activeEndpoint.Id">
|
||||||
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
|
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -24,6 +24,7 @@ angular.module('portainer.services')
|
||||||
} else {
|
} else {
|
||||||
Config.$promise.then(function success(data) {
|
Config.$promise.then(function success(data) {
|
||||||
state.application.authentication = data.authentication;
|
state.application.authentication = data.authentication;
|
||||||
|
state.application.endpointManagement = data.endpointManagement;
|
||||||
state.application.logo = data.logo;
|
state.application.logo = data.logo;
|
||||||
LocalStorage.storeApplicationState(state.application);
|
LocalStorage.storeApplicationState(state.application);
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
|
|
Loading…
Reference in New Issue