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
|
||||
dist
|
||||
portainer-checksum.txt
|
||||
api/cmd/portainer/portainer-*
|
||||
api/cmd/portainer/portainer*
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
|
@ -13,6 +14,7 @@ type Store struct {
|
|||
|
||||
// Services
|
||||
UserService *UserService
|
||||
EndpointService *EndpointService
|
||||
|
||||
db *bolt.DB
|
||||
}
|
||||
|
@ -20,6 +22,8 @@ type Store struct {
|
|||
const (
|
||||
databaseFileName = "portainer.db"
|
||||
userBucketName = "users"
|
||||
endpointBucketName = "endpoints"
|
||||
activeEndpointBucketName = "activeEndpoint"
|
||||
)
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
|
@ -27,8 +31,10 @@ func NewStore(storePath string) *Store {
|
|||
store := &Store{
|
||||
Path: storePath,
|
||||
UserService: &UserService{},
|
||||
EndpointService: &EndpointService{},
|
||||
}
|
||||
store.UserService.store = store
|
||||
store.EndpointService.store = store
|
||||
return store
|
||||
}
|
||||
|
||||
|
@ -45,6 +51,14 @@ func (store *Store) Open() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(activeEndpointBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
|
@ -15,3 +16,22 @@ func MarshalUser(user *portainer.User) ([]byte, error) {
|
|||
func UnmarshalUser(data []byte, user *portainer.User) error {
|
||||
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 (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
// Service implements the CLIService interface
|
||||
|
@ -21,13 +22,12 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
kingpin.Version(version)
|
||||
|
||||
flags := &portainer.CLIFlags{
|
||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(":9000").Short('p').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(),
|
||||
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(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default("false").Bool(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default("/certs/ca.pem").String(),
|
||||
|
@ -41,6 +41,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
|
||||
// ValidateFlags validates the values of the flags.
|
||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
if *flags.Endpoint != "" {
|
||||
if !strings.HasPrefix(*flags.Endpoint, "unix://") && !strings.HasPrefix(*flags.Endpoint, "tcp://") {
|
||||
return errInvalidEnpointProtocol
|
||||
}
|
||||
|
@ -54,6 +55,7 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"github.com/portainer/portainer/bolt"
|
||||
"github.com/portainer/portainer/cli"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"github.com/portainer/portainer/file"
|
||||
"github.com/portainer/portainer/http"
|
||||
"github.com/portainer/portainer/jwt"
|
||||
|
||||
|
@ -24,7 +25,6 @@ func main() {
|
|||
}
|
||||
|
||||
settings := &portainer.Settings{
|
||||
Swarm: *flags.Swarm,
|
||||
HiddenLabels: *flags.Labels,
|
||||
Logo: *flags.Logo,
|
||||
}
|
||||
|
@ -41,15 +41,35 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fileService, err := file.NewService(*flags.Data)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var cryptoService portainer.CryptoService = &crypto.Service{}
|
||||
|
||||
endpointConfiguration := &portainer.EndpointConfiguration{
|
||||
Endpoint: *flags.Endpoint,
|
||||
// Initialize the active endpoint from the CLI only if there is no
|
||||
// active endpoint defined yet.
|
||||
var activeEndpoint *portainer.Endpoint
|
||||
if *flags.Endpoint != "" {
|
||||
activeEndpoint, err = store.EndpointService.GetActive()
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
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{
|
||||
BindAddress: *flags.Addr,
|
||||
|
@ -57,9 +77,11 @@ func main() {
|
|||
Settings: settings,
|
||||
TemplatesURL: *flags.Templates,
|
||||
UserService: store.UserService,
|
||||
EndpointService: store.EndpointService,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
EndpointConfig: endpointConfiguration,
|
||||
FileService: fileService,
|
||||
ActiveEndpoint: activeEndpoint,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||
|
|
|
@ -10,6 +10,12 @@ const (
|
|||
ErrUserNotFound = Error("User not found")
|
||||
)
|
||||
|
||||
// Endpoint errors.
|
||||
const (
|
||||
ErrEndpointNotFound = Error("Endpoint not found")
|
||||
ErrNoActiveEndpoint = Error("Undefined Docker endpoint")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
const (
|
||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||
|
@ -21,6 +27,11 @@ const (
|
|||
ErrInvalidJWTToken = Error("Invalid JWT token")
|
||||
)
|
||||
|
||||
// File errors.
|
||||
const (
|
||||
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
|
||||
)
|
||||
|
||||
// Error represents an application error.
|
||||
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"
|
||||
|
||||
"encoding/json"
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// AuthHandler represents an HTTP API handler for managing authentication.
|
||||
|
@ -27,7 +28,7 @@ const (
|
|||
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
||||
)
|
||||
|
||||
// NewAuthHandler returns a new instance of DialHandler.
|
||||
// NewAuthHandler returns a new instance of AuthHandler.
|
||||
func NewAuthHandler() *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Router: mux.NewRouter(),
|
||||
|
@ -38,8 +39,8 @@ func NewAuthHandler() *AuthHandler {
|
|||
}
|
||||
|
||||
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
handleNotAllowed(w, []string{"POST"})
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ package http
|
|||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
|
@ -12,6 +11,8 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
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
|
||||
endpointURL, err := url.Parse(config.Endpoint)
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if config.TLS {
|
||||
proxy, err = newHTTPSProxy(endpointURL, config)
|
||||
if endpoint.TLS {
|
||||
proxy, err = newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -65,7 +70,6 @@ func (handler *DockerHandler) setupProxy(config *portainer.EndpointConfiguration
|
|||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
|
@ -81,7 +85,6 @@ func singleJoiningSlash(a, b string) string {
|
|||
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves
|
||||
|
||||
func NewSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
|
@ -107,10 +110,10 @@ func newHTTPProxy(u *url.URL) http.Handler {
|
|||
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"
|
||||
proxy := NewSingleHostReverseProxyWithHostHeader(u)
|
||||
config, err := createTLSConfiguration(endpointConfig.TLSCACertPath, endpointConfig.TLSCertPath, endpointConfig.TLSKeyPath)
|
||||
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
|
||||
if err != nil {
|
||||
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 {
|
||||
AuthHandler *AuthHandler
|
||||
UserHandler *UserHandler
|
||||
EndpointHandler *EndpointHandler
|
||||
SettingsHandler *SettingsHandler
|
||||
TemplatesHandler *TemplatesHandler
|
||||
DockerHandler *DockerHandler
|
||||
WebSocketHandler *WebSocketHandler
|
||||
UploadHandler *UploadHandler
|
||||
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)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
|
||||
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") {
|
||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
|
||||
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") {
|
||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
|
||||
|
|
|
@ -11,11 +11,30 @@ type Server struct {
|
|||
BindAddress string
|
||||
AssetsPath string
|
||||
UserService portainer.UserService
|
||||
EndpointService portainer.EndpointService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
EndpointConfig *portainer.EndpointConfiguration
|
||||
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
|
||||
|
@ -23,6 +42,7 @@ func (server *Server) Start() error {
|
|||
middleWareService := &middleWareService{
|
||||
jwtService: server.JWTService,
|
||||
}
|
||||
|
||||
var authHandler = NewAuthHandler()
|
||||
authHandler.UserService = server.UserService
|
||||
authHandler.CryptoService = server.CryptoService
|
||||
|
@ -35,19 +55,31 @@ func (server *Server) Start() error {
|
|||
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||
templatesHandler.templatesURL = server.TemplatesURL
|
||||
var dockerHandler = NewDockerHandler(middleWareService)
|
||||
dockerHandler.setupProxy(server.EndpointConfig)
|
||||
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))
|
||||
|
||||
handler := &Handler{
|
||||
server.Handler = &Handler{
|
||||
AuthHandler: authHandler,
|
||||
UserHandler: userHandler,
|
||||
EndpointHandler: endpointHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
TemplatesHandler: templatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
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 (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||
|
@ -30,8 +31,8 @@ func NewSettingsHandler(middleWareService *middleWareService) *SettingsHandler {
|
|||
|
||||
// handleGetSettings handles GET requests on /settings
|
||||
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
handleNotAllowed(w, []string{"GET"})
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,12 @@ package http
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||
|
@ -32,8 +33,8 @@ func NewTemplatesHandler(middleWareService *middleWareService) *TemplatesHandler
|
|||
|
||||
// handleGetTemplates handles GET requests on /templates
|
||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
handleNotAllowed(w, []string{"GET"})
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
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"
|
||||
|
||||
"encoding/json"
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// 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.handleGetUser(w, r)
|
||||
}))).Methods("GET")
|
||||
}))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{username}", middleWareService.addMiddleWares(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
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.handlePostUserPasswd(w, r)
|
||||
})))
|
||||
|
@ -46,8 +47,8 @@ func NewUserHandler(middleWareService *middleWareService) *UserHandler {
|
|||
|
||||
// handlePostUsers handles POST requests on /users
|
||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
handleNotAllowed(w, []string{"POST"})
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -86,8 +87,8 @@ type postUsersRequest struct {
|
|||
|
||||
// handlePostUserPasswd handles POST requests on /users/:username/passwd
|
||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
handleNotAllowed(w, []string{"POST"})
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -189,8 +190,8 @@ type putUserRequest struct {
|
|||
|
||||
// handlePostAdminInit handles GET requests on /users/admin/check
|
||||
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
handleNotAllowed(w, []string{"GET"})
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -209,8 +210,8 @@ func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.R
|
|||
|
||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
handleNotAllowed(w, []string{"POST"})
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,6 @@ import (
|
|||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/net/websocket"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
|
@ -17,6 +15,9 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
// WebSocketHandler represents an HTTP API handler for proxying requests to a web socket.
|
||||
|
@ -24,7 +25,7 @@ type WebSocketHandler struct {
|
|||
*mux.Router
|
||||
Logger *log.Logger
|
||||
middleWareService *middleWareService
|
||||
endpointConfiguration *portainer.EndpointConfiguration
|
||||
endpoint *portainer.Endpoint
|
||||
}
|
||||
|
||||
// NewWebSocketHandler returns a new instance of WebSocketHandler.
|
||||
|
@ -42,7 +43,7 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
|||
execID := qry.Get("id")
|
||||
|
||||
// Should not be managed here
|
||||
endpoint, err := url.Parse(handler.endpointConfiguration.Endpoint)
|
||||
endpoint, err := url.Parse(handler.endpoint.URL)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to parse endpoint URL: %s", err)
|
||||
return
|
||||
|
@ -57,10 +58,10 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
|||
|
||||
// Should not be managed here
|
||||
var tlsConfig *tls.Config
|
||||
if handler.endpointConfiguration.TLS {
|
||||
tlsConfig, err = createTLSConfiguration(handler.endpointConfiguration.TLSCACertPath,
|
||||
handler.endpointConfiguration.TLSCertPath,
|
||||
handler.endpointConfiguration.TLSKeyPath)
|
||||
if handler.endpoint.TLS {
|
||||
tlsConfig, err = createTLSConfiguration(handler.endpoint.TLSCACertPath,
|
||||
handler.endpoint.TLSCertPath,
|
||||
handler.endpoint.TLSKeyPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||
return
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package portainer
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type (
|
||||
// Pair defines a key/value string pair
|
||||
Pair struct {
|
||||
|
@ -15,7 +19,6 @@ type (
|
|||
Endpoint *string
|
||||
Labels *[]Pair
|
||||
Logo *string
|
||||
Swarm *bool
|
||||
Templates *string
|
||||
TLSVerify *bool
|
||||
TLSCacert *string
|
||||
|
@ -25,15 +28,14 @@ type (
|
|||
|
||||
// Settings represents Portainer settings.
|
||||
Settings struct {
|
||||
Swarm bool `json:"swarm"`
|
||||
HiddenLabels []Pair `json:"hiddenLabels"`
|
||||
Logo string `json:"logo"`
|
||||
}
|
||||
|
||||
// User represent a user account.
|
||||
User struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
}
|
||||
|
||||
// TokenData represents the data embedded in a JWT token.
|
||||
|
@ -41,15 +43,25 @@ type (
|
|||
Username string
|
||||
}
|
||||
|
||||
// EndpointConfiguration represents the data required to connect to a Docker API endpoint.
|
||||
EndpointConfiguration struct {
|
||||
Endpoint string
|
||||
TLS bool
|
||||
TLSCACertPath string
|
||||
TLSCertPath string
|
||||
TLSKeyPath string
|
||||
// EndpointID represents an endpoint identifier.
|
||||
EndpointID int
|
||||
|
||||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it.
|
||||
Endpoint struct {
|
||||
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 interface {
|
||||
ParseFlags(version string) (*CLIFlags, error)
|
||||
|
@ -73,6 +85,17 @@ type (
|
|||
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 interface {
|
||||
Hash(data string) (string, error)
|
||||
|
@ -84,9 +107,25 @@ type (
|
|||
GenerateToken(data *TokenData) (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 (
|
||||
// APIVersion is the version number of portainer API.
|
||||
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
|
||||
)
|
||||
|
|
53
app/app.js
53
app/app.js
|
@ -5,6 +5,7 @@ angular.module('portainer', [
|
|||
'ui.select',
|
||||
'ngCookies',
|
||||
'ngSanitize',
|
||||
'ngFileUpload',
|
||||
'angularUtils.directives.dirPagination',
|
||||
'LocalStorageModule',
|
||||
'angular-jwt',
|
||||
|
@ -19,6 +20,9 @@ angular.module('portainer', [
|
|||
'containers',
|
||||
'createContainer',
|
||||
'docker',
|
||||
'endpoint',
|
||||
'endpointInit',
|
||||
'endpoints',
|
||||
'events',
|
||||
'images',
|
||||
'image',
|
||||
|
@ -270,6 +274,50 @@ angular.module('portainer', [
|
|||
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', {
|
||||
url: '/events/',
|
||||
views: {
|
||||
|
@ -491,18 +539,19 @@ angular.module('portainer', [
|
|||
});
|
||||
|
||||
$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();
|
||||
}
|
||||
});
|
||||
}])
|
||||
// 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
|
||||
.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_ENDPOINT', '/api/docker')
|
||||
.constant('CONFIG_ENDPOINT', '/api/settings')
|
||||
.constant('AUTH_ENDPOINT', '/api/auth')
|
||||
.constant('USERS_ENDPOINT', '/api/users')
|
||||
.constant('ENDPOINTS_ENDPOINT', '/api/endpoints')
|
||||
.constant('TEMPLATES_ENDPOINT', '/api/templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.10.2');
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<div class="login-wrapper">
|
||||
<div class="page-wrapper">
|
||||
<!-- 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">
|
||||
<!-- login box logo -->
|
||||
<div class="row">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="login-logo">
|
||||
<img ng-if="!logo" src="images/logo_alt.png" class="login-logo" alt="Portainer">
|
||||
<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>
|
||||
<!-- !login box logo -->
|
||||
<!-- init password panel -->
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('auth', [])
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'Messages',
|
||||
function ($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, EndpointService, Messages) {
|
||||
|
||||
$scope.authData = {
|
||||
username: 'admin',
|
||||
|
@ -58,10 +58,17 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
|
|||
$scope.authenticationError = false;
|
||||
var username = $sanitize($scope.authData.username);
|
||||
var password = $sanitize($scope.authData.password);
|
||||
Authentication.login(username, password)
|
||||
.then(function() {
|
||||
Authentication.login(username, password).then(function success() {
|
||||
EndpointService.getActive().then(function success(data) {
|
||||
$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';
|
||||
});
|
||||
};
|
||||
|
|
|
@ -152,7 +152,7 @@ function ($scope, $filter, Container, ContainerHelper, Info, Settings, Messages,
|
|||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
if (c.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
Info.get({}, function (d) {
|
||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
|
|
|
@ -62,7 +62,6 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
|||
};
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
|
@ -73,7 +72,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
|||
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if ($scope.swarm) {
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
|
@ -221,7 +220,7 @@ function ($scope, $state, $stateParams, $filter, Config, Info, Container, Contai
|
|||
var containerName = container;
|
||||
if (container && typeof container === 'object') {
|
||||
containerName = $filter('trimcontainername')(container.Names[0]);
|
||||
if ($scope.swarm && $scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM') {
|
||||
containerName = $filter('swarmcontainername')(container);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
<input type="text" class="form-control" ng-model="portBinding.containerPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<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="udp">udp</option>
|
||||
</select>
|
||||
|
@ -243,7 +243,7 @@
|
|||
</div>
|
||||
<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>
|
||||
<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 ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||
</select>
|
||||
|
@ -278,7 +278,7 @@
|
|||
<div class="form-group">
|
||||
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||
<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 ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
|
@ -289,10 +289,10 @@
|
|||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
<input type="text" class="form-control" ng-model="portBinding.TargetPort" placeholder="e.g. 80">
|
||||
</div>
|
||||
<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="udp">udp</option>
|
||||
</select>
|
||||
|
@ -192,7 +192,7 @@
|
|||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<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 ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
|
||||
</select>
|
||||
|
@ -222,7 +222,7 @@
|
|||
<div class="form-group">
|
||||
<label for="container_network" class="col-sm-1 control-label text-left">Network</label>
|
||||
<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 ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
|
@ -247,7 +247,7 @@
|
|||
<i class="fa fa-minus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</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 ng-repeat="net in availableNetworks" ng-value="net.Name">{{ net.Name }}</option>
|
||||
</select>
|
||||
|
|
|
@ -87,7 +87,6 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
|||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
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);
|
||||
} else {
|
||||
$('#pullImageSpinner').hide();
|
||||
$state.go('images', {}, {reload: true});
|
||||
$state.reload();
|
||||
}
|
||||
}, function (e) {
|
||||
$('#pullImageSpinner').hide();
|
||||
|
|
|
@ -13,7 +13,7 @@ function ($scope, $state, Network, Config, Messages, Settings) {
|
|||
|
||||
function prepareNetworkConfiguration() {
|
||||
var config = angular.copy($scope.config);
|
||||
if ($scope.swarm) {
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
config.Driver = 'overlay';
|
||||
// Force IPAM Driver to 'default', should not be required.
|
||||
// See: https://github.com/docker/docker/issues/25735
|
||||
|
@ -34,7 +34,7 @@ function ($scope, $state, Network, Config, Messages, Settings) {
|
|||
} else {
|
||||
Messages.send("Network created", d.Id);
|
||||
$('#createNetworkSpinner').hide();
|
||||
$state.go('networks', {}, {reload: true});
|
||||
$state.reload();
|
||||
}
|
||||
}, function (e) {
|
||||
$('#createNetworkSpinner').hide();
|
||||
|
@ -97,7 +97,6 @@ function ($scope, $state, Network, Config, Messages, Settings) {
|
|||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
fetchNetworks();
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -14,7 +14,7 @@ function ($scope, $stateParams, $state, Service, ServiceHelper, Messages, Settin
|
|||
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
Messages.send("Service successfully scaled", "New replica count: " + service.Replicas);
|
||||
$state.go('services', {}, {reload: true});
|
||||
$state.reload();
|
||||
}, function (e) {
|
||||
$('#loadServicesSpinner').hide();
|
||||
service.Scale = false;
|
||||
|
|
|
@ -16,7 +16,7 @@ function ($scope, $state, $sanitize, Users, Messages) {
|
|||
var newPassword = $sanitize($scope.formValues.newPassword);
|
||||
Users.update({ username: $scope.username, password: newPassword }, function (d) {
|
||||
Messages.send("Success", "Password successfully updated");
|
||||
$state.go('settings', {}, {reload: true});
|
||||
$state.reload();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, "Unable to update password");
|
||||
});
|
||||
|
|
|
@ -8,7 +8,14 @@
|
|||
<span class="menu-icon glyphicon glyphicon-transfer"></span>
|
||||
</a>
|
||||
</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">
|
||||
<a ui-sref="dashboard">Dashboard <span class="menu-icon fa fa-tachometer"></span></a>
|
||||
</li>
|
||||
|
@ -39,8 +46,12 @@
|
|||
<li class="sidebar-list" ng-if="endpointMode.provider === 'DOCKER_STANDALONE'">
|
||||
<a ui-sref="docker">Docker <span class="menu-icon fa fa-th"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-title"><span>Portainer settings</span></li>
|
||||
<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>
|
||||
</ul>
|
||||
<div class="sidebar-footer">
|
||||
|
|
|
@ -1,10 +1,38 @@
|
|||
angular.module('sidebar', [])
|
||||
.controller('SidebarController', ['$scope', 'Settings', 'Config', 'Info',
|
||||
function ($scope, Settings, Config, Info) {
|
||||
.controller('SidebarController', ['$scope', '$state', 'Settings', 'Config', 'EndpointService', 'EndpointMode', 'Messages',
|
||||
function ($scope, $state, Settings, Config, EndpointService, EndpointMode, Messages) {
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
});
|
||||
|
||||
$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>
|
||||
<label for="container_network" class="col-sm-2 control-label text-right">Network</label>
|
||||
<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>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -41,10 +41,10 @@
|
|||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</select>
|
||||
<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">
|
||||
</div>
|
||||
<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="udp">udp</option>
|
||||
</select>
|
||||
|
|
|
@ -115,7 +115,7 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
|
|||
if (v.value || v.set) {
|
||||
var val;
|
||||
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);
|
||||
} else {
|
||||
var container = v.value;
|
||||
|
@ -203,11 +203,10 @@ function ($scope, $q, $state, $filter, $anchorScroll, Config, Info, Container, C
|
|||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.swarm = c.swarm;
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
Network.query({}, function (d) {
|
||||
var networks = d;
|
||||
if ($scope.swarm) {
|
||||
if ($scope.endpointMode.provider === 'DOCKER_SWARM' || $scope.endpointMode.provider === 'DOCKER_SWARM_MODE') {
|
||||
networks = d.filter(function (network) {
|
||||
if (network.Scope === 'global') {
|
||||
return network;
|
||||
|
|
|
@ -106,6 +106,12 @@ angular.module('portainer.filters', [])
|
|||
return 'Stopped';
|
||||
};
|
||||
})
|
||||
.filter('stripprotocol', function() {
|
||||
'use strict';
|
||||
return function (url) {
|
||||
return url.replace(/.*?:\/\//g, '');
|
||||
};
|
||||
})
|
||||
.filter('getstatelabel', function () {
|
||||
'use strict';
|
||||
return function (state) {
|
||||
|
|
|
@ -233,11 +233,11 @@ angular.module('portainer.services', ['ngResource', 'ngSanitize'])
|
|||
'use strict';
|
||||
return $resource(USERS_ENDPOINT + '/:username/:action', {}, {
|
||||
create: { method: 'POST' },
|
||||
get: {method: 'GET', params: { username: '@username' } },
|
||||
get: { method: 'GET', params: { username: '@username' } },
|
||||
update: { method: 'PUT', params: { username: '@username' } },
|
||||
checkPassword: { method: 'POST', params: { username: '@username', action: 'passwd' } },
|
||||
checkAdminUser: {method: 'GET', params: { username: 'admin', action: 'check' }},
|
||||
initAdminUser: {method: 'POST', params: { username: 'admin', action: 'init' }}
|
||||
checkAdminUser: { method: 'GET', params: { username: 'admin', action: 'check' } },
|
||||
initAdminUser: { method: 'POST', params: { username: 'admin', action: 'init' } }
|
||||
});
|
||||
}])
|
||||
.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) {
|
||||
'use strict';
|
||||
return {
|
||||
|
|
|
@ -212,7 +212,7 @@ input[type="radio"] {
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.login-wrapper {
|
||||
.page-wrapper {
|
||||
margin-top: 25px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -220,15 +220,15 @@ input[type="radio"] {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
.simple-box {
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
|
||||
.login-box > div:first-child {
|
||||
.simple-box > div:first-child {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
.simple-box-logo {
|
||||
display: block;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
|
@ -257,3 +257,8 @@ input[type="radio"] {
|
|||
.user-box {
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
.select-endpoint {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,8 @@
|
|||
"rdash-ui": "1.0.*",
|
||||
"moment": "~2.14.1",
|
||||
"xterm.js": "~2.0.1",
|
||||
"font-awesome": "~4.7.0"
|
||||
"font-awesome": "~4.7.0",
|
||||
"ng-file-upload": "~12.2.13"
|
||||
},
|
||||
"resolutions": {
|
||||
"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-resource/angular-resource.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-ui-select/dist/select.min.js'],
|
||||
dest: '<%= distdir %>/js/angular.js'
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
</head>
|
||||
|
||||
<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>
|
||||
|
||||
|
|
Loading…
Reference in New Issue