merge branch 'release-1.11.4' into develop

pull/645/head
Anthony Lapenna 8 years ago
commit 44ceae40b5

@ -67,20 +67,41 @@ func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
return endpoints, nil
}
// CreateEndpoint assign an ID to a new endpoint and saves it.
func (service *EndpointService) CreateEndpoint(endpoint *portainer.Endpoint) error {
// 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))
id, _ := bucket.NextSequence()
endpoint.ID = portainer.EndpointID(id)
for _, endpoint := range toCreate {
err := storeNewEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
data, err := internal.MarshalEndpoint(endpoint)
if err != nil {
return err
for _, endpoint := range toUpdate {
err := marshalAndStoreEndpoint(endpoint, bucket)
if err != nil {
return err
}
}
err = bucket.Put(internal.Itob(int(endpoint.ID)), data)
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))
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)
}

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

@ -13,4 +13,5 @@ const (
defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem"
defaultSyncInterval = "60s"
)

@ -11,4 +11,5 @@ const (
defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultSyncInterval = "60s"
)

@ -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
}
func initJWTService(authenticationEnabled bool) portainer.JWTService {
if authenticationEnabled {
jwtService, err := jwt.NewService()
if err != nil {
log.Fatal(err)
}
return jwtService
}
return nil
}
var jwtService portainer.JWTService
if !*flags.NoAuth {
jwtService, err = jwt.NewService()
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
}
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,
}
}
var cryptoService portainer.CryptoService = &crypto.Service{}
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
endpoints, err := endpointService.Endpoints()
if err != nil {
log.Fatal(err)
}
return &endpoints[0]
}
// 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 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)
}

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

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

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

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

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

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

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

@ -8,7 +8,19 @@
<rd-header-content>Endpoint management</rd-header-content>
</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">
<rd-widget>
<rd-widget-header icon="fa-plus" title="Add a new endpoint">
@ -113,7 +125,7 @@
</div>
</rd-widget-header>
<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>
</div>
<div class="pull-right">
@ -125,7 +137,7 @@
<table class="table table-hover">
<thead>
<tr>
<th></th>
<th ng-if="applicationState.application.endpointManagement"></th>
<th>
<a ui-sref="endpoints" ng-click="order('Name')">
Name
@ -147,16 +159,16 @@
<span ng-show="sortType == 'TLS' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th></th>
<th ng-if="applicationState.application.endpointManagement"></th>
</tr>
</thead>
<tbody>
<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>{{ endpoint.URL | stripprotocol }}</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">
<a ui-sref="endpoint({id: endpoint.Id})"><i class="fa fa-pencil-square-o" aria-hidden="true"></i> Edit</a>
</span>

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

@ -1,6 +1,6 @@
{
"name": "portainer",
"version": "1.11.3",
"version": "1.11.4",
"homepage": "https://github.com/portainer/portainer",
"authors": [
"Anthony Lapenna <anthony.lapenna at gmail dot com>"

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

Loading…
Cancel
Save