feat(endpoints): add the ability to define endpoints from an external source

pull/572/head
Anthony Lapenna 2017-02-06 18:29:34 +13:00
parent 10f7744a62
commit dc78ec5135
13 changed files with 416 additions and 67 deletions

View File

@ -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 {

View File

@ -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
} }

View File

@ -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"
) )

View File

@ -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"
) )

View File

@ -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)

166
api/cron/endpoint_sync.go Normal file
View File

@ -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)
}

34
api/cron/watcher.go Normal file
View File

@ -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
}

View File

@ -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"]

View File

@ -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

View File

@ -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 (

View File

@ -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

View File

@ -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>

View File

@ -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;