mirror of https://github.com/portainer/portainer
feat(global): multi endpoint management (#407)
parent
a08ea134fc
commit
d54d30a7be
|
@ -2,4 +2,4 @@ node_modules
|
||||||
bower_components
|
bower_components
|
||||||
dist
|
dist
|
||||||
portainer-checksum.txt
|
portainer-checksum.txt
|
||||||
api/cmd/portainer/portainer-*
|
api/cmd/portainer/portainer*
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package bolt
|
package bolt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/boltdb/bolt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store defines the implementation of portainer.DataStore using
|
// Store defines the implementation of portainer.DataStore using
|
||||||
|
@ -12,23 +13,28 @@ type Store struct {
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
UserService *UserService
|
UserService *UserService
|
||||||
|
EndpointService *EndpointService
|
||||||
|
|
||||||
db *bolt.DB
|
db *bolt.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
databaseFileName = "portainer.db"
|
databaseFileName = "portainer.db"
|
||||||
userBucketName = "users"
|
userBucketName = "users"
|
||||||
|
endpointBucketName = "endpoints"
|
||||||
|
activeEndpointBucketName = "activeEndpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewStore initializes a new Store and the associated services
|
// NewStore initializes a new Store and the associated services
|
||||||
func NewStore(storePath string) *Store {
|
func NewStore(storePath string) *Store {
|
||||||
store := &Store{
|
store := &Store{
|
||||||
Path: storePath,
|
Path: storePath,
|
||||||
UserService: &UserService{},
|
UserService: &UserService{},
|
||||||
|
EndpointService: &EndpointService{},
|
||||||
}
|
}
|
||||||
store.UserService.store = store
|
store.UserService.store = store
|
||||||
|
store.EndpointService.store = store
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +51,14 @@ func (store *Store) Open() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.CreateBucketIfNotExists([]byte(activeEndpointBucketName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/bolt/internal"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EndpointService represents a service for managing users.
|
||||||
|
type EndpointService struct {
|
||||||
|
store *Store
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
activeEndpointID = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Endpoint returns an endpoint by ID.
|
||||||
|
func (service *EndpointService) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||||
|
var data []byte
|
||||||
|
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||||
|
value := bucket.Get(internal.Itob(int(ID)))
|
||||||
|
if value == nil {
|
||||||
|
return portainer.ErrEndpointNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
data = make([]byte, len(value))
|
||||||
|
copy(data, value)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint portainer.Endpoint
|
||||||
|
err = internal.UnmarshalEndpoint(data, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &endpoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoints return an array containing all the endpoints.
|
||||||
|
func (service *EndpointService) Endpoints() ([]portainer.Endpoint, error) {
|
||||||
|
var endpoints []portainer.Endpoint
|
||||||
|
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||||
|
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
var endpoint portainer.Endpoint
|
||||||
|
err := internal.UnmarshalEndpoint(v, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
endpoints = append(endpoints, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints, 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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEndpoint updates an endpoint.
|
||||||
|
func (service *EndpointService) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||||
|
data, err := internal.MarshalEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||||
|
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEndpoint deletes an endpoint.
|
||||||
|
func (service *EndpointService) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||||
|
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(endpointBucketName))
|
||||||
|
err := bucket.Delete(internal.Itob(int(ID)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActive returns the active endpoint.
|
||||||
|
func (service *EndpointService) GetActive() (*portainer.Endpoint, error) {
|
||||||
|
var data []byte
|
||||||
|
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||||
|
value := bucket.Get(internal.Itob(activeEndpointID))
|
||||||
|
if value == nil {
|
||||||
|
return portainer.ErrEndpointNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
data = make([]byte, len(value))
|
||||||
|
copy(data, value)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint portainer.Endpoint
|
||||||
|
err = internal.UnmarshalEndpoint(data, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &endpoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetActive saves an endpoint as active.
|
||||||
|
func (service *EndpointService) SetActive(endpoint *portainer.Endpoint) error {
|
||||||
|
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(activeEndpointBucketName))
|
||||||
|
|
||||||
|
data, err := internal.MarshalEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bucket.Put(internal.Itob(activeEndpointID), data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package internal
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,3 +16,22 @@ func MarshalUser(user *portainer.User) ([]byte, error) {
|
||||||
func UnmarshalUser(data []byte, user *portainer.User) error {
|
func UnmarshalUser(data []byte, user *portainer.User) error {
|
||||||
return json.Unmarshal(data, user)
|
return json.Unmarshal(data, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalEndpoint encodes an endpoint to binary format.
|
||||||
|
func MarshalEndpoint(endpoint *portainer.Endpoint) ([]byte, error) {
|
||||||
|
return json.Marshal(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalEndpoint decodes an endpoint from a binary data.
|
||||||
|
func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
|
||||||
|
return json.Unmarshal(data, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Itob returns an 8-byte big endian representation of v.
|
||||||
|
// This function is typically used for encoding integer IDs to byte slices
|
||||||
|
// so that they can be used as BoltDB keys.
|
||||||
|
func Itob(v int) []byte {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(b, uint64(v))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ package cli
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service implements the CLIService interface
|
// Service implements the CLIService interface
|
||||||
|
@ -21,13 +22,12 @@ 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(),
|
||||||
|
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(":9000").Short('p').String(),
|
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').String(),
|
||||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String(),
|
Assets: kingpin.Flag("assets", "Path to the assets").Default(".").Short('a').String(),
|
||||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String(),
|
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default("/data").Short('d').String(),
|
||||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Default("unix:///var/run/docker.sock").Short('H').String(),
|
|
||||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
|
||||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
|
||||||
Swarm: kingpin.Flag("swarm", "Swarm cluster support").Default("false").Short('s').Bool(),
|
|
||||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String(),
|
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default("https://raw.githubusercontent.com/portainer/templates/master/templates.json").Short('t').String(),
|
||||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default("false").Bool(),
|
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default("false").Bool(),
|
||||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String(),
|
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String(),
|
||||||
|
@ -41,17 +41,19 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
|
|
||||||
// ValidateFlags validates the values of the flags.
|
// ValidateFlags validates the values of the flags.
|
||||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||||
if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") {
|
if *flags.Endpoint != "" {
|
||||||
return errInvalidEnpointProtocol
|
if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") {
|
||||||
}
|
return errInvalidEnpointProtocol
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(*flags.Endpoint, "unix://") {
|
if strings.HasPrefix(*flags.Endpoint, "unix://") {
|
||||||
socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://")
|
socketPath := strings.TrimPrefix(*flags.Endpoint, "unix://")
|
||||||
if _, err := os.Stat(socketPath); err != nil {
|
if _, err := os.Stat(socketPath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return errSocketNotFound
|
return errSocketNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"github.com/portainer/portainer/bolt"
|
"github.com/portainer/portainer/bolt"
|
||||||
"github.com/portainer/portainer/cli"
|
"github.com/portainer/portainer/cli"
|
||||||
"github.com/portainer/portainer/crypto"
|
"github.com/portainer/portainer/crypto"
|
||||||
|
"github.com/portainer/portainer/file"
|
||||||
"github.com/portainer/portainer/http"
|
"github.com/portainer/portainer/http"
|
||||||
"github.com/portainer/portainer/jwt"
|
"github.com/portainer/portainer/jwt"
|
||||||
|
|
||||||
|
@ -24,7 +25,6 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := &portainer.Settings{
|
settings := &portainer.Settings{
|
||||||
Swarm: *flags.Swarm,
|
|
||||||
HiddenLabels: *flags.Labels,
|
HiddenLabels: *flags.Labels,
|
||||||
Logo: *flags.Logo,
|
Logo: *flags.Logo,
|
||||||
}
|
}
|
||||||
|
@ -41,25 +41,47 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileService, err := file.NewService(*flags.Data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
var cryptoService portainer.CryptoService = &crypto.Service{}
|
var cryptoService portainer.CryptoService = &crypto.Service{}
|
||||||
|
|
||||||
endpointConfiguration := &portainer.EndpointConfiguration{
|
// Initialize the active endpoint from the CLI only if there is no
|
||||||
Endpoint: *flags.Endpoint,
|
// active endpoint defined yet.
|
||||||
TLS: *flags.TLSVerify,
|
var activeEndpoint *portainer.Endpoint
|
||||||
TLSCACertPath: *flags.TLSCacert,
|
if *flags.Endpoint != "" {
|
||||||
TLSCertPath: *flags.TLSCert,
|
activeEndpoint, err = store.EndpointService.GetActive()
|
||||||
TLSKeyPath: *flags.TLSKey,
|
if err == portainer.ErrEndpointNotFound {
|
||||||
|
activeEndpoint = &portainer.Endpoint{
|
||||||
|
Name: "primary",
|
||||||
|
URL: *flags.Endpoint,
|
||||||
|
TLS: *flags.TLSVerify,
|
||||||
|
TLSCACertPath: *flags.TLSCacert,
|
||||||
|
TLSCertPath: *flags.TLSCert,
|
||||||
|
TLSKeyPath: *flags.TLSKey,
|
||||||
|
}
|
||||||
|
err = store.EndpointService.CreateEndpoint(activeEndpoint)
|
||||||
|
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,
|
||||||
UserService: store.UserService,
|
UserService: store.UserService,
|
||||||
CryptoService: cryptoService,
|
EndpointService: store.EndpointService,
|
||||||
JWTService: jwtService,
|
CryptoService: cryptoService,
|
||||||
EndpointConfig: endpointConfiguration,
|
JWTService: jwtService,
|
||||||
|
FileService: fileService,
|
||||||
|
ActiveEndpoint: activeEndpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||||
|
|
|
@ -10,6 +10,12 @@ const (
|
||||||
ErrUserNotFound = Error("User not found")
|
ErrUserNotFound = Error("User not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Endpoint errors.
|
||||||
|
const (
|
||||||
|
ErrEndpointNotFound = Error("Endpoint not found")
|
||||||
|
ErrNoActiveEndpoint = Error("Undefined Docker endpoint")
|
||||||
|
)
|
||||||
|
|
||||||
// Crypto errors.
|
// Crypto errors.
|
||||||
const (
|
const (
|
||||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||||
|
@ -21,6 +27,11 @@ const (
|
||||||
ErrInvalidJWTToken = Error("Invalid JWT token")
|
ErrInvalidJWTToken = Error("Invalid JWT token")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// File errors.
|
||||||
|
const (
|
||||||
|
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
|
||||||
|
)
|
||||||
|
|
||||||
// Error represents an application error.
|
// Error represents an application error.
|
||||||
type Error string
|
type Error string
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
|
||||||
|
TLSStorePath = "tls"
|
||||||
|
// TLSCACertFile represents the name on disk for a TLS CA file.
|
||||||
|
TLSCACertFile = "ca.pem"
|
||||||
|
// TLSCertFile represents the name on disk for a TLS certificate file.
|
||||||
|
TLSCertFile = "cert.pem"
|
||||||
|
// TLSKeyFile represents the name on disk for a TLS key file.
|
||||||
|
TLSKeyFile = "key.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a service for managing files.
|
||||||
|
type Service struct {
|
||||||
|
fileStorePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService initializes a new service.
|
||||||
|
func NewService(fileStorePath string) (*Service, error) {
|
||||||
|
service := &Service{
|
||||||
|
fileStorePath: fileStorePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := service.createFolderInStoreIfNotExist(TLSStorePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r.
|
||||||
|
func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error {
|
||||||
|
ID := strconv.Itoa(int(endpointID))
|
||||||
|
endpointStorePath := path.Join(TLSStorePath, ID)
|
||||||
|
err := service.createFolderInStoreIfNotExist(endpointStorePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName string
|
||||||
|
switch fileType {
|
||||||
|
case portainer.TLSFileCA:
|
||||||
|
fileName = TLSCACertFile
|
||||||
|
case portainer.TLSFileCert:
|
||||||
|
fileName = TLSCertFile
|
||||||
|
case portainer.TLSFileKey:
|
||||||
|
fileName = TLSKeyFile
|
||||||
|
default:
|
||||||
|
return portainer.ErrUndefinedTLSFileType
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsFilePath := path.Join(endpointStorePath, fileName)
|
||||||
|
err = service.createFileInStore(tlsFilePath, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
|
||||||
|
func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType) (string, error) {
|
||||||
|
var fileName string
|
||||||
|
switch fileType {
|
||||||
|
case portainer.TLSFileCA:
|
||||||
|
fileName = TLSCACertFile
|
||||||
|
case portainer.TLSFileCert:
|
||||||
|
fileName = TLSCertFile
|
||||||
|
case portainer.TLSFileKey:
|
||||||
|
fileName = TLSKeyFile
|
||||||
|
default:
|
||||||
|
return "", portainer.ErrUndefinedTLSFileType
|
||||||
|
}
|
||||||
|
ID := strconv.Itoa(int(endpointID))
|
||||||
|
return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
|
||||||
|
func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
|
||||||
|
ID := strconv.Itoa(int(endpointID))
|
||||||
|
endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID)
|
||||||
|
err := os.RemoveAll(endpointPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFolderInStoreIfNotExist creates a new folder in the file store if it doesn't exists on the file system.
|
||||||
|
func (service *Service) createFolderInStoreIfNotExist(name string) error {
|
||||||
|
path := path.Join(service.fileStorePath, name)
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
os.Mkdir(path, 0600)
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFile creates a new file in the file store with the content from r.
|
||||||
|
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||||
|
path := path.Join(service.fileStorePath, filePath)
|
||||||
|
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
_, err = io.Copy(out, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -4,11 +4,12 @@ import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/asaskevich/govalidator"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthHandler represents an HTTP API handler for managing authentication.
|
// AuthHandler represents an HTTP API handler for managing authentication.
|
||||||
|
@ -27,7 +28,7 @@ const (
|
||||||
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAuthHandler returns a new instance of DialHandler.
|
// NewAuthHandler returns a new instance of AuthHandler.
|
||||||
func NewAuthHandler() *AuthHandler {
|
func NewAuthHandler() *AuthHandler {
|
||||||
h := &AuthHandler{
|
h := &AuthHandler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
|
@ -38,8 +39,8 @@ func NewAuthHandler() *AuthHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != http.MethodPost {
|
||||||
handleNotAllowed(w, []string{"POST"})
|
handleNotAllowed(w, []string{http.MethodPost})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package http
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
@ -12,6 +11,8 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||||
|
@ -36,18 +37,22 @@ func NewDockerHandler(middleWareService *middleWareService) *DockerHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
handler.proxy.ServeHTTP(w, r)
|
if handler.proxy != nil {
|
||||||
|
handler.proxy.ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
Error(w, portainer.ErrNoActiveEndpoint, http.StatusNotFound, handler.Logger)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *DockerHandler) setupProxy(config *portainer.EndpointConfiguration) error {
|
func (handler *DockerHandler) setupProxy(endpoint *portainer.Endpoint) error {
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
endpointURL, err := url.Parse(config.Endpoint)
|
endpointURL, err := url.Parse(endpoint.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if endpointURL.Scheme == "tcp" {
|
if endpointURL.Scheme == "tcp" {
|
||||||
if config.TLS {
|
if endpoint.TLS {
|
||||||
proxy, err = newHTTPSProxy(endpointURL, config)
|
proxy, err = newHTTPSProxy(endpointURL, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -65,7 +70,6 @@ func (handler *DockerHandler) setupProxy(config *portainer.EndpointConfiguration
|
||||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||||
|
|
||||||
func singleJoiningSlash(a, b string) string {
|
func singleJoiningSlash(a, b string) string {
|
||||||
aslash := strings.HasSuffix(a, "/")
|
aslash := strings.HasSuffix(a, "/")
|
||||||
bslash := strings.HasPrefix(b, "/")
|
bslash := strings.HasPrefix(b, "/")
|
||||||
|
@ -81,7 +85,6 @@ func singleJoiningSlash(a, b string) string {
|
||||||
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves
|
// HTTP header, which NewSingleHostReverseProxy deliberately preserves
|
||||||
|
|
||||||
func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||||
targetQuery := target.RawQuery
|
targetQuery := target.RawQuery
|
||||||
director := func(req *http.Request) {
|
director := func(req *http.Request) {
|
||||||
|
@ -107,10 +110,10 @@ func newHTTPProxy(u *url.URL) http.Handler {
|
||||||
return NewSingleHostReverseProxyWithHostHeader(u)
|
return NewSingleHostReverseProxyWithHostHeader(u)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPSProxy(u *url.URL, endpointConfig *portainer.EndpointConfiguration) (http.Handler, error) {
|
func newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
proxy := NewSingleHostReverseProxyWithHostHeader(u)
|
proxy := NewSingleHostReverseProxyWithHostHeader(u)
|
||||||
config, err := createTLSConfiguration(endpointConfig.TLSCACertPath, endpointConfig.TLSCertPath, endpointConfig.TLSKeyPath)
|
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,294 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEndpointHandler returns a new instance of EndpointHandler.
|
||||||
|
func NewEndpointHandler(middleWareService *middleWareService) *EndpointHandler {
|
||||||
|
h := &EndpointHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
middleWareService: middleWareService,
|
||||||
|
}
|
||||||
|
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handlePostEndpoints(w, r)
|
||||||
|
}))).Methods(http.MethodPost)
|
||||||
|
h.Handle("/endpoints", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handleGetEndpoints(w, r)
|
||||||
|
}))).Methods(http.MethodGet)
|
||||||
|
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handleGetEndpoint(w, r)
|
||||||
|
}))).Methods(http.MethodGet)
|
||||||
|
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handlePutEndpoint(w, r)
|
||||||
|
}))).Methods(http.MethodPut)
|
||||||
|
h.Handle("/endpoints/{id}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handleDeleteEndpoint(w, r)
|
||||||
|
}))).Methods(http.MethodDelete)
|
||||||
|
h.Handle("/endpoints/{id}/active", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handlePostEndpoint(w, r)
|
||||||
|
}))).Methods(http.MethodPost)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetEndpoints handles GET requests on /endpoints
|
||||||
|
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||||
|
endpoints, err := handler.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encodeJSON(w, endpoints, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostEndpoints handles POST requests on /endpoints
|
||||||
|
// 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) {
|
||||||
|
var req postEndpointsRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := &portainer.Endpoint{
|
||||||
|
Name: req.Name,
|
||||||
|
URL: req.URL,
|
||||||
|
TLS: req.TLS,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.TLS {
|
||||||
|
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||||
|
endpoint.TLSCACertPath = caCertPath
|
||||||
|
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||||
|
endpoint.TLSCertPath = certPath
|
||||||
|
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||||
|
endpoint.TLSKeyPath = keyPath
|
||||||
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeEndpointParameter := r.FormValue("active")
|
||||||
|
if activeEndpointParameter != "" {
|
||||||
|
active, err := strconv.ParseBool(activeEndpointParameter)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if active == true {
|
||||||
|
err = handler.server.updateActiveEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
type postEndpointsRequest struct {
|
||||||
|
Name string `valid:"required"`
|
||||||
|
URL string `valid:"required"`
|
||||||
|
TLS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type postEndpointsResponse struct {
|
||||||
|
ID int `json:"Id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||||
|
// GET /endpoints/0 returns active endpoint
|
||||||
|
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
endpointID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var endpoint *portainer.Endpoint
|
||||||
|
if id == "0" {
|
||||||
|
endpoint, err = handler.EndpointService.GetActive()
|
||||||
|
if err == portainer.ErrEndpointNotFound {
|
||||||
|
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if handler.server.ActiveEndpoint == nil {
|
||||||
|
err = handler.server.updateActiveEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
endpoint, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainer.ErrEndpointNotFound {
|
||||||
|
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encodeJSON(w, endpoint, handler.Logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostEndpoint handles POST requests on /endpoints/:id/active
|
||||||
|
func (handler *EndpointHandler) handlePostEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
endpointID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainer.ErrEndpointNotFound {
|
||||||
|
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.server.updateActiveEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||||
|
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
endpointID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req putEndpointsRequest
|
||||||
|
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = govalidator.ValidateStruct(req)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := &portainer.Endpoint{
|
||||||
|
ID: portainer.EndpointID(endpointID),
|
||||||
|
Name: req.Name,
|
||||||
|
URL: req.URL,
|
||||||
|
TLS: req.TLS,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.TLS {
|
||||||
|
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||||
|
endpoint.TLSCACertPath = caCertPath
|
||||||
|
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||||
|
endpoint.TLSCertPath = certPath
|
||||||
|
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||||
|
endpoint.TLSKeyPath = keyPath
|
||||||
|
} else {
|
||||||
|
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type putEndpointsRequest struct {
|
||||||
|
Name string `valid:"required"`
|
||||||
|
URL string `valid:"required"`
|
||||||
|
TLS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||||
|
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id := vars["id"]
|
||||||
|
|
||||||
|
endpointID, err := strconv.Atoi(id)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainer.ErrEndpointNotFound {
|
||||||
|
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.TLS {
|
||||||
|
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,10 +13,12 @@ import (
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
AuthHandler *AuthHandler
|
AuthHandler *AuthHandler
|
||||||
UserHandler *UserHandler
|
UserHandler *UserHandler
|
||||||
|
EndpointHandler *EndpointHandler
|
||||||
SettingsHandler *SettingsHandler
|
SettingsHandler *SettingsHandler
|
||||||
TemplatesHandler *TemplatesHandler
|
TemplatesHandler *TemplatesHandler
|
||||||
DockerHandler *DockerHandler
|
DockerHandler *DockerHandler
|
||||||
WebSocketHandler *WebSocketHandler
|
WebSocketHandler *WebSocketHandler
|
||||||
|
UploadHandler *UploadHandler
|
||||||
FileHandler http.Handler
|
FileHandler http.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,10 +35,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||||
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
|
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
|
||||||
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/api/endpoints") {
|
||||||
|
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||||
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
|
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
|
||||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||||
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
|
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
|
||||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||||
|
} else if strings.HasPrefix(r.URL.Path, "/api/upload") {
|
||||||
|
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||||
} else if strings.HasPrefix(r.URL.Path, "/api/websocket") {
|
} else if strings.HasPrefix(r.URL.Path, "/api/websocket") {
|
||||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||||
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
|
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
|
||||||
|
|
|
@ -8,14 +8,33 @@ 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
|
||||||
UserService portainer.UserService
|
UserService portainer.UserService
|
||||||
CryptoService portainer.CryptoService
|
EndpointService portainer.EndpointService
|
||||||
JWTService portainer.JWTService
|
CryptoService portainer.CryptoService
|
||||||
Settings *portainer.Settings
|
JWTService portainer.JWTService
|
||||||
TemplatesURL string
|
FileService portainer.FileService
|
||||||
EndpointConfig *portainer.EndpointConfiguration
|
Settings *portainer.Settings
|
||||||
|
TemplatesURL string
|
||||||
|
ActiveEndpoint *portainer.Endpoint
|
||||||
|
Handler *Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) updateActiveEndpoint(endpoint *portainer.Endpoint) error {
|
||||||
|
if endpoint != nil {
|
||||||
|
server.ActiveEndpoint = endpoint
|
||||||
|
server.Handler.WebSocketHandler.endpoint = endpoint
|
||||||
|
err := server.Handler.DockerHandler.setupProxy(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = server.EndpointService.SetActive(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the HTTP server
|
// Start starts the HTTP server
|
||||||
|
@ -23,6 +42,7 @@ func (server *Server) Start() error {
|
||||||
middleWareService := &middleWareService{
|
middleWareService := &middleWareService{
|
||||||
jwtService: server.JWTService,
|
jwtService: server.JWTService,
|
||||||
}
|
}
|
||||||
|
|
||||||
var authHandler = NewAuthHandler()
|
var authHandler = NewAuthHandler()
|
||||||
authHandler.UserService = server.UserService
|
authHandler.UserService = server.UserService
|
||||||
authHandler.CryptoService = server.CryptoService
|
authHandler.CryptoService = server.CryptoService
|
||||||
|
@ -35,19 +55,31 @@ func (server *Server) Start() error {
|
||||||
var templatesHandler = NewTemplatesHandler(middleWareService)
|
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||||
templatesHandler.templatesURL = server.TemplatesURL
|
templatesHandler.templatesURL = server.TemplatesURL
|
||||||
var dockerHandler = NewDockerHandler(middleWareService)
|
var dockerHandler = NewDockerHandler(middleWareService)
|
||||||
dockerHandler.setupProxy(server.EndpointConfig)
|
|
||||||
var websocketHandler = NewWebSocketHandler()
|
var websocketHandler = NewWebSocketHandler()
|
||||||
websocketHandler.endpointConfiguration = server.EndpointConfig
|
// EndpointHandler requires a reference to the server to be able to update the active endpoint.
|
||||||
|
var endpointHandler = NewEndpointHandler(middleWareService)
|
||||||
|
endpointHandler.EndpointService = server.EndpointService
|
||||||
|
endpointHandler.FileService = server.FileService
|
||||||
|
endpointHandler.server = server
|
||||||
|
var uploadHandler = NewUploadHandler(middleWareService)
|
||||||
|
uploadHandler.FileService = server.FileService
|
||||||
var fileHandler = http.FileServer(http.Dir(server.AssetsPath))
|
var fileHandler = http.FileServer(http.Dir(server.AssetsPath))
|
||||||
|
|
||||||
handler := &Handler{
|
server.Handler = &Handler{
|
||||||
AuthHandler: authHandler,
|
AuthHandler: authHandler,
|
||||||
UserHandler: userHandler,
|
UserHandler: userHandler,
|
||||||
|
EndpointHandler: endpointHandler,
|
||||||
SettingsHandler: settingsHandler,
|
SettingsHandler: settingsHandler,
|
||||||
TemplatesHandler: templatesHandler,
|
TemplatesHandler: templatesHandler,
|
||||||
DockerHandler: dockerHandler,
|
DockerHandler: dockerHandler,
|
||||||
WebSocketHandler: websocketHandler,
|
WebSocketHandler: websocketHandler,
|
||||||
FileHandler: fileHandler,
|
FileHandler: fileHandler,
|
||||||
|
UploadHandler: uploadHandler,
|
||||||
}
|
}
|
||||||
return http.ListenAndServe(server.BindAddress, handler)
|
err := server.updateActiveEndpoint(server.ActiveEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.ListenAndServe(server.BindAddress, server.Handler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,11 @@ package http
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SettingsHandler represents an HTTP API handler for managing settings.
|
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||||
|
@ -30,8 +31,8 @@ func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
|
||||||
|
|
||||||
// handleGetSettings handles GET requests on /settings
|
// handleGetSettings handles GET requests on /settings
|
||||||
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" {
|
if r.Method != http.MethodGet {
|
||||||
handleNotAllowed(w, []string{"GET"})
|
handleNotAllowed(w, []string{http.MethodGet})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,12 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||||
|
@ -32,8 +33,8 @@ func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler
|
||||||
|
|
||||||
// handleGetTemplates handles GET requests on /templates
|
// handleGetTemplates handles GET requests on /templates
|
||||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" {
|
if r.Method != http.MethodGet {
|
||||||
handleNotAllowed(w, []string{"GET"})
|
handleNotAllowed(w, []string{http.MethodGet})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UploadHandler represents an HTTP API handler for managing file uploads.
|
||||||
|
type UploadHandler struct {
|
||||||
|
*mux.Router
|
||||||
|
Logger *log.Logger
|
||||||
|
FileService portainer.FileService
|
||||||
|
middleWareService *middleWareService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUploadHandler returns a new instance of UploadHandler.
|
||||||
|
func NewUploadHandler(middleWareService *middleWareService) *UploadHandler {
|
||||||
|
h := &UploadHandler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||||
|
middleWareService: middleWareService,
|
||||||
|
}
|
||||||
|
h.Handle("/upload/tls/{endpointID}/{certificate:(ca|cert|key)}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.handlePostUploadTLS(w, r)
|
||||||
|
})))
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
handleNotAllowed(w, []string{http.MethodPost})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
endpointID := vars["endpointID"]
|
||||||
|
certificate := vars["certificate"]
|
||||||
|
ID, err := strconv.Atoi(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, _, err := r.FormFile("file")
|
||||||
|
defer file.Close()
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileType portainer.TLSFileType
|
||||||
|
switch certificate {
|
||||||
|
case "ca":
|
||||||
|
fileType = portainer.TLSFileCA
|
||||||
|
case "cert":
|
||||||
|
fileType = portainer.TLSFileCert
|
||||||
|
case "key":
|
||||||
|
fileType = portainer.TLSFileKey
|
||||||
|
default:
|
||||||
|
Error(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file)
|
||||||
|
if err != nil {
|
||||||
|
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,11 +4,12 @@ import (
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/asaskevich/govalidator"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserHandler represents an HTTP API handler for managing users.
|
// UserHandler represents an HTTP API handler for managing users.
|
||||||
|
@ -32,10 +33,10 @@ func NewUserHandler(middleWareService *middleWareService) *UserHandler {
|
||||||
})))
|
})))
|
||||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
h.handleGetUser(w, r)
|
h.handleGetUser(w, r)
|
||||||
}))).Methods("GET")
|
}))).Methods(http.MethodGet)
|
||||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
h.handlePutUser(w, r)
|
h.handlePutUser(w, r)
|
||||||
}))).Methods("PUT")
|
}))).Methods(http.MethodPut)
|
||||||
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
h.Handle("/users/{username}/passwd", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
h.handlePostUserPasswd(w, r)
|
h.handlePostUserPasswd(w, r)
|
||||||
})))
|
})))
|
||||||
|
@ -46,8 +47,8 @@ func NewUserHandler(middleWareService *middleWareService) *UserHandler {
|
||||||
|
|
||||||
// handlePostUsers handles POST requests on /users
|
// handlePostUsers handles POST requests on /users
|
||||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != http.MethodPost {
|
||||||
handleNotAllowed(w, []string{"POST"})
|
handleNotAllowed(w, []string{http.MethodPost})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,8 +87,8 @@ type postUsersRequest struct {
|
||||||
|
|
||||||
// handlePostUserPasswd handles POST requests on /users/:username/passwd
|
// handlePostUserPasswd handles POST requests on /users/:username/passwd
|
||||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != http.MethodPost {
|
||||||
handleNotAllowed(w, []string{"POST"})
|
handleNotAllowed(w, []string{http.MethodPost})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,8 +190,8 @@ type putUserRequest struct {
|
||||||
|
|
||||||
// handlePostAdminInit handles GET requests on /users/admin/check
|
// handlePostAdminInit handles GET requests on /users/admin/check
|
||||||
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "GET" {
|
if r.Method != http.MethodGet {
|
||||||
handleNotAllowed(w, []string{"GET"})
|
handleNotAllowed(w, []string{http.MethodGet})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,8 +210,8 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R
|
||||||
|
|
||||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||||
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != "POST" {
|
if r.Method != http.MethodPost {
|
||||||
handleNotAllowed(w, []string{"POST"})
|
handleNotAllowed(w, []string{http.MethodPost})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"golang.org/x/net/websocket"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
@ -17,14 +15,17 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
||||||
type WebSocketHandler struct {
|
type WebSocketHandler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Logger *log.Logger
|
Logger *log.Logger
|
||||||
middleWareService *middleWareService
|
middleWareService *middleWareService
|
||||||
endpointConfiguration *portainer.EndpointConfiguration
|
endpoint *portainer.Endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebSocketHandler returns a new instance of WebSocketHandler.
|
// NewWebSocketHandler returns a new instance of WebSocketHandler.
|
||||||
|
@ -42,7 +43,7 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||||
execID := qry.Get("id")
|
execID := qry.Get("id")
|
||||||
|
|
||||||
// Should not be managed here
|
// Should not be managed here
|
||||||
endpoint, err := url.Parse(handler.endpointConfiguration.Endpoint)
|
endpoint, err := url.Parse(handler.endpoint.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Unable to parse endpoint URL: %s", err)
|
log.Fatalf("Unable to parse endpoint URL: %s", err)
|
||||||
return
|
return
|
||||||
|
@ -57,10 +58,10 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||||
|
|
||||||
// Should not be managed here
|
// Should not be managed here
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
if handler.endpointConfiguration.TLS {
|
if handler.endpoint.TLS {
|
||||||
tlsConfig, err = createTLSConfiguration(handler.endpointConfiguration.TLSCACertPath,
|
tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath,
|
||||||
handler.endpointConfiguration.TLSCertPath,
|
handler.endpoint.TLSCertPath,
|
||||||
handler.endpointConfiguration.TLSKeyPath)
|
handler.endpoint.TLSKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package portainer
|
package portainer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Pair defines a key/value string pair
|
// Pair defines a key/value string pair
|
||||||
Pair struct {
|
Pair struct {
|
||||||
|
@ -15,7 +19,6 @@ type (
|
||||||
Endpoint *string
|
Endpoint *string
|
||||||
Labels *[]Pair
|
Labels *[]Pair
|
||||||
Logo *string
|
Logo *string
|
||||||
Swarm *bool
|
|
||||||
Templates *string
|
Templates *string
|
||||||
TLSVerify *bool
|
TLSVerify *bool
|
||||||
TLSCacert *string
|
TLSCacert *string
|
||||||
|
@ -25,15 +28,14 @@ type (
|
||||||
|
|
||||||
// Settings represents Portainer settings.
|
// Settings represents Portainer settings.
|
||||||
Settings struct {
|
Settings struct {
|
||||||
Swarm bool `json:"swarm"`
|
|
||||||
HiddenLabels []Pair `json:"hiddenLabels"`
|
HiddenLabels []Pair `json:"hiddenLabels"`
|
||||||
Logo string `json:"logo"`
|
Logo string `json:"logo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// User represent a user account.
|
// User represent a user account.
|
||||||
User struct {
|
User struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"Username"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"Password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenData represents the data embedded in a JWT token.
|
// TokenData represents the data embedded in a JWT token.
|
||||||
|
@ -41,15 +43,25 @@ type (
|
||||||
Username string
|
Username string
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointConfiguration represents the data required to connect to a Docker API endpoint.
|
// EndpointID represents an endpoint identifier.
|
||||||
EndpointConfiguration struct {
|
EndpointID int
|
||||||
Endpoint string
|
|
||||||
TLS bool
|
// Endpoint represents a Docker endpoint with all the info required
|
||||||
TLSCACertPath string
|
// to connect to it.
|
||||||
TLSCertPath string
|
Endpoint struct {
|
||||||
TLSKeyPath string
|
ID EndpointID `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
URL string `json:"URL"`
|
||||||
|
TLS bool `json:"TLS"`
|
||||||
|
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||||
|
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||||
|
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLSFileType represents a type of TLS file required to connect to a Docker endpoint.
|
||||||
|
// It can be either a TLS CA file, a TLS certificate file or a TLS key file.
|
||||||
|
TLSFileType int
|
||||||
|
|
||||||
// CLIService represents a service for managing CLI.
|
// CLIService represents a service for managing CLI.
|
||||||
CLIService interface {
|
CLIService interface {
|
||||||
ParseFlags(version string) (*CLIFlags, error)
|
ParseFlags(version string) (*CLIFlags, error)
|
||||||
|
@ -73,6 +85,17 @@ type (
|
||||||
UpdateUser(user *User) error
|
UpdateUser(user *User) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EndpointService represents a service for managing endpoints.
|
||||||
|
EndpointService interface {
|
||||||
|
Endpoint(ID EndpointID) (*Endpoint, error)
|
||||||
|
Endpoints() ([]Endpoint, error)
|
||||||
|
CreateEndpoint(endpoint *Endpoint) error
|
||||||
|
UpdateEndpoint(ID EndpointID, endpoint *Endpoint) error
|
||||||
|
DeleteEndpoint(ID EndpointID) error
|
||||||
|
GetActive() (*Endpoint, error)
|
||||||
|
SetActive(endpoint *Endpoint) error
|
||||||
|
}
|
||||||
|
|
||||||
// CryptoService represents a service for encrypting/hashing data.
|
// CryptoService represents a service for encrypting/hashing data.
|
||||||
CryptoService interface {
|
CryptoService interface {
|
||||||
Hash(data string) (string, error)
|
Hash(data string) (string, error)
|
||||||
|
@ -84,9 +107,25 @@ type (
|
||||||
GenerateToken(data *TokenData) (string, error)
|
GenerateToken(data *TokenData) (string, error)
|
||||||
VerifyToken(token string) error
|
VerifyToken(token string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileService represents a service for managing files.
|
||||||
|
FileService interface {
|
||||||
|
StoreTLSFile(endpointID EndpointID, fileType TLSFileType, r io.Reader) error
|
||||||
|
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
|
||||||
|
DeleteTLSFiles(endpointID EndpointID) error
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of portainer API.
|
// APIVersion is the version number of portainer API.
|
||||||
APIVersion = "1.10.2"
|
APIVersion = "1.10.2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TLSFileCA represents a TLS CA certificate file.
|
||||||
|
TLSFileCA TLSFileType = iota
|
||||||
|
// TLSFileCert represents a TLS certificate file.
|
||||||
|
TLSFileCert
|
||||||
|
// TLSFileKey represents a TLS key file.
|
||||||
|
TLSFileKey
|
||||||
|
)
|
||||||
|
|
55
app/app.js
55
app/app.js
|
@ -5,6 +5,7 @@ angular.module('portainer', [
|
||||||
'ui.select',
|
'ui.select',
|
||||||
'ngCookies',
|
'ngCookies',
|
||||||
'ngSanitize',
|
'ngSanitize',
|
||||||
|
'ngFileUpload',
|
||||||
'angularUtils.directives.dirPagination',
|
'angularUtils.directives.dirPagination',
|
||||||
'LocalStorageModule',
|
'LocalStorageModule',
|
||||||
'angular-jwt',
|
'angular-jwt',
|
||||||
|
@ -19,6 +20,9 @@ angular.module('portainer', [
|
||||||
'containers',
|
'containers',
|
||||||
'createContainer',
|
'createContainer',
|
||||||
'docker',
|
'docker',
|
||||||
|
'endpoint',
|
||||||
|
'endpointInit',
|
||||||
|
'endpoints',
|
||||||
'events',
|
'events',
|
||||||
'images',
|
'images',
|
||||||
'image',
|
'image',
|
||||||
|
@ -270,6 +274,50 @@ angular.module('portainer', [
|
||||||
requiresLogin: true
|
requiresLogin: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.state('endpoints', {
|
||||||
|
url: '/endpoints/',
|
||||||
|
views: {
|
||||||
|
"content": {
|
||||||
|
templateUrl: 'app/components/endpoints/endpoints.html',
|
||||||
|
controller: 'EndpointsController'
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||||
|
controller: 'SidebarController'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
requiresLogin: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('endpoint', {
|
||||||
|
url: '^/endpoints/:id',
|
||||||
|
views: {
|
||||||
|
"content": {
|
||||||
|
templateUrl: 'app/components/endpoint/endpoint.html',
|
||||||
|
controller: 'EndpointController'
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||||
|
controller: 'SidebarController'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
requiresLogin: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('endpointInit', {
|
||||||
|
url: '/init/endpoint',
|
||||||
|
views: {
|
||||||
|
"content": {
|
||||||
|
templateUrl: 'app/components/endpointInit/endpointInit.html',
|
||||||
|
controller: 'EndpointInitController'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
requiresLogin: true
|
||||||
|
}
|
||||||
|
})
|
||||||
.state('events', {
|
.state('events', {
|
||||||
url: '/events/',
|
url: '/events/',
|
||||||
views: {
|
views: {
|
||||||
|
@ -491,18 +539,19 @@ angular.module('portainer', [
|
||||||
});
|
});
|
||||||
|
|
||||||
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
|
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {
|
||||||
if ((fromState.name === 'auth' || fromState.name === '') && Authentication.isAuthenticated()) {
|
if (toState.name !== 'endpointInit' && (fromState.name === 'auth' || fromState.name === '' || fromState.name === 'endpointInit') && Authentication.isAuthenticated()) {
|
||||||
EndpointMode.determineEndpointMode();
|
EndpointMode.determineEndpointMode();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}])
|
}])
|
||||||
// This is your docker url that the api will use to make requests
|
// This is your docker url that the api will use to make requests
|
||||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||||
.constant('DOCKER_ENDPOINT', '/api/docker')
|
|
||||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||||
.constant('CONFIG_ENDPOINT', '/api/settings')
|
.constant('DOCKER_ENDPOINT', '/api/docker')
|
||||||
|
.constant('CONFIG_ENDPOINT', '/api/settings')
|
||||||
.constant('AUTH_ENDPOINT', '/api/auth')
|
.constant('AUTH_ENDPOINT', '/api/auth')
|
||||||
.constant('USERS_ENDPOINT', '/api/users')
|
.constant('USERS_ENDPOINT', '/api/users')
|
||||||
|
.constant('ENDPOINTS_ENDPOINT', '/api/endpoints')
|
||||||
.constant('TEMPLATES_ENDPOINT', '/api/templates')
|
.constant('TEMPLATES_ENDPOINT', '/api/templates')
|
||||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||||
.constant('UI_VERSION', 'v1.10.2');
|
.constant('UI_VERSION', 'v1.10.2');
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<div class="login-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- login box -->
|
<!-- login box -->
|
||||||
<div class="container login-box">
|
<div class="container simple-box">
|
||||||
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
||||||
<!-- login box logo -->
|
<!-- login box logo -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<img ng-if="logo" ng-src="{{ logo }}" class="login-logo">
|
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||||
<img ng-if="!logo" src="images/logo_alt.png" class="login-logo" alt="Portainer">
|
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||||
</div>
|
</div>
|
||||||
<!-- !login box logo -->
|
<!-- !login box logo -->
|
||||||
<!-- init password panel -->
|
<!-- init password panel -->
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('auth', [])
|
angular.module('auth', [])
|
||||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'Messages',
|
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'Messages',
|
||||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, Messages) {
|
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, Messages) {
|
||||||
|
|
||||||
$scope.authData = {
|
$scope.authData = {
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
|
@ -58,10 +58,17 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
|
||||||
$scope.authenticationError = false;
|
$scope.authenticationError = false;
|
||||||
var username = $sanitize($scope.authData.username);
|
var username = $sanitize($scope.authData.username);
|
||||||
var password = $sanitize($scope.authData.password);
|
var password = $sanitize($scope.authData.password);
|
||||||
Authentication.login(username, password)
|
Authentication.login(username, password).then(function success() {
|
||||||
.then(function() {
|
EndpointService.getActive().then(function success(data) {
|
||||||
$state.go('dashboard');
|
$state.go('dashboard');
|
||||||
}, function() {
|
}, function error(err) {
|
||||||
|
if (err.status === 404) {
|
||||||
|
$state.go('endpointInit');
|
||||||
|
} else {
|
||||||
|
Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, function error() {
|
||||||
$scope.authData.error = 'Invalid credentials';
|
$scope.authData.error = 'Invalid credentials';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -152,7 +152,7 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
|
||||||
|
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
$scope.containersToHideLabels = c.hiddenLabels;
|
$scope.containersToHideLabels = c.hiddenLabels;
|
||||||
if (c.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
|
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||||
Info.get({}, function (d) {
|
Info.get({}, function (d) {
|
||||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
||||||
update({all: Settings.displayAll ? 1 : 0});
|
update({all: Settings.displayAll ? 1 : 0});
|
||||||
|
|
|
@ -62,7 +62,6 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
||||||
};
|
};
|
||||||
|
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
$scope.swarm = c.swarm;
|
|
||||||
var containersToHideLabels = c.hiddenLabels;
|
var containersToHideLabels = c.hiddenLabels;
|
||||||
|
|
||||||
Volume.query({}, function (d) {
|
Volume.query({}, function (d) {
|
||||||
|
@ -73,7 +72,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
||||||
|
|
||||||
Network.query({}, function (d) {
|
Network.query({}, function (d) {
|
||||||
var networks = d;
|
var networks = d;
|
||||||
if ($scope.swarm) {
|
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||||
networks = d.filter(function (network) {
|
networks = d.filter(function (network) {
|
||||||
if (network.Scope === 'global') {
|
if (network.Scope === 'global') {
|
||||||
return network;
|
return network;
|
||||||
|
@ -221,7 +220,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
||||||
var containerName = container;
|
var containerName = container;
|
||||||
if (container && typeof container === 'object') {
|
if (container && typeof container === 'object') {
|
||||||
containerName = $filter('trimcontainername')(container.Names[0]);
|
containerName = $filter('trimcontainername')(container.Names[0]);
|
||||||
if ($scope.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
|
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||||
containerName = $filter('swarmcontainername')(container);
|
containerName = $filter('swarmcontainername')(container);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group col-sm-1 input-group-sm">
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
<select class="selectpicker form-control" ng-model="portBinding.protocol">
|
<select class="form-control" ng-model="portBinding.protocol">
|
||||||
<option value="tcp">tcp</option>
|
<option value="tcp">tcp</option>
|
||||||
<option value="udp">udp</option>
|
<option value="udp">udp</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -243,7 +243,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
<span class="input-group-addon"><input type="checkbox" ng-model="volume.isPath" ng-click="resetVolumePath($index)">Path</span>
|
<span class="input-group-addon"><input type="checkbox" ng-model="volume.isPath" ng-click="resetVolumePath($index)">Path</span>
|
||||||
<select class="selectpicker form-control" ng-model="volume.name" ng-if="!volume.isPath">
|
<select class="form-control" ng-model="volume.name" ng-if="!volume.isPath">
|
||||||
<option selected disabled hidden value="">Select a volume</option>
|
<option selected disabled hidden value="">Select a volume</option>
|
||||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -278,7 +278,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<select class="selectpicker form-control" ng-model="config.HostConfig.NetworkMode" id="container_network">
|
<select class="form-control" ng-model="config.HostConfig.NetworkMode" id="container_network">
|
||||||
<option selected disabled hidden value="">Select a network</option>
|
<option selected disabled hidden value="">Select a network</option>
|
||||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -289,10 +289,10 @@
|
||||||
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
|
<div class="form-group" ng-if="config.HostConfig.NetworkMode == 'container'">
|
||||||
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
|
<label for="container_network_container" class="col-sm-1 control-label text-left">Container</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
|
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
|
||||||
<option selected disabled hidden value="">Select a container</option>
|
<option selected disabled hidden value="">Select a container</option>
|
||||||
</select>
|
</select>
|
||||||
<select ng-if="endpointMode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="formValues.NetworkContainer">
|
<select ng-if="endpointMode.provider === 'DOCKER_SWARM'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="formValues.NetworkContainer">
|
||||||
<option selected disabled hidden value="">Select a container</option>
|
<option selected disabled hidden value="">Select a container</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
<input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80">
|
<input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group col-sm-1 input-group-sm">
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
<select class="selectpicker form-control" ng-model="portBinding.Protocol">
|
<select class="form-control" ng-model="portBinding.Protocol">
|
||||||
<option value="tcp">tcp</option>
|
<option value="tcp">tcp</option>
|
||||||
<option value="udp">udp</option>
|
<option value="udp">udp</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -192,7 +192,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group col-sm-5 input-group-sm">
|
<div class="input-group col-sm-5 input-group-sm">
|
||||||
<span class="input-group-addon"><input type="checkbox" ng-model="volume.Bind">bind</span>
|
<span class="input-group-addon"><input type="checkbox" ng-model="volume.Bind">bind</span>
|
||||||
<select class="selectpicker form-control" ng-model="volume.Source" ng-if="!volume.Bind">
|
<select class="form-control" ng-model="volume.Source" ng-if="!volume.Bind">
|
||||||
<option selected disabled hidden value="">Select a volume</option>
|
<option selected disabled hidden value="">Select a volume</option>
|
||||||
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -222,7 +222,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<select class="selectpicker form-control" ng-model="formValues.Network">
|
<select class="form-control" ng-model="formValues.Network">
|
||||||
<option selected disabled hidden value="">Select a network</option>
|
<option selected disabled hidden value="">Select a network</option>
|
||||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -247,7 +247,7 @@
|
||||||
<i class="fa fa-minus" aria-hidden="true"></i>
|
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<select class="selectpicker form-control" ng-model="network.Name">
|
<select class="form-control" ng-model="network.Name">
|
||||||
<option selected disabled hidden value="">Select a network</option>
|
<option selected disabled hidden value="">Select a network</option>
|
||||||
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
<option ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -87,7 +87,6 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
$scope.swarm = c.swarm;
|
|
||||||
fetchDashboardData(c.hiddenLabels);
|
fetchDashboardData(c.hiddenLabels);
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Endpoint details">
|
||||||
|
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||||
|
</rd-header-title>
|
||||||
|
<rd-header-content>
|
||||||
|
<a ui-sref="endpoints">Endpoints</a> > <a ui-sref="endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
|
||||||
|
</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<!-- name-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="container_name" ng-model="endpoint.Name" placeholder="e.g. docker-prod01">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input -->
|
||||||
|
<!-- endpoint-url-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="endpoint_url" class="col-sm-2 control-label text-left">Endpoint URL</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input ng-disabled="endpointType === 'local'" type="text" class="form-control" id="endpoint_url" ng-model="endpoint.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !endpoint-url-input -->
|
||||||
|
<!-- tls-checkbox -->
|
||||||
|
<div class="form-group" ng-if="endpointType === 'remote'">
|
||||||
|
<label for="tls" class="col-sm-2 control-label text-left">TLS</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="checkbox" name="tls" ng-model="endpoint.TLS">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !tls-checkbox -->
|
||||||
|
<!-- tls-certs -->
|
||||||
|
<div ng-if="endpoint.TLS">
|
||||||
|
<!-- ca-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
<span ng-if="formValues.TLSCACert !== endpoint.TLSCACert">{{ formValues.TLSCACert.name }}</span>
|
||||||
|
<i class="fa fa-check green-icon" ng-if="formValues.TLSCACert && formValues.TLSCACert === endpoint.TLSCACert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !ca-input -->
|
||||||
|
<!-- cert-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
<span ng-if="formValues.TLSCert !== endpoint.TLSCert">{{ formValues.TLSCert.name }}</span>
|
||||||
|
<i class="fa fa-check green-icon" ng-if="formValues.TLSCert && formValues.TLSCert === endpoint.TLSCert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !cert-input -->
|
||||||
|
<!-- key-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-2 control-label text-left">TLS key</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
<span ng-if="formValues.TLSKey !== endpoint.TLSKey">{{ formValues.TLSKey.name }}</span>
|
||||||
|
<i class="fa fa-check green-icon" ng-if="formValues.TLSKey && formValues.TLSKey === endpoint.TLSKey" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !key-input -->
|
||||||
|
</div>
|
||||||
|
<!-- !tls-certs -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="updateEndpoint()">Update endpoint</button>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ui-sref="endpoints">Cancel</a>
|
||||||
|
<i id="updateEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
||||||
|
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,55 @@
|
||||||
|
angular.module('endpoint', [])
|
||||||
|
.controller('EndpointController', ['$scope', '$state', '$stateParams', '$filter', 'EndpointService', 'Messages',
|
||||||
|
function ($scope, $state, $stateParams, $filter, EndpointService, Messages) {
|
||||||
|
$scope.state = {
|
||||||
|
error: '',
|
||||||
|
uploadInProgress: false
|
||||||
|
};
|
||||||
|
$scope.formValues = {
|
||||||
|
TLSCACert: null,
|
||||||
|
TLSCert: null,
|
||||||
|
TLSKey: null
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.updateEndpoint = function() {
|
||||||
|
var ID = $scope.endpoint.Id;
|
||||||
|
var name = $scope.endpoint.Name;
|
||||||
|
var URL = $scope.endpoint.URL;
|
||||||
|
var TLS = $scope.endpoint.TLS;
|
||||||
|
var TLSCACert = $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null;
|
||||||
|
var TLSCert = $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null;
|
||||||
|
var TLSKey = $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null;
|
||||||
|
EndpointService.updateEndpoint(ID, name, URL, TLS, TLSCACert, TLSCert, TLSKey).then(function success(data) {
|
||||||
|
Messages.send("Endpoint updated", $scope.endpoint.Name);
|
||||||
|
$state.go('endpoints');
|
||||||
|
}, function error(err) {
|
||||||
|
$scope.state.error = err.msg;
|
||||||
|
}, function update(evt) {
|
||||||
|
if (evt.upload) {
|
||||||
|
$scope.state.uploadInProgress = evt.upload;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEndpoint(endpointID) {
|
||||||
|
$('#loadingViewSpinner').show();
|
||||||
|
EndpointService.endpoint($stateParams.id).then(function success(data) {
|
||||||
|
$('#loadingViewSpinner').hide();
|
||||||
|
$scope.endpoint = data;
|
||||||
|
if (data.URL.indexOf("unix://") === 0) {
|
||||||
|
$scope.endpointType = 'local';
|
||||||
|
} else {
|
||||||
|
$scope.endpointType = 'remote';
|
||||||
|
}
|
||||||
|
$scope.endpoint.URL = $filter('stripprotocol')(data.URL);
|
||||||
|
$scope.formValues.TLSCACert = data.TLSCACert;
|
||||||
|
$scope.formValues.TLSCert = data.TLSCert;
|
||||||
|
$scope.formValues.TLSKey = data.TLSKey;
|
||||||
|
}, function error(err) {
|
||||||
|
$('#loadingViewSpinner').hide();
|
||||||
|
Messages.error("Failure", err, "Unable to retrieve endpoint details");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndpoint($stateParams.id);
|
||||||
|
}]);
|
|
@ -0,0 +1,139 @@
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<!-- simple box -->
|
||||||
|
<div class="container simple-box">
|
||||||
|
<div class="col-md-8 col-md-offset-2 col-sm-8 col-sm-offset-2">
|
||||||
|
<!-- simple box logo -->
|
||||||
|
<div class="row">
|
||||||
|
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||||
|
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||||
|
</div>
|
||||||
|
<!-- !simple box logo -->
|
||||||
|
<!-- init-endpoint panel -->
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-body">
|
||||||
|
<!-- init-endpoint form -->
|
||||||
|
<form class="form-horizontal" style="margin: 20px;" enctype="multipart/form-data" method="POST">
|
||||||
|
<!-- comment -->
|
||||||
|
<div class="form-group">
|
||||||
|
<p>Connect Portainer to a Docker engine or Swarm cluster endpoint.</p>
|
||||||
|
</div>
|
||||||
|
<!-- !comment input -->
|
||||||
|
<!-- endpoin-type radio -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="radio">
|
||||||
|
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType">Manage the Docker instance where Portainer is running</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType">Manage a remote Docker instance</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- endpoint-type radio -->
|
||||||
|
<!-- local-endpoint -->
|
||||||
|
<div ng-if="formValues.endpointType === 'local'" style="margin-top: 25px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<span class="small text-muted">Note: ensure that the Docker socket is bind mounted in the Portainer container at <code>/var/run/docker.sock</code></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- connect button -->
|
||||||
|
<div class="form-group" style="margin-top: 10px;">
|
||||||
|
<div class="col-sm-12 controls">
|
||||||
|
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
||||||
|
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||||
|
</p>
|
||||||
|
<button type="submit" class="btn btn-primary pull-right" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !connect button -->
|
||||||
|
</div>
|
||||||
|
<!-- !local-endpoint -->
|
||||||
|
<!-- remote-endpoint -->
|
||||||
|
<div ng-if="formValues.endpointType === 'remote'" style="margin-top: 25px;">
|
||||||
|
<!-- name-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_name" class="col-sm-3 control-label text-left">Name</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input -->
|
||||||
|
<!-- endpoint-url-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="endpoint_url" class="col-sm-3 control-label text-left">Endpoint URL</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !endpoint-url-input -->
|
||||||
|
<!-- tls-checkbox -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tls" class="col-sm-3 control-label text-left">TLS</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input type="checkbox" name="tls" ng-model="formValues.TLS">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !tls-checkbox -->
|
||||||
|
<!-- tls-certs -->
|
||||||
|
<div ng-if="formValues.TLS">
|
||||||
|
<!-- ca-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label text-left">TLS CA certificate</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.TLSCACert.name }}
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !ca-input -->
|
||||||
|
<!-- cert-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tls_cert" class="col-sm-3 control-label text-left">TLS certificate</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.TLSCert.name }}
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !cert-input -->
|
||||||
|
<!-- key-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-3 control-label text-left">TLS key</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.TLSKey.name }}
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !key-input -->
|
||||||
|
</div>
|
||||||
|
<!-- !tls-certs -->
|
||||||
|
<!-- connect button -->
|
||||||
|
<div class="form-group" style="margin-top: 10px;">
|
||||||
|
<div class="col-sm-12 controls">
|
||||||
|
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
|
||||||
|
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||||
|
</p>
|
||||||
|
<button type="submit" class="btn btn-primary pull-right" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !connect button -->
|
||||||
|
</div>
|
||||||
|
<!-- !remote-endpoint -->
|
||||||
|
</form>
|
||||||
|
<!-- !init-endpoint form -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !init-endpoint panel -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !simple box -->
|
||||||
|
</div>
|
|
@ -0,0 +1,57 @@
|
||||||
|
angular.module('endpointInit', [])
|
||||||
|
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'Messages',
|
||||||
|
function ($scope, $state, EndpointService, Messages) {
|
||||||
|
$scope.state = {
|
||||||
|
error: '',
|
||||||
|
uploadInProgress: false
|
||||||
|
};
|
||||||
|
$scope.formValues = {
|
||||||
|
endpointType: "remote",
|
||||||
|
Name: '',
|
||||||
|
URL: '',
|
||||||
|
TLS: false,
|
||||||
|
TLSCACert: null,
|
||||||
|
TLSCert: null,
|
||||||
|
TLSKey: null
|
||||||
|
};
|
||||||
|
|
||||||
|
EndpointService.getActive().then(function success(data) {
|
||||||
|
$state.go('dashboard');
|
||||||
|
}, function error(err) {
|
||||||
|
if (err.status !== 404) {
|
||||||
|
Messages.error("Failure", err, 'Unable to verify Docker endpoint existence');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.createLocalEndpoint = function() {
|
||||||
|
$scope.state.error = '';
|
||||||
|
var name = "local";
|
||||||
|
var URL = "unix:///var/run/docker.sock";
|
||||||
|
var TLS = false;
|
||||||
|
EndpointService.createLocalEndpoint(name, URL, TLS, true).then(function success(data) {
|
||||||
|
$state.go('dashboard');
|
||||||
|
}, function error(err) {
|
||||||
|
$scope.state.error = 'Unable to create endpoint';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.createRemoteEndpoint = function() {
|
||||||
|
$scope.state.error = '';
|
||||||
|
var name = $scope.formValues.Name;
|
||||||
|
var URL = $scope.formValues.URL;
|
||||||
|
var TLS = $scope.formValues.TLS;
|
||||||
|
var TLSCAFile = $scope.formValues.TLSCACert;
|
||||||
|
var TLSCertFile = $scope.formValues.TLSCert;
|
||||||
|
var TLSKeyFile = $scope.formValues.TLSKey;
|
||||||
|
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, TLS ? false : true).then(function success(data) {
|
||||||
|
$state.go('dashboard');
|
||||||
|
}, function error(err) {
|
||||||
|
$scope.state.uploadInProgress = false;
|
||||||
|
$scope.state.error = err.msg;
|
||||||
|
}, function update(evt) {
|
||||||
|
if (evt.upload) {
|
||||||
|
$scope.state.uploadInProgress = evt.upload;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -0,0 +1,175 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title="Endpoints">
|
||||||
|
<a data-toggle="tooltip" title="Refresh" ui-sref="endpoints" ui-sref-opts="{reload: true}">
|
||||||
|
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</rd-header-title>
|
||||||
|
<rd-header-content>Endpoint management</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-plus" title="Add a new endpoint">
|
||||||
|
</rd-widget-header>
|
||||||
|
<rd-widget-body>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<!-- name-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="container_name" class="col-sm-2 control-label text-left">Name</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !name-input -->
|
||||||
|
<!-- endpoint-url-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="endpoint_url" class="col-sm-2 control-label text-left">Endpoint URL</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !endpoint-url-input -->
|
||||||
|
<!-- tls-checkbox -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tls" class="col-sm-2 control-label text-left">TLS</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="checkbox" name="tls" ng-model="formValues.TLS">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !tls-checkbox -->
|
||||||
|
<!-- tls-certs -->
|
||||||
|
<div ng-if="formValues.TLS">
|
||||||
|
<!-- ca-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.TLSCACert.name }}
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !ca-input -->
|
||||||
|
<!-- cert-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.TLSCert.name }}
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !cert-input -->
|
||||||
|
<!-- key-input -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-2 control-label text-left">TLS key</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||||
|
<span style="margin-left: 5px;">
|
||||||
|
{{ formValues.TLSKey.name }}
|
||||||
|
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||||
|
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !key-input -->
|
||||||
|
</div>
|
||||||
|
<!-- !tls-certs -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="addEndpoint()">Add endpoint</button>
|
||||||
|
<i id="createEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
||||||
|
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-header icon="fa-plug" title="Endpoints">
|
||||||
|
<div class="pull-right">
|
||||||
|
<i id="loadEndpointsSpinner" class="fa fa-cog fa-2x fa-spin" style="margin-top: 5px;"></i>
|
||||||
|
</div>
|
||||||
|
</rd-widget-header>
|
||||||
|
<rd-widget-taskbar classes="col-lg-12">
|
||||||
|
<div class="pull-left">
|
||||||
|
<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">
|
||||||
|
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||||
|
</div>
|
||||||
|
</rd-widget-taskbar>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="endpoints" ng-click="order('Name')">
|
||||||
|
Name
|
||||||
|
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="endpoints" ng-click="order('URL')">
|
||||||
|
URL
|
||||||
|
<span ng-show="sortType == 'URL' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'URL' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ui-sref="endpoints" ng-click="order('TLS')">
|
||||||
|
TLS
|
||||||
|
<span ng-show="sortType == 'TLS' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'TLS' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr dir-paginate="endpoint in (state.filteredEndpoints = (endpoints | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||||
|
<td><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>
|
||||||
|
<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>
|
||||||
|
<span class="small text-muted" ng-if="endpoint.Id === activeEndpoint.Id">
|
||||||
|
<i class="fa fa-lock" aria-hidden="true"></i> You cannot edit the active endpoint
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="!endpoints">
|
||||||
|
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="endpoints.length == 0">
|
||||||
|
<td colspan="5" class="text-center text-muted">No endpoints available.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div ng-if="endpoints" class="pull-left pagination-controls">
|
||||||
|
<dir-pagination-controls></dir-pagination-controls>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
<rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,100 @@
|
||||||
|
angular.module('endpoints', [])
|
||||||
|
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'Settings', 'Messages',
|
||||||
|
function ($scope, $state, EndpointService, Settings, Messages) {
|
||||||
|
$scope.state = {
|
||||||
|
error: '',
|
||||||
|
uploadInProgress: false,
|
||||||
|
selectedItemCount: 0
|
||||||
|
};
|
||||||
|
$scope.sortType = 'Name';
|
||||||
|
$scope.sortReverse = true;
|
||||||
|
$scope.pagination_count = Settings.pagination_count;
|
||||||
|
|
||||||
|
$scope.formValues = {
|
||||||
|
Name: '',
|
||||||
|
URL: '',
|
||||||
|
TLS: false,
|
||||||
|
TLSCACert: null,
|
||||||
|
TLSCert: null,
|
||||||
|
TLSKey: null
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.order = function(sortType) {
|
||||||
|
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||||
|
$scope.sortType = sortType;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.selectItem = function (item) {
|
||||||
|
if (item.Checked) {
|
||||||
|
$scope.state.selectedItemCount++;
|
||||||
|
} else {
|
||||||
|
$scope.state.selectedItemCount--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addEndpoint = function() {
|
||||||
|
$scope.state.error = '';
|
||||||
|
var name = $scope.formValues.Name;
|
||||||
|
var URL = $scope.formValues.URL;
|
||||||
|
var TLS = $scope.formValues.TLS;
|
||||||
|
var TLSCAFile = $scope.formValues.TLSCACert;
|
||||||
|
var TLSCertFile = $scope.formValues.TLSCert;
|
||||||
|
var TLSKeyFile = $scope.formValues.TLSKey;
|
||||||
|
EndpointService.createRemoteEndpoint(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, false).then(function success(data) {
|
||||||
|
Messages.send("Endpoint created", name);
|
||||||
|
$state.reload();
|
||||||
|
}, function error(err) {
|
||||||
|
$scope.state.uploadInProgress = false;
|
||||||
|
$scope.state.error = err.msg;
|
||||||
|
}, function update(evt) {
|
||||||
|
if (evt.upload) {
|
||||||
|
$scope.state.uploadInProgress = evt.upload;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeAction = function () {
|
||||||
|
$('#loadEndpointsSpinner').show();
|
||||||
|
var counter = 0;
|
||||||
|
var complete = function () {
|
||||||
|
counter = counter - 1;
|
||||||
|
if (counter === 0) {
|
||||||
|
$('#loadEndpointsSpinner').hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
angular.forEach($scope.endpoints, function (endpoint) {
|
||||||
|
if (endpoint.Checked) {
|
||||||
|
counter = counter + 1;
|
||||||
|
EndpointService.deleteEndpoint(endpoint.Id).then(function success(data) {
|
||||||
|
Messages.send("Endpoint deleted", endpoint.Name);
|
||||||
|
var index = $scope.endpoints.indexOf(endpoint);
|
||||||
|
$scope.endpoints.splice(index, 1);
|
||||||
|
complete();
|
||||||
|
}, function error(err) {
|
||||||
|
Messages.error("Failure", err, 'Unable to remove endpoint');
|
||||||
|
complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function fetchEndpoints() {
|
||||||
|
$('#loadEndpointsSpinner').show();
|
||||||
|
EndpointService.endpoints().then(function success(data) {
|
||||||
|
$scope.endpoints = data;
|
||||||
|
EndpointService.getActive().then(function success(data) {
|
||||||
|
$scope.activeEndpoint = data;
|
||||||
|
$('#loadEndpointsSpinner').hide();
|
||||||
|
}, function error(err) {
|
||||||
|
$('#loadEndpointsSpinner').hide();
|
||||||
|
Messages.error("Failure", err, "Unable to retrieve active endpoint");
|
||||||
|
});
|
||||||
|
}, function error(err) {
|
||||||
|
$('#loadEndpointsSpinner').hide();
|
||||||
|
Messages.error("Failure", err, "Unable to retrieve endpoints");
|
||||||
|
$scope.endpoints = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchEndpoints();
|
||||||
|
}]);
|
|
@ -38,7 +38,7 @@ function ($scope, $state, Config, Image, ImageHelper, Messages, Settings) {
|
||||||
Messages.error('Error', {}, detail.error);
|
Messages.error('Error', {}, detail.error);
|
||||||
} else {
|
} else {
|
||||||
$('#pullImageSpinner').hide();
|
$('#pullImageSpinner').hide();
|
||||||
$state.go('images', {}, {reload: true});
|
$state.reload();
|
||||||
}
|
}
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
$('#pullImageSpinner').hide();
|
$('#pullImageSpinner').hide();
|
||||||
|
|
|
@ -13,7 +13,7 @@ function ($scope, $state, Network, Config, Messages, Settings) {
|
||||||
|
|
||||||
function prepareNetworkConfiguration() {
|
function prepareNetworkConfiguration() {
|
||||||
var config = angular.copy($scope.config);
|
var config = angular.copy($scope.config);
|
||||||
if ($scope.swarm) {
|
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||||
config.Driver = 'overlay';
|
config.Driver = 'overlay';
|
||||||
// Force IPAM Driver to 'default', should not be required.
|
// Force IPAM Driver to 'default', should not be required.
|
||||||
// See: https://github.com/docker/docker/issues/25735
|
// See: https://github.com/docker/docker/issues/25735
|
||||||
|
@ -34,7 +34,7 @@ function ($scope, $state, Network, Config, Messages, Settings) {
|
||||||
} else {
|
} else {
|
||||||
Messages.send("Network created", d.Id);
|
Messages.send("Network created", d.Id);
|
||||||
$('#createNetworkSpinner').hide();
|
$('#createNetworkSpinner').hide();
|
||||||
$state.go('networks', {}, {reload: true});
|
$state.reload();
|
||||||
}
|
}
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
$('#createNetworkSpinner').hide();
|
$('#createNetworkSpinner').hide();
|
||||||
|
@ -97,7 +97,6 @@ function ($scope, $state, Network, Config, Messages, Settings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
$scope.swarm = c.swarm;
|
|
||||||
fetchNetworks();
|
fetchNetworks();
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -14,7 +14,7 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settin
|
||||||
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
||||||
$('#loadServicesSpinner').hide();
|
$('#loadServicesSpinner').hide();
|
||||||
Messages.send("Service successfully scaled", "New replica count: " + service.Replicas);
|
Messages.send("Service successfully scaled", "New replica count: " + service.Replicas);
|
||||||
$state.go('services', {}, {reload: true});
|
$state.reload();
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
$('#loadServicesSpinner').hide();
|
$('#loadServicesSpinner').hide();
|
||||||
service.Scale = false;
|
service.Scale = false;
|
||||||
|
|
|
@ -16,7 +16,7 @@ function ($scope, $state, $sanitize, Users, Messages) {
|
||||||
var newPassword = $sanitize($scope.formValues.newPassword);
|
var newPassword = $sanitize($scope.formValues.newPassword);
|
||||||
Users.update({ username: $scope.username, password: newPassword }, function (d) {
|
Users.update({ username: $scope.username, password: newPassword }, function (d) {
|
||||||
Messages.send("Success", "Password successfully updated");
|
Messages.send("Success", "Password successfully updated");
|
||||||
$state.go('settings', {}, {reload: true});
|
$state.reload();
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
Messages.error("Failure", e, "Unable to update password");
|
Messages.error("Failure", e, "Unable to update password");
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,14 @@
|
||||||
<span class="menu-icon glyphicon glyphicon-transfer"></span>
|
<span class="menu-icon glyphicon glyphicon-transfer"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-title"><span>NAVIGATION</span></li>
|
<li class="sidebar-title">
|
||||||
|
<span>Active endpoint</span>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-title">
|
||||||
|
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)">
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-title"><span>Endpoint actions</span></li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="dashboard">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
<a ui-sref="dashboard">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -39,8 +46,12 @@
|
||||||
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
|
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
|
||||||
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
|
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="sidebar-title"><span>Portainer settings</span></li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="settings">Settings <span class="menu-icon fa fa-wrench"></span></a>
|
<a ui-sref="settings">Password <span class="menu-icon fa fa-lock"></span></a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-list">
|
||||||
|
<a ui-sref="endpoints">Endpoints <span class="menu-icon fa fa-plug"></span></a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
|
|
|
@ -1,10 +1,38 @@
|
||||||
angular.module('sidebar', [])
|
angular.module('sidebar', [])
|
||||||
.controller('SidebarController', ['$scope', 'Settings', 'Config', 'Info',
|
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'EndpointMode', 'Messages',
|
||||||
function ($scope, Settings, Config, Info) {
|
function ($scope, $state, Settings, Config, EndpointService, EndpointMode, Messages) {
|
||||||
|
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
$scope.logo = c.logo;
|
$scope.logo = c.logo;
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.uiVersion = Settings.uiVersion;
|
$scope.uiVersion = Settings.uiVersion;
|
||||||
|
|
||||||
|
$scope.switchEndpoint = function(endpoint) {
|
||||||
|
EndpointService.setActive(endpoint.Id).then(function success(data) {
|
||||||
|
EndpointMode.determineEndpointMode();
|
||||||
|
$state.reload();
|
||||||
|
}, function error(err) {
|
||||||
|
Messages.error("Failure", err, "Unable to switch to new endpoint");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function fetchEndpoints() {
|
||||||
|
EndpointService.endpoints().then(function success(data) {
|
||||||
|
$scope.endpoints = data;
|
||||||
|
EndpointService.getActive().then(function success(data) {
|
||||||
|
angular.forEach($scope.endpoints, function (endpoint) {
|
||||||
|
if (endpoint.Id === data.Id) {
|
||||||
|
$scope.activeEndpoint = endpoint;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, function error(err) {
|
||||||
|
Messages.error("Failure", err, "Unable to retrieve active endpoint");
|
||||||
|
});
|
||||||
|
}, function error(err) {
|
||||||
|
$scope.endpoints = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchEndpoints();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<select class="selectpicker form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
<select class="form-control" ng-options="net.Name for net in availableNetworks" ng-model="formValues.network">
|
||||||
<option disabled hidden value="">Select a network</option>
|
<option disabled hidden value="">Select a network</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,10 +41,10 @@
|
||||||
<div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
|
<div ng-repeat="var in state.selectedTemplate.env" ng-if="!var.set" class="form-group">
|
||||||
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
<label for="field_{{ $index }}" class="col-sm-2 control-label text-left">{{ var.label }}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
<select ng-if="endpointMode.provider !== 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|containername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||||
<option selected disabled hidden value="">Select a container</option>
|
<option selected disabled hidden value="">Select a container</option>
|
||||||
</select>
|
</select>
|
||||||
<select ng-if="endpointMode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="selectpicker form-control" ng-model="var.value">
|
<select ng-if="endpointMode.provider === 'DOCKER_SWARM' && var.type === 'container'" ng-options="container|swarmcontainername for container in runningContainers" class="form-control" ng-model="var.value">
|
||||||
<option selected disabled hidden value="">Select a container</option>
|
<option selected disabled hidden value="">Select a container</option>
|
||||||
</select>
|
</select>
|
||||||
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
<input ng-if="!var.type || !var.type === 'container'" type="text" class="form-control" ng-model="var.value" id="field_{{ $index }}">
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group col-sm-1 input-group-sm">
|
<div class="input-group col-sm-1 input-group-sm">
|
||||||
<select class="selectpicker form-control" ng-model="portBinding.protocol">
|
<select class="form-control" ng-model="portBinding.protocol">
|
||||||
<option value="tcp">tcp</option>
|
<option value="tcp">tcp</option>
|
||||||
<option value="udp">udp</option>
|
<option value="udp">udp</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -115,7 +115,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
|
||||||
if (v.value || v.set) {
|
if (v.value || v.set) {
|
||||||
var val;
|
var val;
|
||||||
if (v.type && v.type === 'container') {
|
if (v.type && v.type === 'container') {
|
||||||
if ($scope.swarm && $scope.formValues.network.Scope === 'global') {
|
if ($scope.endpointMode.provider === 'DOCKER_SWARM' && $scope.formValues.network.Scope === 'global') {
|
||||||
val = $filter('swarmcontainername')(v.value);
|
val = $filter('swarmcontainername')(v.value);
|
||||||
} else {
|
} else {
|
||||||
var container = v.value;
|
var container = v.value;
|
||||||
|
@ -203,11 +203,10 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.$promise.then(function (c) {
|
Config.$promise.then(function (c) {
|
||||||
$scope.swarm = c.swarm;
|
|
||||||
var containersToHideLabels = c.hiddenLabels;
|
var containersToHideLabels = c.hiddenLabels;
|
||||||
Network.query({}, function (d) {
|
Network.query({}, function (d) {
|
||||||
var networks = d;
|
var networks = d;
|
||||||
if ($scope.swarm) {
|
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||||
networks = d.filter(function (network) {
|
networks = d.filter(function (network) {
|
||||||
if (network.Scope === 'global') {
|
if (network.Scope === 'global') {
|
||||||
return network;
|
return network;
|
||||||
|
|
|
@ -106,6 +106,12 @@ angular.module('portainer.filters', [])
|
||||||
return 'Stopped';
|
return 'Stopped';
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
.filter('stripprotocol', function() {
|
||||||
|
'use strict';
|
||||||
|
return function (url) {
|
||||||
|
return url.replace(/.*?:\/\//g, '');
|
||||||
|
};
|
||||||
|
})
|
||||||
.filter('getstatelabel', function () {
|
.filter('getstatelabel', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (state) {
|
return function (state) {
|
||||||
|
|
|
@ -189,12 +189,12 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||||
'use strict';
|
'use strict';
|
||||||
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#2-5-networks
|
// http://docs.docker.com/reference/api/docker_remote_api_<%= remoteApiVersion %>/#2-5-networks
|
||||||
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, {
|
return $resource(Settings.url + '/volumes/:name/:action', {name: '@name'}, {
|
||||||
query: {method: 'GET'},
|
query: {method: 'GET'},
|
||||||
get: {method: 'GET'},
|
get: {method: 'GET'},
|
||||||
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
|
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler},
|
||||||
remove: {
|
remove: {
|
||||||
method: 'DELETE', transformResponse: genericHandler
|
method: 'DELETE', transformResponse: genericHandler
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}])
|
}])
|
||||||
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
|
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
|
||||||
|
@ -233,11 +233,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(USERS_ENDPOINT + '/:username/:action', {}, {
|
return $resource(USERS_ENDPOINT + '/:username/:action', {}, {
|
||||||
create: { method: 'POST' },
|
create: { method: 'POST' },
|
||||||
get: {method: 'GET', params: { username: '@username' } },
|
get: { method: 'GET', params: { username: '@username' } },
|
||||||
update: { method: 'PUT', params: { username: '@username' } },
|
update: { method: 'PUT', params: { username: '@username' } },
|
||||||
checkPassword: { method: 'POST', params: { username: '@username', action: 'passwd' } },
|
checkPassword: { method: 'POST', params: { username: '@username', action: 'passwd' } },
|
||||||
checkAdminUser: {method: 'GET', params: { username: 'admin', action: 'check' }},
|
checkAdminUser: { method: 'GET', params: { username: 'admin', action: 'check' } },
|
||||||
initAdminUser: {method: 'POST', params: { username: 'admin', action: 'init' }}
|
initAdminUser: { method: 'POST', params: { username: 'admin', action: 'init' } }
|
||||||
});
|
});
|
||||||
}])
|
}])
|
||||||
.factory('EndpointMode', ['$rootScope', 'Info', function EndpointMode($rootScope, Info) {
|
.factory('EndpointMode', ['$rootScope', 'Info', function EndpointMode($rootScope, Info) {
|
||||||
|
@ -304,6 +304,144 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}])
|
}])
|
||||||
|
.factory('FileUploadService', ['$q', 'Upload', function FileUploadFactory($q, Upload) {
|
||||||
|
'use strict';
|
||||||
|
function uploadFile(url, file) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
Upload.upload({
|
||||||
|
url: url,
|
||||||
|
data: { file: file }
|
||||||
|
}).then(function success(data) {
|
||||||
|
deferred.resolve(data);
|
||||||
|
}, function error(e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
}, function progress(evt) {
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
uploadTLSFilesForEndpoint: function(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
var queue = [];
|
||||||
|
|
||||||
|
if (TLSCAFile !== null) {
|
||||||
|
var uploadTLSCA = uploadFile('api/upload/tls/' + endpointID + '/ca', TLSCAFile);
|
||||||
|
queue.push(uploadTLSCA);
|
||||||
|
}
|
||||||
|
if (TLSCertFile !== null) {
|
||||||
|
var uploadTLSCert = uploadFile('api/upload/tls/' + endpointID + '/cert', TLSCertFile);
|
||||||
|
queue.push(uploadTLSCert);
|
||||||
|
}
|
||||||
|
if (TLSKeyFile !== null) {
|
||||||
|
var uploadTLSKey = uploadFile('api/upload/tls/' + endpointID + '/key', TLSKeyFile);
|
||||||
|
queue.push(uploadTLSKey);
|
||||||
|
}
|
||||||
|
$q.all(queue).then(function (data) {
|
||||||
|
deferred.resolve(data);
|
||||||
|
}, function (err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
}, function update(evt) {
|
||||||
|
deferred.notify(evt);
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}])
|
||||||
|
.factory('Endpoints', ['$resource', 'ENDPOINTS_ENDPOINT', function EndpointsFactory($resource, ENDPOINTS_ENDPOINT) {
|
||||||
|
'use strict';
|
||||||
|
return $resource(ENDPOINTS_ENDPOINT + '/:id/:action', {}, {
|
||||||
|
create: { method: 'POST' },
|
||||||
|
query: { method: 'GET', isArray: true },
|
||||||
|
get: { method: 'GET', params: { id: '@id' } },
|
||||||
|
update: { method: 'PUT', params: { id: '@id' } },
|
||||||
|
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||||
|
getActiveEndpoint: { method: 'GET', params: { id: '0' } },
|
||||||
|
setActiveEndpoint: { method: 'POST', params: { id: '@id', action: 'active' } }
|
||||||
|
});
|
||||||
|
}])
|
||||||
|
.factory('EndpointService', ['$q', '$timeout', 'Endpoints', 'FileUploadService', function EndpointServiceFactory($q, $timeout, Endpoints, FileUploadService) {
|
||||||
|
'use strict';
|
||||||
|
return {
|
||||||
|
getActive: function() {
|
||||||
|
return Endpoints.getActiveEndpoint().$promise;
|
||||||
|
},
|
||||||
|
setActive: function(endpointID) {
|
||||||
|
return Endpoints.setActiveEndpoint({id: endpointID}).$promise;
|
||||||
|
},
|
||||||
|
endpoint: function(endpointID) {
|
||||||
|
return Endpoints.get({id: endpointID}).$promise;
|
||||||
|
},
|
||||||
|
endpoints: function() {
|
||||||
|
return Endpoints.query({}).$promise;
|
||||||
|
},
|
||||||
|
updateEndpoint: function(ID, name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
|
var endpoint = {
|
||||||
|
id: ID,
|
||||||
|
Name: name,
|
||||||
|
URL: "tcp://" + URL,
|
||||||
|
TLS: TLS
|
||||||
|
};
|
||||||
|
var deferred = $q.defer();
|
||||||
|
Endpoints.update({}, endpoint, function success(data) {
|
||||||
|
FileUploadService.uploadTLSFilesForEndpoint(ID, TLSCAFile, TLSCertFile, TLSKeyFile).then(function success(data) {
|
||||||
|
deferred.notify({upload: false});
|
||||||
|
deferred.resolve(data);
|
||||||
|
}, function error(err) {
|
||||||
|
deferred.notify({upload: false});
|
||||||
|
deferred.reject({msg: 'Unable to upload TLS certs', err: err});
|
||||||
|
});
|
||||||
|
}, function error(err) {
|
||||||
|
deferred.reject({msg: 'Unable to update endpoint', err: err});
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
deleteEndpoint: function(endpointID) {
|
||||||
|
return Endpoints.remove({id: endpointID}).$promise;
|
||||||
|
},
|
||||||
|
createLocalEndpoint: function(name, URL, TLS, active) {
|
||||||
|
var endpoint = {
|
||||||
|
Name: "local",
|
||||||
|
URL: "unix:///var/run/docker.sock",
|
||||||
|
TLS: false
|
||||||
|
};
|
||||||
|
return Endpoints.create({active: active}, endpoint).$promise;
|
||||||
|
},
|
||||||
|
createRemoteEndpoint: function(name, URL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, active) {
|
||||||
|
var endpoint = {
|
||||||
|
Name: name,
|
||||||
|
URL: 'tcp://' + URL,
|
||||||
|
TLS: TLS
|
||||||
|
};
|
||||||
|
var deferred = $q.defer();
|
||||||
|
Endpoints.create({active: active}, endpoint, function success(data) {
|
||||||
|
var endpointID = data.Id;
|
||||||
|
if (TLS) {
|
||||||
|
deferred.notify({upload: true});
|
||||||
|
FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile).then(function success(data) {
|
||||||
|
deferred.notify({upload: false});
|
||||||
|
if (active) {
|
||||||
|
Endpoints.setActiveEndpoint({}, {id: endpointID}, function success(data) {
|
||||||
|
deferred.resolve(data);
|
||||||
|
}, function error(err) {
|
||||||
|
deferred.reject({msg: 'Unable to create endpoint', err: err});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
deferred.resolve(data);
|
||||||
|
}
|
||||||
|
}, function error(err) {
|
||||||
|
deferred.notify({upload: false});
|
||||||
|
deferred.reject({msg: 'Unable to upload TLS certs', err: err});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
deferred.resolve(data);
|
||||||
|
}
|
||||||
|
}, function error(err) {
|
||||||
|
deferred.reject({msg: 'Unable to create endpoint', err: err});
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}])
|
||||||
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
|
.factory('Messages', ['$rootScope', '$sanitize', function MessagesFactory($rootScope, $sanitize) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -212,7 +212,7 @@ input[type="radio"] {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-wrapper {
|
.page-wrapper {
|
||||||
margin-top: 25px;
|
margin-top: 25px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -220,15 +220,15 @@ input[type="radio"] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box {
|
.simple-box {
|
||||||
margin-bottom: 80px;
|
margin-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box > div:first-child {
|
.simple-box > div:first-child {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-logo {
|
.simple-box-logo {
|
||||||
display: block;
|
display: block;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -257,3 +257,8 @@ input[type="radio"] {
|
||||||
.user-box {
|
.user-box {
|
||||||
margin-right: 25px;
|
margin-right: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-endpoint {
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
|
@ -43,7 +43,8 @@
|
||||||
"rdash-ui": "1.0.*",
|
"rdash-ui": "1.0.*",
|
||||||
"moment": "~2.14.1",
|
"moment": "~2.14.1",
|
||||||
"xterm.js": "~2.0.1",
|
"xterm.js": "~2.0.1",
|
||||||
"font-awesome": "~4.7.0"
|
"font-awesome": "~4.7.0",
|
||||||
|
"ng-file-upload": "~12.2.13"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"angular": "1.5.5"
|
"angular": "1.5.5"
|
||||||
|
|
|
@ -198,6 +198,7 @@ module.exports = function (grunt) {
|
||||||
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
|
'bower_components/angular-ui-router/release/angular-ui-router.min.js',
|
||||||
'bower_components/angular-resource/angular-resource.min.js',
|
'bower_components/angular-resource/angular-resource.min.js',
|
||||||
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
'bower_components/angular-bootstrap/ui-bootstrap-tpls.min.js',
|
||||||
|
'bower_components/ng-file-upload/ng-file-upload.min.js',
|
||||||
'bower_components/angular-utils-pagination/dirPagination.js',
|
'bower_components/angular-utils-pagination/dirPagination.js',
|
||||||
'bower_components/angular-ui-select/dist/select.min.js'],
|
'bower_components/angular-ui-select/dist/select.min.js'],
|
||||||
dest: '<%= distdir %>/js/angular.js'
|
dest: '<%= distdir %>/js/angular.js'
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body ng-controller="MainController">
|
<body ng-controller="MainController">
|
||||||
<div id="page-wrapper" ng-class="{open: toggle && $state.current.name !== 'auth', nopadding: $state.current.name === 'auth'}" ng-cloak>
|
<div id="page-wrapper" ng-class="{open: toggle && $state.current.name !== 'auth' && $state.current.name !== 'endpointInit', nopadding: $state.current.name === 'auth' || $state.current.name === 'endpointInit'}" ng-cloak>
|
||||||
|
|
||||||
<div id="sideview" ui-view="sidebar"></div>
|
<div id="sideview" ui-view="sidebar"></div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue