Merge branch 'release/1.14.1'

pull/1216/head 1.14.1
Anthony Lapenna 2017-09-20 15:41:06 +02:00
commit 1bccd521f8
123 changed files with 3457 additions and 1514 deletions

View File

@ -2,7 +2,7 @@ package bolt
import "github.com/portainer/portainer" import "github.com/portainer/portainer"
func (m *Migrator) updateSettingsToVersion3() error { func (m *Migrator) updateSettingsToDBVersion3() error {
legacySettings, err := m.SettingsService.Settings() legacySettings, err := m.SettingsService.Settings()
if err != nil { if err != nil {
return err return err

View File

@ -0,0 +1,27 @@
package bolt
import "github.com/portainer/portainer"
func (m *Migrator) updateEndpointsToDBVersion4() error {
legacyEndpoints, err := m.EndpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range legacyEndpoints {
endpoint.TLSConfig = portainer.TLSConfiguration{}
if endpoint.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSConfig.TLSCACertPath = endpoint.TLSCACertPath
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
}
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View File

@ -30,7 +30,7 @@ func NewMigrator(store *Store, version int) *Migrator {
func (m *Migrator) Migrate() error { func (m *Migrator) Migrate() error {
// Portainer < 1.12 // Portainer < 1.12
if m.CurrentDBVersion == 0 { if m.CurrentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1() err := m.updateAdminUserToDBVersion1()
if err != nil { if err != nil {
return err return err
@ -38,7 +38,7 @@ func (m *Migrator) Migrate() error {
} }
// Portainer 1.12.x // Portainer 1.12.x
if m.CurrentDBVersion == 1 { if m.CurrentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2() err := m.updateResourceControlsToDBVersion2()
if err != nil { if err != nil {
return err return err
@ -50,8 +50,16 @@ func (m *Migrator) Migrate() error {
} }
// Portainer 1.13.x // Portainer 1.13.x
if m.CurrentDBVersion == 2 { if m.CurrentDBVersion < 3 {
err := m.updateSettingsToVersion3() err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.CurrentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil { if err != nil {
return err return err
} }

View File

@ -36,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(), NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(), NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(), TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(), TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(), TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),

View File

@ -191,12 +191,15 @@ func main() {
} }
if len(endpoints) == 0 { if len(endpoints) == 0 {
endpoint := &portainer.Endpoint{ endpoint := &portainer.Endpoint{
Name: "primary", Name: "primary",
URL: *flags.Endpoint, URL: *flags.Endpoint,
TLS: *flags.TLSVerify, TLSConfig: portainer.TLSConfiguration{
TLSCACertPath: *flags.TLSCacert, TLS: *flags.TLSVerify,
TLSCertPath: *flags.TLSCert, TLSSkipVerify: false,
TLSKeyPath: *flags.TLSKey, TLSCACertPath: *flags.TLSCacert,
TLSCertPath: *flags.TLSCert,
TLSKeyPath: *flags.TLSKey,
},
AuthorizedUsers: []portainer.UserID{}, AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{}, AuthorizedTeams: []portainer.TeamID{},
} }
@ -245,7 +248,7 @@ func main() {
SSLKey: *flags.SSLKey, SSLKey: *flags.SSLKey,
} }
log.Printf("Starting Portainer on %s", *flags.Addr) log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
err = server.Start() err = server.Start()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -22,6 +22,16 @@ type (
endpointsToUpdate []*portainer.Endpoint endpointsToUpdate []*portainer.Endpoint
endpointsToDelete []*portainer.Endpoint endpointsToDelete []*portainer.Endpoint
} }
fileEndpoint struct {
Name string `json:"Name"`
URL string `json:"URL"`
TLS bool `json:"TLS,omitempty"`
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
TLSCACert string `json:"TLSCACert,omitempty"`
TLSCert string `json:"TLSCert,omitempty"`
TLSKey string `json:"TLSKey,omitempty"`
}
) )
const ( const (
@ -55,6 +65,28 @@ func isValidEndpoint(endpoint *portainer.Endpoint) bool {
return false return false
} }
func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint {
convertedEndpoints := make([]portainer.Endpoint, 0)
for _, e := range fileEndpoints {
endpoint := portainer.Endpoint{
Name: e.Name,
URL: e.URL,
TLSConfig: portainer.TLSConfiguration{},
}
if e.TLS {
endpoint.TLSConfig.TLS = true
endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify
endpoint.TLSConfig.TLSCACertPath = e.TLSCACert
endpoint.TLSConfig.TLSCertPath = e.TLSCert
endpoint.TLSConfig.TLSKeyPath = e.TLSKey
}
convertedEndpoints = append(convertedEndpoints, endpoint)
}
return convertedEndpoints
}
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int { func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
for idx, v := range endpoints { for idx, v := range endpoints {
if endpoint.Name == v.Name && isValidEndpoint(&v) { if endpoint.Name == v.Name && isValidEndpoint(&v) {
@ -66,22 +98,25 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint { func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
var endpoint *portainer.Endpoint var endpoint *portainer.Endpoint
if original.URL != updated.URL || original.TLS != updated.TLS || if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS ||
(updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) || (updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) ||
(updated.TLS && original.TLSCertPath != updated.TLSCertPath) || (updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) ||
(updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) { (updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) ||
(updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) {
endpoint = original endpoint = original
endpoint.URL = updated.URL endpoint.URL = updated.URL
if updated.TLS { if updated.TLSConfig.TLS {
endpoint.TLS = true endpoint.TLSConfig.TLS = true
endpoint.TLSCACertPath = updated.TLSCACertPath endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify
endpoint.TLSCertPath = updated.TLSCertPath endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath
endpoint.TLSKeyPath = updated.TLSKeyPath endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath
endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath
} else { } else {
endpoint.TLS = false endpoint.TLSConfig.TLS = false
endpoint.TLSCACertPath = "" endpoint.TLSConfig.TLSSkipVerify = false
endpoint.TLSCertPath = "" endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSKeyPath = "" endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
} }
} }
return endpoint return endpoint
@ -141,7 +176,7 @@ func (job endpointSyncJob) Sync() error {
return err return err
} }
var fileEndpoints []portainer.Endpoint var fileEndpoints []fileEndpoint
err = json.Unmarshal(data, &fileEndpoints) err = json.Unmarshal(data, &fileEndpoints)
if endpointSyncError(err, job.logger) { if endpointSyncError(err, job.logger) {
return err return err
@ -156,7 +191,9 @@ func (job endpointSyncJob) Sync() error {
return err return err
} }
sync := job.prepareSyncData(storedEndpoints, fileEndpoints) convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
if sync.requireSync() { if sync.requireSync() {
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete) err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
if endpointSyncError(err, job.logger) { if endpointSyncError(err, job.logger) {

View File

@ -4,31 +4,38 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"io/ioutil" "io/ioutil"
"github.com/portainer/portainer"
) )
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key // CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
func CreateTLSConfiguration(caCertPath, certPath, keyPath string, skipTLSVerify bool) (*tls.Config, error) { func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) {
TLSConfig := &tls.Config{}
config := &tls.Config{} if config.TLS {
if config.TLSCertPath != "" && config.TLSKeyPath != "" {
cert, err := tls.LoadX509KeyPair(config.TLSCertPath, config.TLSKeyPath)
if err != nil {
return nil, err
}
if certPath != "" && keyPath != "" { TLSConfig.Certificates = []tls.Certificate{cert}
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, err
} }
config.Certificates = []tls.Certificate{cert}
if !config.TLSSkipVerify {
caCert, err := ioutil.ReadFile(config.TLSCACertPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
TLSConfig.RootCAs = caCertPool
}
TLSConfig.InsecureSkipVerify = config.TLSSkipVerify
} }
if caCertPath != "" { return TLSConfig, nil
caCert, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
config.RootCAs = caCertPool
}
config.InsecureSkipVerify = skipTLSVerify
return config, nil
} }

View File

@ -13,8 +13,10 @@ const (
const ( const (
ErrUserNotFound = Error("User not found") ErrUserNotFound = Error("User not found")
ErrUserAlreadyExists = Error("User already exists") ErrUserAlreadyExists = Error("User already exists")
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed.") ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
ErrAdminAlreadyInitialized = Error("Admin user already initialized") ErrAdminAlreadyInitialized = Error("An administrator user already exists")
ErrCannotRemoveAdmin = Error("Cannot remove the default administrator account")
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
) )
// Team errors. // Team errors.

View File

@ -95,7 +95,7 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
} }
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint. // DeleteTLSFiles deletes a folder in the TLS store path.
func (service *Service) DeleteTLSFiles(folder string) error { func (service *Service) DeleteTLSFiles(folder string) error {
storePath := path.Join(service.fileStorePath, TLSStorePath, folder) storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
err := os.RemoveAll(storePath) err := os.RemoveAll(storePath)
@ -105,6 +105,29 @@ func (service *Service) DeleteTLSFiles(folder string) error {
return nil return nil
} }
// DeleteTLSFile deletes a specific TLS file from a folder.
func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileType) 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
}
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
err := os.Remove(filePath)
if err != nil {
return err
}
return nil
}
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system. // createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
func (service *Service) createDirectoryInStoreIfNotExist(name string) error { func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
path := path.Join(service.fileStorePath, name) path := path.Join(service.fileStorePath, name)

View File

@ -57,10 +57,12 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag
type ( type (
postEndpointsRequest struct { postEndpointsRequest struct {
Name string `valid:"required"` Name string `valid:"required"`
URL string `valid:"required"` URL string `valid:"required"`
PublicURL string `valid:"-"` PublicURL string `valid:"-"`
TLS bool TLS bool
TLSSkipVerify bool
TLSSkipClientVerify bool
} }
postEndpointsResponse struct { postEndpointsResponse struct {
@ -73,10 +75,12 @@ type (
} }
putEndpointsRequest struct { putEndpointsRequest struct {
Name string `valid:"-"` Name string `valid:"-"`
URL string `valid:"-"` URL string `valid:"-"`
PublicURL string `valid:"-"` PublicURL string `valid:"-"`
TLS bool `valid:"-"` TLS bool `valid:"-"`
TLSSkipVerify bool `valid:"-"`
TLSSkipClientVerify bool `valid:"-"`
} }
) )
@ -123,10 +127,13 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
} }
endpoint := &portainer.Endpoint{ endpoint := &portainer.Endpoint{
Name: req.Name, Name: req.Name,
URL: req.URL, URL: req.URL,
PublicURL: req.PublicURL, PublicURL: req.PublicURL,
TLS: req.TLS, TLSConfig: portainer.TLSConfiguration{
TLS: req.TLS,
TLSSkipVerify: req.TLSSkipVerify,
},
AuthorizedUsers: []portainer.UserID{}, AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{}, AuthorizedTeams: []portainer.TeamID{},
} }
@ -139,12 +146,19 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
if req.TLS { if req.TLS {
folder := strconv.Itoa(int(endpoint.ID)) folder := strconv.Itoa(int(endpoint.ID))
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSCACertPath = caCertPath if !req.TLSSkipVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSCertPath = certPath endpoint.TLSConfig.TLSCACertPath = caCertPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) }
endpoint.TLSKeyPath = keyPath
if !req.TLSSkipClientVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
}
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil { if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
@ -284,18 +298,33 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
folder := strconv.Itoa(int(endpoint.ID)) folder := strconv.Itoa(int(endpoint.ID))
if req.TLS { if req.TLS {
endpoint.TLS = true endpoint.TLSConfig.TLS = true
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA) endpoint.TLSConfig.TLSSkipVerify = req.TLSSkipVerify
endpoint.TLSCACertPath = caCertPath if !req.TLSSkipVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert) caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
endpoint.TLSCertPath = certPath endpoint.TLSConfig.TLSCACertPath = caCertPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey) } else {
endpoint.TLSKeyPath = keyPath endpoint.TLSConfig.TLSCACertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
}
if !req.TLSSkipClientVerify {
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSCertPath = certPath
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
endpoint.TLSConfig.TLSKeyPath = keyPath
} else {
endpoint.TLSConfig.TLSCertPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
endpoint.TLSConfig.TLSKeyPath = ""
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
}
} else { } else {
endpoint.TLS = false endpoint.TLSConfig.TLS = false
endpoint.TLSCACertPath = "" endpoint.TLSConfig.TLSSkipVerify = true
endpoint.TLSCertPath = "" endpoint.TLSConfig.TLSCACertPath = ""
endpoint.TLSKeyPath = "" endpoint.TLSConfig.TLSCertPath = ""
endpoint.TLSConfig.TLSKeyPath = ""
err = handler.FileService.DeleteTLSFiles(folder) err = handler.FileService.DeleteTLSFiles(folder)
if err != nil { if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
@ -350,7 +379,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
return return
} }
if endpoint.TLS { if endpoint.TLSConfig.TLS {
err = handler.FileService.DeleteTLSFiles(id) err = handler.FileService.DeleteTLSFiles(id)
if err != nil { if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)

View File

@ -30,6 +30,7 @@ func NewFileHandler(assetPath string) *FileHandler {
"/js": true, "/js": true,
"/images": true, "/images": true,
"/fonts": true, "/fonts": true,
"/ico": true,
}, },
} }
return h return h

View File

@ -78,6 +78,10 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht
resourceControlType = portainer.ServiceResourceControl resourceControlType = portainer.ServiceResourceControl
case "volume": case "volume":
resourceControlType = portainer.VolumeResourceControl resourceControlType = portainer.VolumeResourceControl
case "network":
resourceControlType = portainer.NetworkResourceControl
case "secret":
resourceControlType = portainer.SecretResourceControl
default: default:
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger) httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
return return

View File

@ -82,6 +82,7 @@ type (
} }
postAdminInitRequest struct { postAdminInitRequest struct {
Username string `valid:"required"`
Password string `valid:"required"` Password string `valid:"required"`
} }
) )
@ -358,10 +359,14 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
return return
} }
user, err := handler.UserService.UserByUsername("admin") users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
if err == portainer.ErrUserNotFound { if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if len(users) == 0 {
user := &portainer.User{ user := &portainer.User{
Username: "admin", Username: req.Username,
Role: portainer.AdministratorRole, Role: portainer.AdministratorRole,
} }
user.Password, err = handler.CryptoService.Hash(req.Password) user.Password, err = handler.CryptoService.Hash(req.Password)
@ -375,11 +380,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger) httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return return
} }
} else if err != nil { } else {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if user != nil {
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger) httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
return return
} }
@ -396,6 +397,22 @@ func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Requ
return return
} }
if userID == 1 {
httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveAdmin, http.StatusForbidden, handler.Logger)
return
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
if tokenData.ID == portainer.UserID(userID) {
httperror.WriteErrorResponse(w, portainer.ErrAdminCannotRemoveSelf, http.StatusForbidden, handler.Logger)
return
}
_, err = handler.UserService.User(portainer.UserID(userID)) _, err = handler.UserService.User(portainer.UserID(userID))
if err == portainer.ErrUserNotFound { if err == portainer.ErrUserNotFound {

View File

@ -71,8 +71,8 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
// Should not be managed here // Should not be managed here
var tlsConfig *tls.Config var tlsConfig *tls.Config
if endpoint.TLS { if endpoint.TLSConfig.TLS {
tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false) tlsConfig, err = crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
if err != nil { if err != nil {
log.Fatalf("Unable to create TLS configuration: %s", err) log.Fatalf("Unable to create TLS configuration: %s", err)
return return

View File

@ -82,6 +82,54 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer
return decoratedServiceData, nil return decoratedServiceData, nil
} }
// decorateNetworkList loops through all networks and will decorate any network with an existing resource control.
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
func decorateNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
decoratedNetworkData := make([]interface{}, 0)
for _, network := range networkData {
networkObject := network.(map[string]interface{})
if networkObject[networkIdentifier] == nil {
return nil, ErrDockerNetworkIdentifierNotFound
}
networkID := networkObject[networkIdentifier].(string)
resourceControl := getResourceControlByResourceID(networkID, resourceControls)
if resourceControl != nil {
networkObject = decorateObject(networkObject, resourceControl)
}
decoratedNetworkData = append(decoratedNetworkData, networkObject)
}
return decoratedNetworkData, nil
}
// decorateSecretList loops through all secrets and will decorate any secret with an existing resource control.
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
func decorateSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
decoratedSecretData := make([]interface{}, 0)
for _, secret := range secretData {
secretObject := secret.(map[string]interface{})
if secretObject[secretIdentifier] == nil {
return nil, ErrDockerSecretIdentifierNotFound
}
secretID := secretObject[secretIdentifier].(string)
resourceControl := getResourceControlByResourceID(secretID, resourceControls)
if resourceControl != nil {
secretObject = decorateObject(secretObject, resourceControl)
}
decoratedSecretData = append(decoratedSecretData, secretObject)
}
return decoratedSecretData, nil
}
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} { func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
metadata := make(map[string]interface{}) metadata := make(map[string]interface{})
metadata["ResourceControl"] = resourceControl metadata["ResourceControl"] = resourceControl

View File

@ -24,7 +24,7 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) { func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
u.Scheme = "https" u.Scheme = "https"
proxy := factory.createReverseProxy(u) proxy := factory.createReverseProxy(u)
config, err := crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false) config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -110,3 +110,76 @@ func filterServiceList(serviceData []interface{}, resourceControls []portainer.R
return filteredServiceData, nil return filteredServiceData, nil
} }
// filterNetworkList loops through all networks, filters networks without any resource control (public resources) or with
// any resource control giving access to the user (these networks will be decorated).
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
func filterNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
filteredNetworkData := make([]interface{}, 0)
for _, network := range networkData {
networkObject := network.(map[string]interface{})
if networkObject[networkIdentifier] == nil {
return nil, ErrDockerNetworkIdentifierNotFound
}
networkID := networkObject[networkIdentifier].(string)
resourceControl := getResourceControlByResourceID(networkID, resourceControls)
if resourceControl == nil {
filteredNetworkData = append(filteredNetworkData, networkObject)
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
networkObject = decorateObject(networkObject, resourceControl)
filteredNetworkData = append(filteredNetworkData, networkObject)
}
}
return filteredNetworkData, nil
}
// filterSecretList loops through all secrets, filters secrets without any resource control (public resources) or with
// any resource control giving access to the user (these secrets will be decorated).
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
func filterSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
filteredSecretData := make([]interface{}, 0)
for _, secret := range secretData {
secretObject := secret.(map[string]interface{})
if secretObject[secretIdentifier] == nil {
return nil, ErrDockerSecretIdentifierNotFound
}
secretID := secretObject[secretIdentifier].(string)
resourceControl := getResourceControlByResourceID(secretID, resourceControls)
if resourceControl == nil {
filteredSecretData = append(filteredSecretData, secretObject)
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
secretObject = decorateObject(secretObject, resourceControl)
filteredSecretData = append(filteredSecretData, secretObject)
}
}
return filteredSecretData, nil
}
// filterTaskList loops through all tasks, filters tasks without any resource control (public resources) or with
// any resource control giving access to the user based on the associated service identifier.
// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
func filterTaskList(taskData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
filteredTaskData := make([]interface{}, 0)
for _, task := range taskData {
taskObject := task.(map[string]interface{})
if taskObject[taskServiceIdentifier] == nil {
return nil, ErrDockerTaskServiceIdentifierNotFound
}
serviceID := taskObject[taskServiceIdentifier].(string)
resourceControl := getResourceControlByResourceID(serviceID, resourceControls)
if resourceControl == nil || (resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl)) {
filteredTaskData = append(filteredTaskData, taskObject)
}
}
return filteredTaskData, nil
}

View File

@ -37,7 +37,7 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
} }
if endpointURL.Scheme == "tcp" { if endpointURL.Scheme == "tcp" {
if endpoint.TLS { if endpoint.TLSConfig.TLS {
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint) proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -0,0 +1,66 @@
package proxy
import (
"net/http"
"github.com/portainer/portainer"
)
const (
// ErrDockerNetworkIdentifierNotFound defines an error raised when Portainer is unable to find a network identifier
ErrDockerNetworkIdentifierNotFound = portainer.Error("Docker network identifier not found")
networkIdentifier = "Id"
)
// networkListOperation extracts the response as a JSON object, loop through the networks array
// decorate and/or filter the networks based on resource controls before rewriting the response
func networkListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
var err error
// NetworkList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
responseArray, err := getResponseAsJSONArray(response)
if err != nil {
return err
}
if executor.operationContext.isAdmin {
responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls)
} else {
responseArray, err = filterNetworkList(responseArray, executor.operationContext.resourceControls,
executor.operationContext.userID, executor.operationContext.userTeamIDs)
}
if err != nil {
return err
}
return rewriteResponse(response, responseArray, http.StatusOK)
}
// networkInspectOperation extracts the response as a JSON object, verify that the user
// has access to the network based on resource control and either rewrite an access denied response
// or a decorated network.
func networkInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
// NetworkInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
responseObject, err := getResponseAsJSONOBject(response)
if err != nil {
return err
}
if responseObject[networkIdentifier] == nil {
return ErrDockerNetworkIdentifierNotFound
}
networkID := responseObject[networkIdentifier].(string)
resourceControl := getResourceControlByResourceID(networkID, executor.operationContext.resourceControls)
if resourceControl != nil {
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
executor.operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
}
}
return rewriteResponse(response, responseObject, http.StatusOK)
}

67
api/http/proxy/secrets.go Normal file
View File

@ -0,0 +1,67 @@
package proxy
import (
"net/http"
"github.com/portainer/portainer"
)
const (
// ErrDockerSecretIdentifierNotFound defines an error raised when Portainer is unable to find a secret identifier
ErrDockerSecretIdentifierNotFound = portainer.Error("Docker secret identifier not found")
secretIdentifier = "ID"
)
// secretListOperation extracts the response as a JSON object, loop through the secrets array
// decorate and/or filter the secrets based on resource controls before rewriting the response
func secretListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
var err error
// SecretList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
responseArray, err := getResponseAsJSONArray(response)
if err != nil {
return err
}
if executor.operationContext.isAdmin {
responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
} else {
responseArray, err = filterSecretList(responseArray, executor.operationContext.resourceControls,
executor.operationContext.userID, executor.operationContext.userTeamIDs)
}
if err != nil {
return err
}
return rewriteResponse(response, responseArray, http.StatusOK)
}
// secretInspectOperation extracts the response as a JSON object, verify that the user
// has access to the secret based on resource control (check are done based on the secretID and optional Swarm service ID)
// and either rewrite an access denied response or a decorated secret.
func secretInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
// SecretInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
responseObject, err := getResponseAsJSONOBject(response)
if err != nil {
return err
}
if responseObject[secretIdentifier] == nil {
return ErrDockerSecretIdentifierNotFound
}
secretID := responseObject[secretIdentifier].(string)
resourceControl := getResourceControlByResourceID(secretID, executor.operationContext.resourceControls)
if resourceControl != nil {
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
executor.operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
}
}
return rewriteResponse(response, responseObject, http.StatusOK)
}

View File

@ -34,6 +34,9 @@ func (proxy *socketProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Add(k, v) w.Header().Add(k, v)
} }
} }
w.WriteHeader(res.StatusCode)
if _, err := io.Copy(w, res.Body); err != nil { if _, err := io.Copy(w, res.Body); err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil) httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
} }

36
api/http/proxy/tasks.go Normal file
View File

@ -0,0 +1,36 @@
package proxy
import (
"net/http"
"github.com/portainer/portainer"
)
const (
// ErrDockerTaskServiceIdentifierNotFound defines an error raised when Portainer is unable to find the service identifier associated to a task
ErrDockerTaskServiceIdentifierNotFound = portainer.Error("Docker task service identifier not found")
taskServiceIdentifier = "ServiceID"
)
// taskListOperation extracts the response as a JSON object, loop through the tasks array
// and filter the tasks based on resource controls before rewriting the response
func taskListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
var err error
// TaskList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/TaskList
responseArray, err := getResponseAsJSONArray(response)
if err != nil {
return err
}
if !executor.operationContext.isAdmin {
responseArray, err = filterTaskList(responseArray, executor.operationContext.resourceControls,
executor.operationContext.userID, executor.operationContext.userTeamIDs)
if err != nil {
return err
}
}
return rewriteResponse(response, responseArray, http.StatusOK)
}

View File

@ -53,17 +53,26 @@ func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Resp
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) { func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
path := request.URL.Path path := request.URL.Path
if strings.HasPrefix(path, "/containers") { switch {
case strings.HasPrefix(path, "/containers"):
return p.proxyContainerRequest(request) return p.proxyContainerRequest(request)
} else if strings.HasPrefix(path, "/services") { case strings.HasPrefix(path, "/services"):
return p.proxyServiceRequest(request) return p.proxyServiceRequest(request)
} else if strings.HasPrefix(path, "/volumes") { case strings.HasPrefix(path, "/volumes"):
return p.proxyVolumeRequest(request) return p.proxyVolumeRequest(request)
} else if strings.HasPrefix(path, "/swarm") { case strings.HasPrefix(path, "/networks"):
return p.proxyNetworkRequest(request)
case strings.HasPrefix(path, "/secrets"):
return p.proxySecretRequest(request)
case strings.HasPrefix(path, "/swarm"):
return p.proxySwarmRequest(request) return p.proxySwarmRequest(request)
case strings.HasPrefix(path, "/nodes"):
return p.proxyNodeRequest(request)
case strings.HasPrefix(path, "/tasks"):
return p.proxyTaskRequest(request)
default:
return p.executeDockerRequest(request)
} }
return p.executeDockerRequest(request)
} }
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) { func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
@ -145,10 +154,67 @@ func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Respon
} }
} }
func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/networks/create":
return p.executeDockerRequest(request)
case "/networks":
return p.rewriteOperation(request, networkListOperation)
default:
// assume /networks/{id}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, networkInspectOperation)
}
networkID := path.Base(requestPath)
return p.restrictedOperation(request, networkID)
}
}
func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/secrets/create":
return p.executeDockerRequest(request)
case "/secrets":
return p.rewriteOperation(request, secretListOperation)
default:
// assume /secrets/{id}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, secretInspectOperation)
}
secretID := path.Base(requestPath)
return p.restrictedOperation(request, secretID)
}
}
func (p *proxyTransport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
requestPath := request.URL.Path
// assume /nodes/{id}
if path.Base(requestPath) != "nodes" {
return p.administratorOperation(request)
}
return p.executeDockerRequest(request)
}
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) { func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
return p.administratorOperation(request) return p.administratorOperation(request)
} }
func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/tasks":
return p.rewriteOperation(request, taskListOperation)
default:
// assume /tasks/{id}
return p.executeDockerRequest(request)
}
}
// restrictedOperation ensures that the current user has the required authorizations // restrictedOperation ensures that the current user has the required authorizations
// before executing the original request. // before executing the original request.
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) { func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {

View File

@ -52,7 +52,7 @@ func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearc
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) { func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
if settings.TLSConfig.TLS || settings.StartTLS { if settings.TLSConfig.TLS || settings.StartTLS {
config, err := crypto.CreateTLSConfiguration(settings.TLSConfig.TLSCACertPath, "", "", settings.TLSConfig.TLSSkipVerify) config, err := crypto.CreateTLSConfiguration(&settings.TLSConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -155,16 +155,20 @@ type (
// Endpoint represents a Docker endpoint with all the info required // Endpoint represents a Docker endpoint with all the info required
// to connect to it. // to connect to it.
Endpoint struct { Endpoint struct {
ID EndpointID `json:"Id"` ID EndpointID `json:"Id"`
Name string `json:"Name"` Name string `json:"Name"`
URL string `json:"URL"` URL string `json:"URL"`
PublicURL string `json:"PublicURL"` PublicURL string `json:"PublicURL"`
TLS bool `json:"TLS"` TLSConfig TLSConfiguration `json:"TLSConfig"`
TLSCACertPath string `json:"TLSCACert,omitempty"` AuthorizedUsers []UserID `json:"AuthorizedUsers"`
TLSCertPath string `json:"TLSCert,omitempty"` AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
TLSKeyPath string `json:"TLSKey,omitempty"`
AuthorizedUsers []UserID `json:"AuthorizedUsers"` // Deprecated fields
AuthorizedTeams []TeamID `json:"AuthorizedTeams"` // Deprecated in DBVersion == 4
TLS bool `json:"TLS,omitempty"`
TLSCACertPath string `json:"TLSCACert,omitempty"`
TLSCertPath string `json:"TLSCert,omitempty"`
TLSKeyPath string `json:"TLSKey,omitempty"`
} }
// ResourceControlID represents a resource control identifier. // ResourceControlID represents a resource control identifier.
@ -172,20 +176,18 @@ type (
// ResourceControl represent a reference to a Docker resource with specific access controls // ResourceControl represent a reference to a Docker resource with specific access controls
ResourceControl struct { ResourceControl struct {
ID ResourceControlID `json:"Id"` ID ResourceControlID `json:"Id"`
ResourceID string `json:"ResourceId"` ResourceID string `json:"ResourceId"`
SubResourceIDs []string `json:"SubResourceIds"` SubResourceIDs []string `json:"SubResourceIds"`
Type ResourceControlType `json:"Type"` Type ResourceControlType `json:"Type"`
AdministratorsOnly bool `json:"AdministratorsOnly"` AdministratorsOnly bool `json:"AdministratorsOnly"`
UserAccesses []UserResourceAccess `json:"UserAccesses"`
UserAccesses []UserResourceAccess `json:"UserAccesses"` TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
// Deprecated fields // Deprecated fields
// Deprecated: OwnerID field is deprecated in DBVersion == 2 // Deprecated in DBVersion == 2
OwnerID UserID `json:"OwnerId"` OwnerID UserID `json:"OwnerId,omitempty"`
// Deprecated: AccessLevel field is deprecated in DBVersion == 2 AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
} }
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service). // ResourceControlType represents the type of resource associated to the resource control (volume, container, service).
@ -325,6 +327,7 @@ type (
FileService interface { FileService interface {
StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error
GetPathForTLSFile(folder string, fileType TLSFileType) (string, error) GetPathForTLSFile(folder string, fileType TLSFileType) (string, error)
DeleteTLSFile(folder string, fileType TLSFileType) error
DeleteTLSFiles(folder string) error DeleteTLSFiles(folder string) error
} }
@ -342,9 +345,9 @@ type (
const ( const (
// APIVersion is the version number of the Portainer API. // APIVersion is the version number of the Portainer API.
APIVersion = "1.14.0" APIVersion = "1.14.1"
// DBVersion is the version number of the Portainer database. // DBVersion is the version number of the Portainer database.
DBVersion = 3 DBVersion = 4
// DefaultTemplatesURL represents the default URL for the templates definitions. // DefaultTemplatesURL represents the default URL for the templates definitions.
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json" DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
) )
@ -396,4 +399,8 @@ const (
ServiceResourceControl ServiceResourceControl
// VolumeResourceControl represents a resource control associated to a Docker volume // VolumeResourceControl represents a resource control associated to a Docker volume
VolumeResourceControl VolumeResourceControl
// NetworkResourceControl represents a resource control associated to a Docker network
NetworkResourceControl
// SecretResourceControl represents a resource control associated to a Docker secret
SecretResourceControl
) )

View File

@ -1,27 +1,62 @@
--- ---
swagger: "2.0" swagger: "2.0"
info: info:
description: "Portainer API is an HTTP API served by Portainer. It is used by the\ description: |
\ Portainer UI and everything you can do with the UI can be done using the HTTP\ Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API.
\ API.\nYou can find out more about Portainer at [http://portainer.io](http://portainer.io)\ Examples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8
\ and get some support on [Slack](http://portainer.io/slack/).\n\n# Authentication\n\ You can find out more about Portainer at [http://portainer.io](http://portainer.io) and get some support on [Slack](http://portainer.io/slack/).
\nMost of the API endpoints require to be authenticated as well as some level\
\ of authorization to be used.\nPortainer API uses JSON Web Token to manage authentication\ # Authentication
\ and thus requires you to provide a token in the **Authorization** header of\
\ each request\nwith the **Bearer** authentication mechanism.\n\nExample:\n```\n\ Most of the API endpoints require to be authenticated as well as some level of authorization to be used.
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE\n\ Portainer API uses JSON Web Token to manage authentication and thus requires you to provide a token in the **Authorization** header of each request
```\n\n# Security\n\nEach API endpoint has an associated access policy, it is\ with the **Bearer** authentication mechanism.
\ documented in the description of each endpoint.\n\nDifferent access policies\
\ are available:\n* Public access\n* Authenticated access\n* Restricted access\n\ Example:
* Administrator access\n\n### Public access\n\nNo authentication is required to\ ```
\ access the endpoints with this access policy.\n\n### Authenticated access\n\n\ Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE
Authentication is required to access the endpoints with this access policy.\n\n\ ```
### Restricted access\n\nAuthentication is required to access the endpoints with\
\ this access policy.\nExtra-checks might be added to ensure access to the resource\ # Security
\ is granted. Returned data might also be filtered.\n\n### Administrator access\n\
\nAuthentication as well as an administrator role are required to access the endpoints\ Each API endpoint has an associated access policy, it is documented in the description of each endpoint.
\ with this access policy.\n"
version: "1.14.0" Different access policies are available:
* Public access
* Authenticated access
* Restricted access
* Administrator access
### Public access
No authentication is required to access the endpoints with this access policy.
### Authenticated access
Authentication is required to access the endpoints with this access policy.
### Restricted access
Authentication is required to access the endpoints with this access policy.
Extra-checks might be added to ensure access to the resource is granted. Returned data might also be filtered.
### Administrator access
Authentication as well as an administrator role are required to access the endpoints with this access policy.
# Execute Docker requests
Portainer **DO NOT** expose specific endpoints to manage your Docker resources (create a container, remove a volume, etc...).
Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you can execute Docker requests **via** the Portainer HTTP API.
To do so, you can use the `/endpoints/{id}/docker` Portainer API endpoint (which is not documented below due to Swagger limitations). This
endpoint has a restricted access policy so you still need to be authenticated to be able to query this endpoint. Any query on this endpoint will be proxied to the
Docker API of the associated endpoint (requests and responses objects are the same as documented in the Docker API).
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.14.1"
title: "Portainer API" title: "Portainer API"
contact: contact:
email: "info@portainer.io" email: "info@portainer.io"
@ -63,8 +98,9 @@ paths:
tags: tags:
- "auth" - "auth"
summary: "Authenticate a user" summary: "Authenticate a user"
description: "Use this endpoint to authenticate against Portainer using a username\ description: |
\ and password. \n**Access policy**: public\n" Use this endpoint to authenticate against Portainer using a username and password.
**Access policy**: public
operationId: "AuthenticateUser" operationId: "AuthenticateUser"
consumes: consumes:
- "application/json" - "application/json"
@ -105,8 +141,9 @@ paths:
tags: tags:
- "dockerhub" - "dockerhub"
summary: "Retrieve DockerHub information" summary: "Retrieve DockerHub information"
description: "Use this endpoint to retrieve the information used to connect\ description: |
\ to the DockerHub \n**Access policy**: authenticated\n" Use this endpoint to retrieve the information used to connect to the DockerHub
**Access policy**: authenticated
operationId: "DockerHubInspect" operationId: "DockerHubInspect"
produces: produces:
- "application/json" - "application/json"
@ -124,8 +161,9 @@ paths:
tags: tags:
- "dockerhub" - "dockerhub"
summary: "Update DockerHub information" summary: "Update DockerHub information"
description: "Use this endpoint to update the information used to connect to\ description: |
\ the DockerHub \n**Access policy**: administrator\n" Use this endpoint to update the information used to connect to the DockerHub
**Access policy**: administrator
operationId: "DockerHubUpdate" operationId: "DockerHubUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -157,9 +195,11 @@ paths:
tags: tags:
- "endpoints" - "endpoints"
summary: "List endpoints" summary: "List endpoints"
description: "List all endpoints based on the current user authorizations. Will\n\ description: |
return all endpoints if using an administrator account otherwise it will\n\ List all endpoints based on the current user authorizations. Will
only return authorized endpoints. \n**Access policy**: restricted \n" return all endpoints if using an administrator account otherwise it will
only return authorized endpoints.
**Access policy**: restricted
operationId: "EndpointList" operationId: "EndpointList"
produces: produces:
- "application/json" - "application/json"
@ -177,8 +217,9 @@ paths:
tags: tags:
- "endpoints" - "endpoints"
summary: "Create a new endpoint" summary: "Create a new endpoint"
description: "Create a new endpoint that will be used to manage a Docker environment.\ description: |
\ \n**Access policy**: administrator\n" Create a new endpoint that will be used to manage a Docker environment.
**Access policy**: administrator
operationId: "EndpointCreate" operationId: "EndpointCreate"
consumes: consumes:
- "application/json" - "application/json"
@ -219,8 +260,9 @@ paths:
tags: tags:
- "endpoints" - "endpoints"
summary: "Inspect an endpoint" summary: "Inspect an endpoint"
description: "Retrieve details abount an endpoint. \n**Access policy**: administrator\ description: |
\ \n" Retrieve details abount an endpoint.
**Access policy**: administrator
operationId: "EndpointInspect" operationId: "EndpointInspect"
produces: produces:
- "application/json" - "application/json"
@ -257,7 +299,9 @@ paths:
tags: tags:
- "endpoints" - "endpoints"
summary: "Update an endpoint" summary: "Update an endpoint"
description: "Update an endpoint. \n**Access policy**: administrator\n" description: |
Update an endpoint.
**Access policy**: administrator
operationId: "EndpointUpdate" operationId: "EndpointUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -307,7 +351,9 @@ paths:
tags: tags:
- "endpoints" - "endpoints"
summary: "Remove an endpoint" summary: "Remove an endpoint"
description: "Remove an endpoint. \n**Access policy**: administrator \n" description: |
Remove an endpoint.
**Access policy**: administrator
operationId: "EndpointDelete" operationId: "EndpointDelete"
parameters: parameters:
- name: "id" - name: "id"
@ -348,8 +394,9 @@ paths:
tags: tags:
- "endpoints" - "endpoints"
summary: "Manage accesses to an endpoint" summary: "Manage accesses to an endpoint"
description: "Manage user and team accesses to an endpoint. \n**Access policy**:\ description: |
\ administrator \n" Manage user and team accesses to an endpoint.
**Access policy**: administrator
operationId: "EndpointAccessUpdate" operationId: "EndpointAccessUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -388,15 +435,17 @@ paths:
description: "Server error" description: "Server error"
schema: schema:
$ref: "#/definitions/GenericError" $ref: "#/definitions/GenericError"
/registries: /registries:
get: get:
tags: tags:
- "registries" - "registries"
summary: "List registries" summary: "List registries"
description: "List all registries based on the current user authorizations.\n\ description: |
Will return all registries if using an administrator account otherwise it\n\ List all registries based on the current user authorizations.
will only return authorized registries. \n**Access policy**: restricted \ Will return all registries if using an administrator account otherwise it
\ \n" will only return authorized registries.
**Access policy**: restricted
operationId: "RegistryList" operationId: "RegistryList"
produces: produces:
- "application/json" - "application/json"
@ -414,8 +463,9 @@ paths:
tags: tags:
- "registries" - "registries"
summary: "Create a new registry" summary: "Create a new registry"
description: "Create a new registry. \n**Access policy**: administrator \ description: |
\ \n" Create a new registry.
**Access policy**: administrator
operationId: "RegistryCreate" operationId: "RegistryCreate"
consumes: consumes:
- "application/json" - "application/json"
@ -456,8 +506,9 @@ paths:
tags: tags:
- "registries" - "registries"
summary: "Inspect a registry" summary: "Inspect a registry"
description: "Retrieve details about a registry. \n**Access policy**: administrator\ description: |
\ \n" Retrieve details about a registry.
**Access policy**: administrator
operationId: "RegistryInspect" operationId: "RegistryInspect"
produces: produces:
- "application/json" - "application/json"
@ -494,7 +545,9 @@ paths:
tags: tags:
- "registries" - "registries"
summary: "Update a registry" summary: "Update a registry"
description: "Update a registry. \n**Access policy**: administrator \n" description: |
Update a registry.
**Access policy**: administrator
operationId: "RegistryUpdate" operationId: "RegistryUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -551,8 +604,9 @@ paths:
tags: tags:
- "registries" - "registries"
summary: "Remove a registry" summary: "Remove a registry"
description: "Remove a registry. \n**Access policy**: administrator \ description: |
\ \n" Remove a registry.
**Access policy**: administrator
operationId: "RegistryDelete" operationId: "RegistryDelete"
parameters: parameters:
- name: "id" - name: "id"
@ -586,8 +640,9 @@ paths:
tags: tags:
- "registries" - "registries"
summary: "Manage accesses to a registry" summary: "Manage accesses to a registry"
description: "Manage user and team accesses to a registry. \n**Access policy**:\ description: |
\ administrator \n" Manage user and team accesses to a registry.
**Access policy**: administrator
operationId: "RegistryAccessUpdate" operationId: "RegistryAccessUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -631,8 +686,9 @@ paths:
tags: tags:
- "resource_controls" - "resource_controls"
summary: "Create a new resource control" summary: "Create a new resource control"
description: "Create a new resource control to restrict access to a Docker resource.\ description: |
\ \n**Access policy**: restricted \n" Create a new resource control to restrict access to a Docker resource.
**Access policy**: restricted
operationId: "ResourceControlCreate" operationId: "ResourceControlCreate"
consumes: consumes:
- "application/json" - "application/json"
@ -678,8 +734,9 @@ paths:
tags: tags:
- "resource_controls" - "resource_controls"
summary: "Update a resource control" summary: "Update a resource control"
description: "Update a resource control. \n**Access policy**: restricted \ description: |
\ \n" Update a resource control.
**Access policy**: restricted
operationId: "ResourceControlUpdate" operationId: "ResourceControlUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -729,8 +786,9 @@ paths:
tags: tags:
- "resource_controls" - "resource_controls"
summary: "Remove a resource control" summary: "Remove a resource control"
description: "Remove a resource control. \n**Access policy**: restricted \ description: |
\ \n" Remove a resource control.
**Access policy**: restricted
operationId: "ResourceControlDelete" operationId: "ResourceControlDelete"
parameters: parameters:
- name: "id" - name: "id"
@ -771,8 +829,9 @@ paths:
tags: tags:
- "settings" - "settings"
summary: "Retrieve Portainer settings" summary: "Retrieve Portainer settings"
description: "Retrieve Portainer settings. \n**Access policy**: administrator\ description: |
\ \n" Retrieve Portainer settings.
**Access policy**: administrator
operationId: "SettingsInspect" operationId: "SettingsInspect"
produces: produces:
- "application/json" - "application/json"
@ -790,8 +849,9 @@ paths:
tags: tags:
- "settings" - "settings"
summary: "Update Portainer settings" summary: "Update Portainer settings"
description: "Update Portainer settings. \n**Access policy**: administrator\ description: |
\ \n" Update Portainer settings.
**Access policy**: administrator
operationId: "SettingsUpdate" operationId: "SettingsUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -823,9 +883,9 @@ paths:
tags: tags:
- "settings" - "settings"
summary: "Retrieve Portainer public settings" summary: "Retrieve Portainer public settings"
description: "Retrieve public settings. Returns a small set of settings that\ description: |
\ are not reserved to administrators only. \n**Access policy**: public \ Retrieve public settings. Returns a small set of settings that are not reserved to administrators only.
\ \n" **Access policy**: public
operationId: "PublicSettingsInspect" operationId: "PublicSettingsInspect"
produces: produces:
- "application/json" - "application/json"
@ -844,8 +904,9 @@ paths:
tags: tags:
- "settings" - "settings"
summary: "Test LDAP connectivity" summary: "Test LDAP connectivity"
description: "Test LDAP connectivity using LDAP details. \n**Access policy**:\ description: |
\ administrator \n" Test LDAP connectivity using LDAP details.
**Access policy**: administrator
operationId: "SettingsLDAPCheck" operationId: "SettingsLDAPCheck"
consumes: consumes:
- "application/json" - "application/json"
@ -877,8 +938,9 @@ paths:
tags: tags:
- "status" - "status"
summary: "Check Portainer status" summary: "Check Portainer status"
description: "Retrieve Portainer status. \n**Access policy**: public \ description: |
\ \n" Retrieve Portainer status.
**Access policy**: public
operationId: "StatusInspect" operationId: "StatusInspect"
produces: produces:
- "application/json" - "application/json"
@ -897,9 +959,9 @@ paths:
tags: tags:
- "users" - "users"
summary: "List users" summary: "List users"
description: "List Portainer users. Non-administrator users will only be able\ description: |
\ to list other non-administrator user accounts. \n**Access policy**: restricted\ List Portainer users. Non-administrator users will only be able to list other non-administrator user accounts.
\ \n" **Access policy**: restricted
operationId: "UserList" operationId: "UserList"
produces: produces:
- "application/json" - "application/json"
@ -917,9 +979,10 @@ paths:
tags: tags:
- "users" - "users"
summary: "Create a new user" summary: "Create a new user"
description: "Create a new Portainer user. Only team leaders and administrators\ description: |
\ can create users. Only administrators can\ncreate an administrator user\ Create a new Portainer user. Only team leaders and administrators can create users. Only administrators can
\ account. \n**Access policy**: restricted \n" create an administrator user account.
**Access policy**: restricted
operationId: "UserCreate" operationId: "UserCreate"
consumes: consumes:
- "application/json" - "application/json"
@ -967,8 +1030,9 @@ paths:
tags: tags:
- "users" - "users"
summary: "Inspect a user" summary: "Inspect a user"
description: "Retrieve details about a user. \n**Access policy**: administrator\ description: |
\ \n" Retrieve details about a user.
**Access policy**: administrator
operationId: "UserInspect" operationId: "UserInspect"
produces: produces:
- "application/json" - "application/json"
@ -1005,8 +1069,9 @@ paths:
tags: tags:
- "users" - "users"
summary: "Update a user" summary: "Update a user"
description: "Update user details. A regular user account can only update his\ description: |
\ details. \n**Access policy**: authenticated \n" Update user details. A regular user account can only update his details.
**Access policy**: authenticated
operationId: "UserUpdate" operationId: "UserUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -1056,7 +1121,9 @@ paths:
tags: tags:
- "users" - "users"
summary: "Remove a user" summary: "Remove a user"
description: "Remove a user. \n**Access policy**: administrator \n" description: |
Remove a user.
**Access policy**: administrator
operationId: "UserDelete" operationId: "UserDelete"
parameters: parameters:
- name: "id" - name: "id"
@ -1090,8 +1157,9 @@ paths:
tags: tags:
- "users" - "users"
summary: "Inspect a user memberships" summary: "Inspect a user memberships"
description: "Inspect a user memberships. \n**Access policy**: authenticated\ description: |
\ \n" Inspect a user memberships.
**Access policy**: authenticated
operationId: "UserMembershipsInspect" operationId: "UserMembershipsInspect"
produces: produces:
- "application/json" - "application/json"
@ -1124,13 +1192,15 @@ paths:
description: "Server error" description: "Server error"
schema: schema:
$ref: "#/definitions/GenericError" $ref: "#/definitions/GenericError"
/users/{id}/passwd: /users/{id}/passwd:
post: post:
tags: tags:
- "users" - "users"
summary: "Check password validity for a user" summary: "Check password validity for a user"
description: "Check if the submitted password is valid for the specified user.\ description: |
\ \n**Access policy**: authenticated \n" Check if the submitted password is valid for the specified user.
**Access policy**: authenticated
operationId: "UserPasswordCheck" operationId: "UserPasswordCheck"
consumes: consumes:
- "application/json" - "application/json"
@ -1171,13 +1241,15 @@ paths:
description: "Server error" description: "Server error"
schema: schema:
$ref: "#/definitions/GenericError" $ref: "#/definitions/GenericError"
/users/admin/check: /users/admin/check:
get: get:
tags: tags:
- "users" - "users"
summary: "Check administrator account existence" summary: "Check administrator account existence"
description: "Check if an administrator account exists in the database.\n**Access\ description: |
\ policy**: public \n" Check if an administrator account exists in the database.
**Access policy**: public
operationId: "UserAdminCheck" operationId: "UserAdminCheck"
produces: produces:
- "application/json" - "application/json"
@ -1198,13 +1270,15 @@ paths:
description: "Server error" description: "Server error"
schema: schema:
$ref: "#/definitions/GenericError" $ref: "#/definitions/GenericError"
/users/admin/init: /users/admin/init:
post: post:
tags: tags:
- "users" - "users"
summary: "Initialize administrator account" summary: "Initialize administrator account"
description: "Initialize the 'admin' user account.\n**Access policy**: public\ description: |
\ \n" Initialize the 'admin' user account.
**Access policy**: public
operationId: "UserAdminInit" operationId: "UserAdminInit"
consumes: consumes:
- "application/json" - "application/json"
@ -1238,34 +1312,35 @@ paths:
description: "Server error" description: "Server error"
schema: schema:
$ref: "#/definitions/GenericError" $ref: "#/definitions/GenericError"
/upload/tls/{certificate}: /upload/tls/{certificate}:
post: post:
tags: tags:
- "upload" - "upload"
summary: "Upload TLS files" summary: "Upload TLS files"
description: "Use this endpoint to upload TLS files. \n**Access policy**: administrator\n" description: |
Use this endpoint to upload TLS files.
**Access policy**: administrator
operationId: "UploadTLS" operationId: "UploadTLS"
consumes: consumes:
- "multipart/form-data" - multipart/form-data
produces: produces:
- "application/json" - "application/json"
parameters: parameters:
- name: "certificate" - in: "path"
in: "path" name: "certificate"
description: "TLS file type. Valid values are 'ca', 'cert' or 'key'." description: "TLS file type. Valid values are 'ca', 'cert' or 'key'."
required: true required: true
type: "string" type: "string"
- name: "folder" - in: "query"
in: "query" name: "folder"
description: "Folder where the TLS file will be stored. Will be created if\ description: "Folder where the TLS file will be stored. Will be created if not existing."
\ not existing."
required: true required: true
type: "string" type: "string"
- name: "file" - in: "formData"
in: "formData" name: "file"
description: "The file to upload."
required: false
type: "file" type: "file"
description: "The file to upload."
responses: responses:
200: 200:
description: "Success" description: "Success"
@ -1280,13 +1355,15 @@ paths:
description: "Server error" description: "Server error"
schema: schema:
$ref: "#/definitions/GenericError" $ref: "#/definitions/GenericError"
/teams: /teams:
get: get:
tags: tags:
- "teams" - "teams"
summary: "List teams" summary: "List teams"
description: "List teams. For non-administrator users, will only list the teams\ description: |
\ they are member of. \n**Access policy**: restricted \n" List teams. For non-administrator users, will only list the teams they are member of.
**Access policy**: restricted
operationId: "TeamList" operationId: "TeamList"
produces: produces:
- "application/json" - "application/json"
@ -1304,8 +1381,9 @@ paths:
tags: tags:
- "teams" - "teams"
summary: "Create a new team" summary: "Create a new team"
description: "Create a new team. \n**Access policy**: administrator \ description: |
\ \n" Create a new team.
**Access policy**: administrator
operationId: "TeamCreate" operationId: "TeamCreate"
consumes: consumes:
- "application/json" - "application/json"
@ -1353,8 +1431,9 @@ paths:
tags: tags:
- "teams" - "teams"
summary: "Inspect a team" summary: "Inspect a team"
description: "Retrieve details about a team. Access is only available for administrator\ description: |
\ and leaders of that team. \n**Access policy**: restricted \n" Retrieve details about a team. Access is only available for administrator and leaders of that team.
**Access policy**: restricted
operationId: "TeamInspect" operationId: "TeamInspect"
produces: produces:
- "application/json" - "application/json"
@ -1398,8 +1477,9 @@ paths:
tags: tags:
- "teams" - "teams"
summary: "Update a team" summary: "Update a team"
description: "Update a team. \n**Access policy**: administrator \ description: |
\ \n" Update a team.
**Access policy**: administrator
operationId: "TeamUpdate" operationId: "TeamUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -1442,7 +1522,9 @@ paths:
tags: tags:
- "teams" - "teams"
summary: "Remove a team" summary: "Remove a team"
description: "Remove a team. \n**Access policy**: administrator \n" description: |
Remove a team.
**Access policy**: administrator
operationId: "TeamDelete" operationId: "TeamDelete"
parameters: parameters:
- name: "id" - name: "id"
@ -1471,13 +1553,15 @@ paths:
description: "Server error" description: "Server error"
schema: schema:
$ref: "#/definitions/GenericError" $ref: "#/definitions/GenericError"
/teams/{id}/memberships: /teams/{id}/memberships:
get: get:
tags: tags:
- "teams" - "teams"
summary: "Inspect a team memberships" summary: "Inspect a team memberships"
description: "Inspect a team memberships. Access is only available for administrator\ description: |
\ and leaders of that team. \n**Access policy**: restricted \n" Inspect a team memberships. Access is only available for administrator and leaders of that team.
**Access policy**: restricted
operationId: "TeamMembershipsInspect" operationId: "TeamMembershipsInspect"
produces: produces:
- "application/json" - "application/json"
@ -1510,13 +1594,15 @@ paths:
description: "Server error" description: "Server error"
schema: schema:
$ref: "#/definitions/GenericError" $ref: "#/definitions/GenericError"
/team_memberships: /team_memberships:
get: get:
tags: tags:
- "team_memberships" - "team_memberships"
summary: "List team memberships" summary: "List team memberships"
description: "List team memberships. Access is only available to administrators\ description: |
\ and team leaders. \n**Access policy**: restricted \n" List team memberships. Access is only available to administrators and team leaders.
**Access policy**: restricted
operationId: "TeamMembershipList" operationId: "TeamMembershipList"
produces: produces:
- "application/json" - "application/json"
@ -1541,8 +1627,9 @@ paths:
tags: tags:
- "team_memberships" - "team_memberships"
summary: "Create a new team membership" summary: "Create a new team membership"
description: "Create a new team memberships. Access is only available to administrators\ description: |
\ leaders of the associated team. \n**Access policy**: restricted \n" Create a new team memberships. Access is only available to administrators leaders of the associated team.
**Access policy**: restricted
operationId: "TeamMembershipCreate" operationId: "TeamMembershipCreate"
consumes: consumes:
- "application/json" - "application/json"
@ -1590,9 +1677,9 @@ paths:
tags: tags:
- "team_memberships" - "team_memberships"
summary: "Update a team membership" summary: "Update a team membership"
description: "Update a team membership. Access is only available to administrators\ description: |
\ leaders of the associated team. \n**Access policy**: restricted \ Update a team membership. Access is only available to administrators leaders of the associated team.
\ \n" **Access policy**: restricted
operationId: "TeamMembershipUpdate" operationId: "TeamMembershipUpdate"
consumes: consumes:
- "application/json" - "application/json"
@ -1642,8 +1729,9 @@ paths:
tags: tags:
- "team_memberships" - "team_memberships"
summary: "Remove a team membership" summary: "Remove a team membership"
description: "Remove a team membership. Access is only available to administrators\ description: |
\ leaders of the associated team. \n**Access policy**: restricted \n" Remove a team membership. Access is only available to administrators leaders of the associated team.
**Access policy**: restricted
operationId: "TeamMembershipDelete" operationId: "TeamMembershipDelete"
parameters: parameters:
- name: "id" - name: "id"
@ -1684,17 +1772,18 @@ paths:
tags: tags:
- "templates" - "templates"
summary: "Retrieve App templates" summary: "Retrieve App templates"
description: "Retrieve App templates. \nYou can find more information about\ description: |
\ the format at http://portainer.readthedocs.io/en/stable/templates.html \ Retrieve App templates.
\ \n**Access policy**: authenticated \n" You can find more information about the format at http://portainer.readthedocs.io/en/stable/templates.html
**Access policy**: authenticated
operationId: "TemplateList" operationId: "TemplateList"
produces: produces:
- "application/json" - "application/json"
parameters: parameters:
- name: "key" - name: "key"
in: "query" in: "query"
description: "Templates key. Valid values are 'container' or 'linuxserver.io'."
required: true required: true
description: "Templates key. Valid values are 'container' or 'linuxserver.io'."
type: "string" type: "string"
responses: responses:
200: 200:
@ -1780,7 +1869,7 @@ definitions:
description: "Is analytics enabled" description: "Is analytics enabled"
Version: Version:
type: "string" type: "string"
example: "1.14.0" example: "1.14.1"
description: "Portainer API version" description: "Portainer API version"
PublicSettingsInspectResponse: PublicSettingsInspectResponse:
type: "object" type: "object"
@ -1799,8 +1888,8 @@ definitions:
AuthenticationMethod: AuthenticationMethod:
type: "integer" type: "integer"
example: 1 example: 1
description: "Active authentication method for the Portainer instance. Valid\ description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
\ values are: 1 for managed or 2 for LDAP."
TLSConfiguration: TLSConfiguration:
type: "object" type: "object"
properties: properties:
@ -1824,14 +1913,14 @@ definitions:
type: "string" type: "string"
example: "/data/tls/key.pem" example: "/data/tls/key.pem"
description: "Path to the TLS client key file" description: "Path to the TLS client key file"
LDAPSearchSettings: LDAPSearchSettings:
type: "object" type: "object"
properties: properties:
BaseDN: BaseDN:
type: "string" type: "string"
example: "dc=ldap,dc=domain,dc=tld" example: "dc=ldap,dc=domain,dc=tld"
description: "The distinguished name of the element from which the LDAP server\ description: "The distinguished name of the element from which the LDAP server will search for users"
\ will search for users"
Filter: Filter:
type: "string" type: "string"
example: "(objectClass=account)" example: "(objectClass=account)"
@ -1840,6 +1929,7 @@ definitions:
type: "string" type: "string"
example: "uid" example: "uid"
description: "LDAP attribute which denotes the username" description: "LDAP attribute which denotes the username"
LDAPSettings: LDAPSettings:
type: "object" type: "object"
properties: properties:
@ -1865,6 +1955,7 @@ definitions:
type: "array" type: "array"
items: items:
$ref: "#/definitions/LDAPSearchSettings" $ref: "#/definitions/LDAPSearchSettings"
Settings: Settings:
type: "object" type: "object"
properties: properties:
@ -1893,8 +1984,7 @@ definitions:
AuthenticationMethod: AuthenticationMethod:
type: "integer" type: "integer"
example: 1 example: 1
description: "Active authentication method for the Portainer instance. Valid\ description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
\ values are: 1 for managed or 2 for LDAP."
LDAPSettings: LDAPSettings:
$ref: "#/definitions/LDAPSettings" $ref: "#/definitions/LDAPSettings"
Settings_BlackListedLabels: Settings_BlackListedLabels:
@ -2060,6 +2150,14 @@ definitions:
type: "boolean" type: "boolean"
example: true example: true
description: "Require TLS to connect against this endpoint" description: "Require TLS to connect against this endpoint"
TLSSkipVerify:
type: "boolean"
example: false
description: "Skip server verification when using TLS"
TLSSkipClientVerify:
type: "boolean"
example: false
description: "Skip client verification when using TLS"
EndpointCreateResponse: EndpointCreateResponse:
type: "object" type: "object"
properties: properties:
@ -2091,6 +2189,14 @@ definitions:
type: "boolean" type: "boolean"
example: true example: true
description: "Require TLS to connect against this endpoint" description: "Require TLS to connect against this endpoint"
TLSSkipVerify:
type: "boolean"
example: false
description: "Skip server verification when using TLS"
TLSSkipClientVerify:
type: "boolean"
example: false
description: "Skip client verification when using TLS"
EndpointAccessUpdateRequest: EndpointAccessUpdateRequest:
type: "object" type: "object"
properties: properties:
@ -2257,8 +2363,8 @@ definitions:
SettingsUpdateRequest: SettingsUpdateRequest:
type: "object" type: "object"
required: required:
- "AuthenticationMethod"
- "TemplatesURL" - "TemplatesURL"
- "AuthenticationMethod"
properties: properties:
TemplatesURL: TemplatesURL:
type: "string" type: "string"
@ -2285,8 +2391,7 @@ definitions:
AuthenticationMethod: AuthenticationMethod:
type: "integer" type: "integer"
example: 1 example: 1
description: "Active authentication method for the Portainer instance. Valid\ description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
\ values are: 1 for managed or 2 for LDAP."
LDAPSettings: LDAPSettings:
$ref: "#/definitions/LDAPSettings" $ref: "#/definitions/LDAPSettings"
UserCreateRequest: UserCreateRequest:
@ -2383,12 +2488,13 @@ definitions:
type: "array" type: "array"
items: items:
$ref: "#/definitions/TeamMembership" $ref: "#/definitions/TeamMembership"
TeamMembershipCreateRequest: TeamMembershipCreateRequest:
type: "object" type: "object"
required: required:
- "Role"
- "TeamID"
- "UserID" - "UserID"
- "TeamID"
- "Role"
properties: properties:
UserID: UserID:
type: "integer" type: "integer"
@ -2401,8 +2507,7 @@ definitions:
Role: Role:
type: "integer" type: "integer"
example: 1 example: 1
description: "Role for the user inside the team (1 for leader and 2 for regular\ description: "Role for the user inside the team (1 for leader and 2 for regular member)"
\ member)"
TeamMembershipCreateResponse: TeamMembershipCreateResponse:
type: "object" type: "object"
properties: properties:
@ -2417,9 +2522,9 @@ definitions:
TeamMembershipUpdateRequest: TeamMembershipUpdateRequest:
type: "object" type: "object"
required: required:
- "Role"
- "TeamID"
- "UserID" - "UserID"
- "TeamID"
- "Role"
properties: properties:
UserID: UserID:
type: "integer" type: "integer"
@ -2432,8 +2537,7 @@ definitions:
Role: Role:
type: "integer" type: "integer"
example: 1 example: 1
description: "Role for the user inside the team (1 for leader and 2 for regular\ description: "Role for the user inside the team (1 for leader and 2 for regular member)"
\ member)"
SettingsLDAPCheckRequest: SettingsLDAPCheckRequest:
type: "object" type: "object"
properties: properties:

View File

@ -23,6 +23,7 @@ angular.module('portainer', [
'container', 'container',
'containerConsole', 'containerConsole',
'containerLogs', 'containerLogs',
'containerStats',
'serviceLogs', 'serviceLogs',
'containers', 'containers',
'createContainer', 'createContainer',
@ -31,14 +32,15 @@ angular.module('portainer', [
'createSecret', 'createSecret',
'createService', 'createService',
'createVolume', 'createVolume',
'docker', 'engine',
'endpoint', 'endpoint',
'endpointAccess', 'endpointAccess',
'endpointInit',
'endpoints', 'endpoints',
'events', 'events',
'image', 'image',
'images', 'images',
'initAdmin',
'initEndpoint',
'main', 'main',
'network', 'network',
'networks', 'networks',
@ -53,8 +55,8 @@ angular.module('portainer', [
'settings', 'settings',
'settingsAuthentication', 'settingsAuthentication',
'sidebar', 'sidebar',
'stats',
'swarm', 'swarm',
'swarmVisualizer',
'task', 'task',
'team', 'team',
'teams', 'teams',
@ -63,7 +65,8 @@ angular.module('portainer', [
'users', 'users',
'userSettings', 'userSettings',
'volume', 'volume',
'volumes']) 'volumes',
'rzModule'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) { .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
'use strict'; 'use strict';
@ -157,8 +160,8 @@ angular.module('portainer', [
url: '^/containers/:id/stats', url: '^/containers/:id/stats',
views: { views: {
'content@': { 'content@': {
templateUrl: 'app/components/stats/stats.html', templateUrl: 'app/components/containerStats/containerStats.html',
controller: 'StatsController' controller: 'ContainerStatsController'
}, },
'sidebar@': { 'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html', templateUrl: 'app/components/sidebar/sidebar.html',
@ -321,12 +324,39 @@ angular.module('portainer', [
} }
} }
}) })
.state('docker', { .state('init', {
url: '/docker/', abstract: true,
url: '/init',
views: { views: {
'content@': { 'content@': {
templateUrl: 'app/components/docker/docker.html', template: '<div ui-view="content@"></div>'
controller: 'DockerController' }
}
})
.state('init.endpoint', {
url: '/endpoint',
views: {
'content@': {
templateUrl: 'app/components/initEndpoint/initEndpoint.html',
controller: 'InitEndpointController'
}
}
})
.state('init.admin', {
url: '/admin',
views: {
'content@': {
templateUrl: 'app/components/initAdmin/initAdmin.html',
controller: 'InitAdminController'
}
}
})
.state('engine', {
url: '/engine/',
views: {
'content@': {
templateUrl: 'app/components/engine/engine.html',
controller: 'EngineController'
}, },
'sidebar@': { 'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html', templateUrl: 'app/components/sidebar/sidebar.html',
@ -373,15 +403,6 @@ angular.module('portainer', [
} }
} }
}) })
.state('endpointInit', {
url: '/init/endpoint',
views: {
'content@': {
templateUrl: 'app/components/endpointInit/endpointInit.html',
controller: 'EndpointInitController'
}
}
})
.state('events', { .state('events', {
url: '/events/', url: '/events/',
views: { views: {
@ -716,7 +737,7 @@ angular.module('portainer', [
} }
}) })
.state('swarm', { .state('swarm', {
url: '/swarm/', url: '/swarm',
views: { views: {
'content@': { 'content@': {
templateUrl: 'app/components/swarm/swarm.html', templateUrl: 'app/components/swarm/swarm.html',
@ -727,7 +748,21 @@ angular.module('portainer', [
controller: 'SidebarController' controller: 'SidebarController'
} }
} }
}); })
.state('swarm.visualizer', {
url: '/visualizer',
views: {
'content@': {
templateUrl: 'app/components/swarmVisualizer/swarmVisualizer.html',
controller: 'SwarmVisualizerController'
},
'sidebar@': {
templateUrl: 'app/components/sidebar/sidebar.html',
controller: 'SidebarController'
}
}
})
;
}]) }])
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) { .run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) {
EndpointProvider.initialize(); EndpointProvider.initialize();

View File

@ -1,92 +1,38 @@
<div class="page-wrapper"> <div class="page-wrapper">
<!-- login box --> <!-- login box -->
<div class="container simple-box"> <div class="container simple-box">
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3"> <div class="col-sm-6 col-sm-offset-3">
<!-- login box logo --> <!-- login box logo -->
<div class="row"> <div class="row">
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer"> <img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
</div> </div>
<!-- !login box logo --> <!-- !login box logo -->
<!-- init password panel -->
<div class="panel panel-default" ng-if="initPassword">
<div class="panel-body">
<!-- init password form -->
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
Please specify a password for the <b>admin</b> user account.
</p>
</div>
<!-- !comment input -->
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password.length >= 8]" aria-hidden="true"></i>
Your password must be at least 8 characters long
</p>
</div>
<!-- !comment input -->
<!-- password input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="admin_password" type="password" class="form-control" name="password" ng-model="initPasswordData.password" autofocus>
</div>
<!-- !password input -->
<!-- comment -->
<div class="input-group">
<p style="margin: 5px;">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password !== '' && initPasswordData.password === initPasswordData.password_confirmation]" aria-hidden="true"></i>
Confirm your password
</p>
</div>
<!-- !comment input -->
<!-- password confirmation input -->
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="password_confirmation" type="password" class="form-control" name="password" ng-model="initPasswordData.password_confirmation">
</div>
<!-- !password confirmation input -->
<!-- validate button -->
<div class="form-group">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="initPasswordData.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Unable to create default user
</p>
<button type="submit" class="btn btn-primary pull-right" ng-disabled="initPasswordData.password.length < 8 || initPasswordData.password !== initPasswordData.password_confirmation" ng-click="createAdminUser()"><i class="fa fa-key" aria-hidden="true"></i> Validate</button>
</div>
</div>
<!-- !validate button -->
</form>
<!-- !init password form -->
</div>
</div>
<!-- !init password panel -->
<!-- login panel --> <!-- login panel -->
<div class="panel panel-default" ng-if="!initPassword"> <div class="panel panel-default">
<div class="panel-body"> <div class="panel-body">
<!-- login form --> <!-- login form -->
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST"> <form class="simple-box-form form-horizontal">
<!-- username input --> <!-- username input -->
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span> <span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
<input id="username" type="text" class="form-control" name="username" ng-model="authData.username" placeholder="Username"> <input id="username" type="text" class="form-control" name="username" ng-model="formValues.Username" auto-focus>
</div> </div>
<!-- !username input --> <!-- !username input -->
<!-- password input --> <!-- password input -->
<div class="input-group"> <div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span> <span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input id="password" type="password" class="form-control" name="password" ng-model="authData.password" autofocus> <input id="password" type="password" class="form-control" name="password" ng-model="formValues.Password">
</div> </div>
<!-- !password input --> <!-- !password input -->
<!-- login button --> <!-- login button -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12 controls"> <div class="col-sm-12">
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;"> <button type="submit" class="btn btn-primary btn-sm pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }} <span class="pull-left" style="margin: 5px;" ng-if="state.AuthenticationError">
</p> <i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button> <span class="small text-danger">{{ state.AuthenticationError }}</span>
</span>
</div> </div>
</div> </div>
<!-- !login button --> <!-- !login button -->

View File

@ -1,113 +1,110 @@
angular.module('auth', []) angular.module('auth', [])
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', .controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService',
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) { function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) {
$scope.authData = {
username: 'admin',
password: '',
error: ''
};
$scope.initPasswordData = {
password: '',
password_confirmation: '',
error: false
};
$scope.logo = StateManager.getState().application.logo; $scope.logo = StateManager.getState().application.logo;
if (!$scope.applicationState.application.authentication) { $scope.formValues = {
EndpointService.endpoints() Username: '',
.then(function success(data) { Password: ''
if (data.length > 0) {
endpointID = EndpointProvider.endpointID();
if (!endpointID) {
endpointID = data[0].Id;
EndpointProvider.setEndpointID(endpointID);
}
StateManager.updateEndpointState(true)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
});
}
else {
$state.go('endpointInit');
}
}, function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
});
} else {
Users.checkAdminUser({}, function () {},
function (e) {
if (e.status === 404) {
$scope.initPassword = true;
} else {
Notifications.error('Failure', e, 'Unable to verify administrator account existence');
}
});
}
if ($stateParams.logout) {
Authentication.logout();
}
if ($stateParams.error) {
$scope.authData.error = $stateParams.error;
Authentication.logout();
}
if (Authentication.isAuthenticated()) {
$state.go('dashboard');
}
$scope.createAdminUser = function() {
var password = $sanitize($scope.initPasswordData.password);
Users.initAdminUser({password: password}, function (d) {
$scope.initPassword = false;
$timeout(function() {
var element = $window.document.getElementById('password');
if(element) {
element.focus();
}
});
}, function (e) {
$scope.initPassword.error = true;
});
}; };
$scope.authenticateUser = function() { $scope.state = {
$scope.authenticationError = false; AuthenticationError: ''
var username = $sanitize($scope.authData.username); };
var password = $sanitize($scope.authData.password);
Authentication.login(username, password) function setActiveEndpointAndRedirectToDashboard(endpoint) {
var endpointID = EndpointProvider.endpointID();
if (!endpointID) {
EndpointProvider.setEndpointID(endpoint.Id);
}
StateManager.updateEndpointState(true)
.then(function success(data) { .then(function success(data) {
return EndpointService.endpoints(); $state.go('dashboard');
}) })
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
});
}
function unauthenticatedFlow() {
EndpointService.endpoints()
.then(function success(data) { .then(function success(data) {
var userDetails = Authentication.getUserDetails(); var endpoints = data;
if (data.length > 0) { if (endpoints.length > 0) {
endpointID = EndpointProvider.endpointID(); setActiveEndpointAndRedirectToDashboard(endpoints[0]);
if (!endpointID) { } else {
endpointID = data[0].Id; $state.go('init.endpoint');
EndpointProvider.setEndpointID(endpointID);
}
StateManager.updateEndpointState(true)
.then(function success() {
$state.go('dashboard');
}, function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
});
}
else if (data.length === 0 && userDetails.role === 1) {
$state.go('endpointInit');
} else if (data.length === 0 && userDetails.role === 2) {
Authentication.logout();
$scope.authData.error = 'User not allowed. Please contact your administrator.';
} }
}) })
.catch(function error(err) { .catch(function error(err) {
$scope.authData.error = 'Authentication error'; Notifications.error('Failure', err, 'Unable to retrieve endpoints');
});
}
function authenticatedFlow() {
UserService.administratorExists()
.then(function success(exists) {
if (!exists) {
$state.go('init.admin');
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to verify administrator account existence');
});
}
$scope.authenticateUser = function() {
var username = $scope.formValues.Username;
var password = $scope.formValues.Password;
SettingsService.publicSettings()
.then(function success(data) {
var settings = data;
if (settings.AuthenticationMethod === 1) {
username = $sanitize(username);
password = $sanitize(password);
}
return Authentication.login(username, password);
})
.then(function success() {
return EndpointService.endpoints();
})
.then(function success(data) {
var endpoints = data;
var userDetails = Authentication.getUserDetails();
if (endpoints.length > 0) {
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
} else if (endpoints.length === 0 && userDetails.role === 1) {
$state.go('init.endpoint');
} else if (endpoints.length === 0 && userDetails.role === 2) {
Authentication.logout();
$scope.state.AuthenticationError = 'User not allowed. Please contact your administrator.';
}
})
.catch(function error() {
$scope.state.AuthenticationError = 'Invalid credentials';
}); });
}; };
function initView() {
if ($stateParams.logout || $stateParams.error) {
Authentication.logout();
$scope.state.AuthenticationError = $stateParams.error;
return;
}
if (Authentication.isAuthenticated()) {
$state.go('dashboard');
}
var authenticationEnabled = $scope.applicationState.application.authentication;
if (!authenticationEnabled) {
unauthenticatedFlow();
} else {
authenticatedFlow();
}
}
initView();
}]); }]);

View File

@ -20,8 +20,8 @@
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button> <button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button> <button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
<button class="btn btn-danger" ng-click="confirmRemove()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button> <button class="btn btn-danger" ng-click="confirmRemove()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
<button class="btn btn-danger" ng-click="recreate()"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Recreate</button> <button class="btn btn-danger" ng-click="recreate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Recreate</button>
<button class="btn btn-primary" ng-click="duplicate()"><i class="fa fa-files-o space-right" aria-hidden="true"></i>Duplicate/Edit</button> <button class="btn btn-primary" ng-click="duplicate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-files-o space-right" aria-hidden="true"></i>Duplicate/Edit</button>
</div> </div>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>

View File

@ -0,0 +1,123 @@
<rd-header>
<rd-header-title title="Container statistics">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Stats
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-md-12">
<rd-widget>
<rd-widget-header icon="fa-info-circle" title="About statistics">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
This view displays real-time statistics about the container <b>{{ container.Name|trimcontainername }}</b> as well as a list of the running processes
inside this container.
</span>
</div>
</div>
<div class="form-group">
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left">
Refresh rate
</label>
<div class="col-sm-3 col-md-2">
<select id="refreshRate" ng-model="state.refreshRate" ng-change="changeUpdateRepeater()" class="form-control">
<option value="5">5s</option>
<option value="10">10s</option>
<option value="30">30s</option>
<option value="60">60s</option>
</select>
</div>
<span>
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
</span>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-4 col-md-6 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="memoryChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-4 col-md-6 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="cpuChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-4 col-md-12 col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
<rd-widget-body>
<div class="chart-container" style="position: relative;">
<canvas id="networkChart" width="770" height="300"></canvas>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-sm-12" ng-if="applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Processes">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th ng-repeat="title in processInfo.Titles">
<a ng-click="order(title)">
{{ title }}
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="processDetails in state.filteredProcesses = (processInfo.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td ng-repeat="procInfo in processDetails track by $index">{{ procInfo }}</td>
</tr>
<tr ng-if="!processInfo.Processes">
<td colspan="processInfo.Titles.length" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="state.filteredProcesses.length === 0">
<td colspan="processInfo.Titles.length" class="text-center text-muted">No processes available.</td>
</tr>
</tbody>
</table>
<div ng-if="processInfo.Processes" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -0,0 +1,159 @@
angular.module('containerStats', [])
.controller('ContainerStatsController', ['$q', '$scope', '$stateParams', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'Pagination',
function ($q, $scope, $stateParams, $document, $interval, ContainerService, ChartService, Notifications, Pagination) {
$scope.state = {
refreshRate: '5'
};
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
$scope.sortType = 'CMD';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
};
$scope.$on('$destroy', function() {
stopRepeater();
});
function stopRepeater() {
var repeater = $scope.repeater;
if (angular.isDefined(repeater)) {
$interval.cancel(repeater);
repeater = null;
}
}
function updateNetworkChart(stats, chart) {
var rx = stats.Networks[0].rx_bytes;
var tx = stats.Networks[0].tx_bytes;
var label = moment(stats.Date).format('HH:mm:ss');
ChartService.UpdateNetworkChart(label, rx, tx, chart);
}
function updateMemoryChart(stats, chart) {
var label = moment(stats.Date).format('HH:mm:ss');
var value = stats.MemoryUsage;
ChartService.UpdateMemoryChart(label, value, chart);
}
function updateCPUChart(stats, chart) {
var label = moment(stats.Date).format('HH:mm:ss');
var value = calculateCPUPercentUnix(stats);
ChartService.UpdateCPUChart(label, value, chart);
}
function calculateCPUPercentUnix(stats) {
var cpuPercent = 0.0;
var cpuDelta = stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage;
var systemDelta = stats.CurrentCPUSystemUsage - stats.PreviousCPUSystemUsage;
if (systemDelta > 0.0 && cpuDelta > 0.0) {
cpuPercent = (cpuDelta / systemDelta) * stats.CPUCores * 100.0;
}
return cpuPercent;
}
$scope.changeUpdateRepeater = function() {
var networkChart = $scope.networkChart;
var cpuChart = $scope.cpuChart;
var memoryChart = $scope.memoryChart;
stopRepeater();
setUpdateRepeater(networkChart, cpuChart, memoryChart);
$('#refreshRateChange').show();
$('#refreshRateChange').fadeOut(1500);
};
function startChartUpdate(networkChart, cpuChart, memoryChart) {
$('#loadingViewSpinner').show();
$q.all({
stats: ContainerService.containerStats($stateParams.id),
top: ContainerService.containerTop($stateParams.id)
})
.then(function success(data) {
var stats = data.stats;
$scope.processInfo = data.top;
updateNetworkChart(stats, networkChart);
updateMemoryChart(stats, memoryChart);
updateCPUChart(stats, cpuChart);
setUpdateRepeater(networkChart, cpuChart, memoryChart);
})
.catch(function error(err) {
stopRepeater();
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
function setUpdateRepeater(networkChart, cpuChart, memoryChart) {
var refreshRate = $scope.state.refreshRate;
$scope.repeater = $interval(function() {
$q.all({
stats: ContainerService.containerStats($stateParams.id),
top: ContainerService.containerTop($stateParams.id)
})
.then(function success(data) {
var stats = data.stats;
$scope.processInfo = data.top;
updateNetworkChart(stats, networkChart);
updateMemoryChart(stats, memoryChart);
updateCPUChart(stats, cpuChart);
})
.catch(function error(err) {
stopRepeater();
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
});
}, refreshRate * 1000);
}
function initCharts() {
var networkChartCtx = $('#networkChart');
var networkChart = ChartService.CreateNetworkChart(networkChartCtx);
$scope.networkChart = networkChart;
var cpuChartCtx = $('#cpuChart');
var cpuChart = ChartService.CreateCPUChart(cpuChartCtx);
$scope.cpuChart = cpuChart;
var memoryChartCtx = $('#memoryChart');
var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx);
$scope.memoryChart = memoryChart;
startChartUpdate(networkChart, cpuChart, memoryChart);
}
function initView() {
$('#loadingViewSpinner').show();
ContainerService.container($stateParams.id)
.then(function success(data) {
$scope.container = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve container information');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
$document.ready(function() {
initCharts();
});
}
initView();
}]);

View File

@ -61,6 +61,9 @@
<span ng-show="sortType == 'Names' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Names' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
<a data-toggle="tooltip" title="More" ng-click="truncateMore();" ng-show="showMore">
<i class="fa fa-plus-square" aria-hidden="true"></i>
</a>
</th> </th>
<th> <th>
<a ui-sref="containers" ng-click="order('Image')"> <a ui-sref="containers" ng-click="order('Image')">
@ -106,8 +109,8 @@
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) !== -1" class="label label-{{ container.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ container.Status }}</span> <span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) !== -1" class="label label-{{ container.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ container.Status }}</span>
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) === -1" class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span> <span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) === -1" class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span>
</td> </td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername|truncate: 40}}</a></td> <td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername|truncate: truncate_size}}</a></td>
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: 40}}</a></td> <td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: truncate_size}}</a></td>
<td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></td> <td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></td>
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td> <td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td> <td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>

View File

@ -1,13 +1,16 @@
angular.module('containers', []) angular.module('containers', [])
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'SystemService', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider', .controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'SystemService', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider', 'LocalStorage',
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, SystemService, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) { function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, SystemService, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider, LocalStorage) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers'); $scope.state.pagination_count = Pagination.getPaginationCount('containers');
$scope.state.displayAll = true; $scope.state.displayAll = LocalStorage.getFilterContainerShowAll();
$scope.state.displayIP = false; $scope.state.displayIP = false;
$scope.sortType = 'State'; $scope.sortType = 'State';
$scope.sortReverse = false; $scope.sortReverse = false;
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
$scope.truncate_size = 40;
$scope.showMore = true;
$scope.order = function (sortType) { $scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType; $scope.sortType = sortType;
@ -130,6 +133,7 @@ angular.module('containers', [])
}; };
$scope.toggleGetAll = function () { $scope.toggleGetAll = function () {
LocalStorage.storeFilterContainerShowAll($scope.state.displayAll);
update({all: $scope.state.displayAll ? 1 : 0}); update({all: $scope.state.displayAll ? 1 : 0});
}; };
@ -161,6 +165,12 @@ angular.module('containers', [])
batch($scope.containers, Container.remove, 'Removed'); batch($scope.containers, Container.remove, 'Removed');
}; };
$scope.truncateMore = function(size) {
$scope.truncate_size = 80;
$scope.showMore = false;
};
$scope.confirmRemoveAction = function () { $scope.confirmRemoveAction = function () {
var isOneContainerRunning = false; var isOneContainerRunning = false;
angular.forEach($scope.containers, function (c) { angular.forEach($scope.containers, function (c) {
@ -205,7 +215,7 @@ angular.module('containers', [])
if(container.Status === 'paused') { if(container.Status === 'paused') {
$scope.state.noPausedItemsSelected = false; $scope.state.noPausedItemsSelected = false;
} else if(container.Status === 'stopped' || } else if(container.Status === 'stopped' ||
container.Status === 'created') { container.Status === 'created') {
$scope.state.noStoppedItemsSelected = false; $scope.state.noStoppedItemsSelected = false;
} else if(container.Status === 'running') { } else if(container.Status === 'running') {

View File

@ -403,7 +403,6 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper,
} }
function loadFromContainerImageConfig(d) { function loadFromContainerImageConfig(d) {
// If no registry found, we let default DockerHub and let full image path
var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image); var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image);
RegistryService.retrieveRegistryFromRepository($scope.config.Image) RegistryService.retrieveRegistryFromRepository($scope.config.Image)
.then(function success(data) { .then(function success(data) {

View File

@ -21,24 +21,30 @@
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Image configuration Image configuration
</div> </div>
<!-- image-and-registry --> <div ng-if="!formValues.Registry && fromContainer">
<div class="form-group"> <i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry> <span class="small text-danger" style="margin-left: 5px;">The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register that registry first.</span>
</div> </div>
<!-- !image-and-registry --> <div ng-if="formValues.Registry || !fromContainer">
<!-- always-pull --> <!-- image-and-registry -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry>
<label for="ownership" class="control-label text-left">
Always pull the image
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically try to pull the specified image before creating the container."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
</label>
</div> </div>
<!-- !image-and-registry -->
<!-- always-pull -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Always pull the image
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically try to pull the specified image before creating the container."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
</label>
</div>
</div>
<!-- !always-pull -->
</div> </div>
<!-- !always-pull -->
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Ports configuration Ports configuration
</div> </div>
@ -106,7 +112,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="create()">Start container</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image || (!formValues.Registry && fromContainer)" ng-click="create()">Start container</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="containers">Cancel</a> <a type="button" class="btn btn-default btn-sm" ui-sref="containers">Cancel</a>
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i> <i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span> <span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>

View File

@ -1,13 +1,21 @@
angular.module('createNetwork', []) angular.module('createNetwork', [])
.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network', 'LabelHelper', .controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
function ($scope, $state, Notifications, Network, LabelHelper) { function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
$scope.formValues = { $scope.formValues = {
DriverOptions: [], DriverOptions: [],
Subnet: '', Subnet: '',
Gateway: '', Gateway: '',
Labels: [] Labels: [],
AccessControlData: new AccessControlFormData()
}; };
$scope.state = {
formValidationError: ''
};
$scope.availableNetworkDrivers = [];
$scope.config = { $scope.config = {
Driver: 'bridge', Driver: 'bridge',
CheckDuplicate: true, CheckDuplicate: true,
@ -37,23 +45,6 @@ function ($scope, $state, Notifications, Network, LabelHelper) {
$scope.formValues.Labels.splice(index, 1); $scope.formValues.Labels.splice(index, 1);
}; };
function createNetwork(config) {
$('#createNetworkSpinner').show();
Network.create(config, function (d) {
if (d.message) {
$('#createNetworkSpinner').hide();
Notifications.error('Unable to create network', {}, d.message);
} else {
Notifications.success('Network created', d.Id);
$('#createNetworkSpinner').hide();
$state.go('networks', {}, {reload: true});
}
}, function (e) {
$('#createNetworkSpinner').hide();
Notifications.error('Failure', e, 'Unable to create network');
});
}
function prepareIPAMConfiguration(config) { function prepareIPAMConfiguration(config) {
if ($scope.formValues.Subnet) { if ($scope.formValues.Subnet) {
var ipamConfig = {}; var ipamConfig = {};
@ -85,8 +76,66 @@ function ($scope, $state, Notifications, Network, LabelHelper) {
return config; return config;
} }
function validateForm(accessControlData, isAdmin) {
$scope.state.formValidationError = '';
var error = '';
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
$scope.state.formValidationError = error;
return false;
}
return true;
}
$scope.create = function () { $scope.create = function () {
var config = prepareConfiguration(); $('#createResourceSpinner').show();
createNetwork(config);
var networkConfiguration = prepareConfiguration();
var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false;
if (!validateForm(accessControlData, isAdmin)) {
$('#createResourceSpinner').hide();
return;
}
NetworkService.create(networkConfiguration)
.then(function success(data) {
var networkIdentifier = data.Id;
var userId = userDetails.ID;
return ResourceControlService.applyResourceControl('network', networkIdentifier, userId, accessControlData, []);
})
.then(function success() {
Notifications.success('Network successfully created');
$state.go('networks', {}, {reload: true});
})
.catch(function error(err) {
Notifications.error('Failure', err, 'An error occured during network creation');
})
.finally(function final() {
$('#createResourceSpinner').hide();
});
}; };
function initView() {
$('#loadingViewSpinner').show();
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
var apiVersion = $scope.applicationState.endpoint.apiVersion;
if(endpointProvider !== 'DOCKER_SWARM') {
PluginService.networkPlugins(apiVersion < 1.25)
.then(function success(data){
$scope.availableNetworkDrivers = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve network drivers');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
}
initView();
}]); }]);

View File

@ -1,5 +1,7 @@
<rd-header> <rd-header>
<rd-header-title title="Create network"></rd-header-title> <rd-header-title title="Create network">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
</rd-header-title>
<rd-header-content> <rd-header-content>
<a ui-sref="networks">Networks</a> &gt; Add network <a ui-sref="networks">Networks</a> &gt; Add network
</rd-header-content> </rd-header-content>
@ -39,8 +41,11 @@
<!-- driver-input --> <!-- driver-input -->
<div class="form-group"> <div class="form-group">
<label for="network_driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label> <label for="network_driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
<div class="col-sm-10"> <div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName"> <select class="form-control" ng-options="driver for driver in availableNetworkDrivers" ng-model="config.Driver" ng-if="availableNetworkDrivers.length > 0">
<option disabled hidden value="">Select a driver</option>
</select>
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName" ng-if="availableNetworkDrivers.length === 0">
</div> </div>
</div> </div>
<!-- !driver-input --> <!-- !driver-input -->
@ -116,6 +121,9 @@
</div> </div>
</div> </div>
<!-- !internal --> <!-- !internal -->
<!-- access-control -->
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
<!-- !access-control -->
<!-- actions --> <!-- actions -->
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Actions Actions
@ -124,7 +132,8 @@
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="create()">Create network</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="create()">Create network</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="networks">Cancel</a> <a type="button" class="btn btn-default btn-sm" ui-sref="networks">Cancel</a>
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i> <i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div> </div>
</div> </div>
<!-- !actions --> <!-- !actions -->

View File

@ -1,11 +1,17 @@
angular.module('createSecret', []) angular.module('createSecret', [])
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', .controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
function ($scope, $state, Notifications, SecretService, LabelHelper) { function ($scope, $state, Notifications, SecretService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
$scope.formValues = { $scope.formValues = {
Name: '', Name: '',
Data: '', Data: '',
Labels: [], Labels: [],
encodeSecret: true encodeSecret: true,
AccessControlData: new AccessControlFormData()
};
$scope.state = {
formValidationError: ''
}; };
$scope.addLabel = function() { $scope.addLabel = function() {
@ -36,10 +42,38 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
return config; return config;
} }
function createSecret(config) { function validateForm(accessControlData, isAdmin) {
$('#createSecretSpinner').show(); $scope.state.formValidationError = '';
SecretService.create(config) var error = '';
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
$scope.state.formValidationError = error;
return false;
}
return true;
}
$scope.create = function () {
$('#createResourceSpinner').show();
var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false;
if (!validateForm(accessControlData, isAdmin)) {
$('#createResourceSpinner').hide();
return;
}
var secretConfiguration = prepareConfiguration();
SecretService.create(secretConfiguration)
.then(function success(data) { .then(function success(data) {
var secretIdentifier = data.ID;
var userId = userDetails.ID;
return ResourceControlService.applyResourceControl('secret', secretIdentifier, userId, accessControlData, []);
})
.then(function success() {
Notifications.success('Secret successfully created'); Notifications.success('Secret successfully created');
$state.go('secrets', {}, {reload: true}); $state.go('secrets', {}, {reload: true});
}) })
@ -47,12 +81,7 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
Notifications.error('Failure', err, 'Unable to create secret'); Notifications.error('Failure', err, 'Unable to create secret');
}) })
.finally(function final() { .finally(function final() {
$('#createSecretSpinner').hide(); $('#createResourceSpinner').hide();
}); });
}
$scope.create = function () {
var config = prepareConfiguration();
createSecret(config);
}; };
}]); }]);

View File

@ -66,6 +66,9 @@
<!-- !labels-input-list --> <!-- !labels-input-list -->
</div> </div>
<!-- !labels--> <!-- !labels-->
<!-- access-control -->
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
<!-- !access-control -->
<!-- actions --> <!-- actions -->
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Actions Actions
@ -74,7 +77,8 @@
<div class="col-sm-12"> <div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.Data" ng-click="create()">Create secret</button> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.Data" ng-click="create()">Create secret</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="secrets">Cancel</a> <a type="button" class="btn btn-default btn-sm" ui-sref="secrets">Cancel</a>
<i id="createSecretSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i> <i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div> </div>
</div> </div>
<!-- !actions --> <!-- !actions -->

View File

@ -1,8 +1,8 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference. // See app/components/templates/templatesController.js as a reference.
angular.module('createService', []) angular.module('createService', [])
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', .controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', 'NodeService',
function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper) { function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService) {
$scope.formValues = { $scope.formValues = {
Name: '', Name: '',
@ -28,13 +28,25 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
UpdateOrder: 'stop-first', UpdateOrder: 'stop-first',
FailureAction: 'pause', FailureAction: 'pause',
Secrets: [], Secrets: [],
AccessControlData: new AccessControlFormData() AccessControlData: new AccessControlFormData(),
CpuLimit: 0,
CpuReservation: 0,
MemoryLimit: 0,
MemoryReservation: 0,
MemoryLimitUnit: 'MB',
MemoryReservationUnit: 'MB'
}; };
$scope.state = { $scope.state = {
formValidationError: '' formValidationError: ''
}; };
$scope.refreshSlider = function () {
$timeout(function () {
$scope.$broadcast('rzSliderForceRender');
});
};
$scope.addPortBinding = function() { $scope.addPortBinding = function() {
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' }); $scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
}; };
@ -224,6 +236,38 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
} }
} }
function prepareResourcesCpuConfig(config, input) {
// CPU Limit
if (input.CpuLimit > 0) {
config.TaskTemplate.Resources.Limits.NanoCPUs = input.CpuLimit * 1000000000;
}
// CPU Reservation
if (input.CpuReservation > 0) {
config.TaskTemplate.Resources.Reservations.NanoCPUs = input.CpuReservation * 1000000000;
}
}
function prepareResourcesMemoryConfig(config, input) {
// Memory Limit - Round to 0.125
var memoryLimit = (Math.round(input.MemoryLimit * 8) / 8).toFixed(3);
memoryLimit *= 1024 * 1024;
if (input.MemoryLimitUnit === 'GB') {
memoryLimit *= 1024;
}
if (memoryLimit > 0) {
config.TaskTemplate.Resources.Limits.MemoryBytes = memoryLimit;
}
// Memory Resevation - Round to 0.125
var memoryReservation = (Math.round(input.MemoryReservation * 8) / 8).toFixed(3);
memoryReservation *= 1024 * 1024;
if (input.MemoryReservationUnit === 'GB') {
memoryReservation *= 1024;
}
if (memoryReservation > 0) {
config.TaskTemplate.Resources.Reservations.MemoryBytes = memoryReservation;
}
}
function prepareConfiguration() { function prepareConfiguration() {
var input = $scope.formValues; var input = $scope.formValues;
var config = { var config = {
@ -232,7 +276,11 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
ContainerSpec: { ContainerSpec: {
Mounts: [] Mounts: []
}, },
Placement: {} Placement: {},
Resources: {
Limits: {},
Reservations: {}
}
}, },
Mode: {}, Mode: {},
EndpointSpec: {} EndpointSpec: {}
@ -248,6 +296,8 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
prepareUpdateConfig(config, input); prepareUpdateConfig(config, input);
prepareSecretConfig(config, input); prepareSecretConfig(config, input);
preparePlacementConfig(config, input); preparePlacementConfig(config, input);
prepareResourcesCpuConfig(config, input);
prepareResourcesMemoryConfig(config, input);
return config; return config;
} }
@ -305,16 +355,30 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
function initView() { function initView() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
var apiVersion = $scope.applicationState.endpoint.apiVersion; var apiVersion = $scope.applicationState.endpoint.apiVersion;
var provider = $scope.applicationState.endpoint.mode.provider;
$q.all({ $q.all({
volumes: VolumeService.volumes(), volumes: VolumeService.volumes(),
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
networks: NetworkService.networks(true, true, false, false) networks: NetworkService.networks(true, true, false, false),
nodes: NodeService.nodes()
}) })
.then(function success(data) { .then(function success(data) {
$scope.availableVolumes = data.volumes; $scope.availableVolumes = data.volumes;
$scope.availableNetworks = data.networks; $scope.availableNetworks = data.networks;
$scope.availableSecrets = data.secrets; $scope.availableSecrets = data.secrets;
// Set max cpu value
var maxCpus = 0;
for (var n in data.nodes) {
if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) {
maxCpus = data.nodes[n].CPUs;
}
}
if (maxCpus > 0) {
$scope.state.sliderMaxCpu = maxCpus / 1000000000;
} else {
$scope.state.sliderMaxCpu = 32;
}
}) })
.catch(function error(err) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to initialize view'); Notifications.error('Failure', err, 'Unable to initialize view');

View File

@ -133,7 +133,7 @@
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li> <li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li> <li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li>
<li class="interactive" ng-if="applicationState.endpoint.apiVersion >= 1.25"><a data-target="#secrets" data-toggle="tab">Secrets</a></li> <li class="interactive" ng-if="applicationState.endpoint.apiVersion >= 1.25"><a data-target="#secrets" data-toggle="tab">Secrets</a></li>
<li class="interactive"><a data-target="#placement" data-toggle="tab">Placement</a></li> <li class="interactive"><a data-target="#resources-placement" data-toggle="tab" ng-click="refreshSlider()">Resources & Placement</a></li>
</ul> </ul>
<!-- tab-content --> <!-- tab-content -->
<div class="tab-content"> <div class="tab-content">
@ -442,9 +442,9 @@
<!-- tab-secrets --> <!-- tab-secrets -->
<div class="tab-pane" id="secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" ng-include="'app/components/createService/includes/secret.html'"></div> <div class="tab-pane" id="secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" ng-include="'app/components/createService/includes/secret.html'"></div>
<!-- !tab-secrets --> <!-- !tab-secrets -->
<!-- tab-placement --> <!-- tab-resources-placement -->
<div class="tab-pane" id="placement" ng-include="'app/components/createService/includes/placement.html'"></div> <div class="tab-pane" id="resources-placement" ng-include="'app/components/createService/includes/resources-placement.html'"></div>
<!-- !tab-placement --> <!-- !tab-resources-placement -->
</div> </div>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>

View File

@ -1,57 +0,0 @@
<form class="form-horizontal" style="margin-top: 15px;">
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Placement constraints</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="constraint in formValues.PlacementConstraints" style="margin-top: 2px;">
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="constraint.key" placeholder="e.g. node.role">
</div>
<div class="input-group col-sm-1 input-group-sm">
<select name="constraintOperator" class="form-control" ng-model="constraint.operator">
<option value="==">==</option>
<option value="!=">!=</option>
</select>
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="constraint.value" placeholder="e.g. manager">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementConstraint($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</form>
<form class="form-horizontal" style="margin-top: 15px;" ng-if="applicationState.endpoint.apiVersion >= 1.30">
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Placement preferences</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementPreference()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement preference
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="preference in formValues.PlacementPreferences" style="margin-top: 2px;">
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">strategy</span>
<input type="text" class="form-control" ng-model="preference.strategy" placeholder="e.g. spread">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="preference.value" placeholder="e.g. node.labels.datacenter">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementPreference($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,136 @@
<form class="form-horizontal" style="margin-top: 15px;">
<div class="col-sm-12 form-section-title">
Resources
</div>
<!-- memory-reservation-input -->
<div class="form-group">
<label for="memory-reservation" class="col-sm-3 col-lg-2 control-label text-left">
Memory reservation
</label>
<div class="col-sm-3">
<input type="number" step="0.125" min="0" class="form-control" ng-model="formValues.MemoryReservation" id="memory-reservation" placeholder="e.g. 64">
</div>
<div class="col-sm-2">
<select class="form-control" ng-model="formValues.MemoryReservationUnit">
<option value="MB">MB</option>
<option value="GB">GB</option>
</select>
</div>
<div class="col-sm-4">
<p class="small text-muted">
Minimum memory available on a node to run a task
</p>
</div>
</div>
<!-- !memory-reservation-input -->
<!-- memory-limit-input -->
<div class="form-group">
<label for="memory-limit" class="col-sm-3 col-lg-2 control-label text-left">
Memory limit
</label>
<div class="col-sm-3">
<input type="number" step="0.125" min="0" class="form-control" ng-model="formValues.MemoryLimit" id="memory-limit" placeholder="e.g. 128">
</div>
<div class="col-sm-2">
<select class="form-control" ng-model="formValues.MemoryLimitUnit">
<option value="MB">MB</option>
<option value="GB">GB</option>
</select>
</div>
<div class="col-sm-4">
<p class="small text-muted">
Maximum memory usage per task (set to 0 for unlimited)
</p>
</div>
</div>
<!-- !memory-limit-input -->
<!-- cpu-reservation-input -->
<div class="form-group">
<label for="cpu-reservation" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
CPU reservation
</label>
<div class="col-sm-5">
<por-slider model="formValues.CpuReservation" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="state.sliderMaxCpu"></por-slider>
</div>
<div class="col-sm-4" style="margin-top: 20px;">
<p class="small text-muted">
Minimum CPU available on a node to run a task
</p>
</div>
</div>
<!-- !cpu-reservation-input -->
<!-- cpu-limit-input -->
<div class="form-group">
<label for="cpu-limit" class="col-sm-3 col-lg-2 control-label text-left" style="margin-top: 20px;">
CPU limit
</label>
<div class="col-sm-5">
<por-slider model="formValues.CpuLimit" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="state.sliderMaxCpu"></por-slider>
</div>
<div class="col-sm-4" style="margin-top: 20px;">
<p class="small text-muted">
Maximum CPU usage per task
</p>
</div>
</div>
<!-- !cpu-limit-input -->
<div class="col-sm-12 form-section-title">
Placement
</div>
<!-- placement-constraints -->
<div class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Placement constraints</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="constraint in formValues.PlacementConstraints" style="margin-top: 2px;">
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">name</span>
<input type="text" class="form-control" ng-model="constraint.key" placeholder="e.g. node.role">
</div>
<div class="input-group col-sm-1 input-group-sm">
<select name="constraintOperator" class="form-control" ng-model="constraint.operator">
<option value="==">==</option>
<option value="!=">!=</option>
</select>
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="constraint.value" placeholder="e.g. manager">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementConstraint($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- !placement-constraints -->
<!-- placement-preferences -->
<div class="form-group" ng-if="applicationState.endpoint.apiVersion >= 1.30">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Placement preferences</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementPreference()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement preference
</span>
</div>
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
<div ng-repeat="preference in formValues.PlacementPreferences" style="margin-top: 2px;">
<div class="input-group col-sm-4 input-group-sm">
<span class="input-group-addon">strategy</span>
<input type="text" class="form-control" ng-model="preference.strategy" placeholder="e.g. spread">
</div>
<div class="input-group col-sm-5 input-group-sm">
<span class="input-group-addon">value</span>
<input type="text" class="form-control" ng-model="preference.value" placeholder="e.g. node.labels.datacenter">
</div>
<button class="btn btn-sm btn-danger" type="button" ng-click="removePlacementPreference($index)">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- !placement-preferences -->
</form>

View File

@ -125,9 +125,6 @@
<div class="widget-icon blue pull-left"> <div class="widget-icon blue pull-left">
<i class="fa fa-cubes"></i> <i class="fa fa-cubes"></i>
</div> </div>
<div class="pull-right" ng-if="infoData.Driver">
<div><i class="fa fa-hdd-o space-right"></i>{{ infoData.Driver }} driver</div>
</div>
<div class="title">{{ volumeData.total }}</div> <div class="title">{{ volumeData.total }}</div>
<div class="comment">Volumes</div> <div class="comment">Volumes</div>
</rd-widget-body> </rd-widget-body>

View File

@ -12,6 +12,9 @@
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<form class="form-horizontal"> <form class="form-horizontal">
<div class="col-sm-12 form-section-title">
Configuration
</div>
<!-- name-input --> <!-- name-input -->
<div class="form-group"> <div class="form-group">
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label> <label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
@ -42,73 +45,19 @@
</div> </div>
</div> </div>
<!-- !endpoint-public-url-input --> <!-- !endpoint-public-url-input -->
<!-- tls-checkbox --> <!-- endpoint-security -->
<div class="form-group" ng-if="endpointType === 'remote'"> <div ng-if="endpointType === 'remote'">
<div class="col-sm-12"> <div class="col-sm-12 form-section-title">
<label for="tls" class="control-label text-left"> Security
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="endpoint.TLS"><i></i>
</label>
</div> </div>
<por-endpoint-security form-data="formValues.SecurityFormData" endpoint="endpoint"></por-endpoint-security>
</div> </div>
<!-- !tls-checkbox --> <!-- !endpoint-security -->
<!-- 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="form-group">
<div class="col-sm-12"> <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> <button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && ((endpoint.TLSVerify && !formValues.TLSCACert) || (endpoint.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="updateEndpoint()">Update endpoint</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="endpoints">Cancel</a> <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> <i id="updateResourceSpinner" 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>
</div> </div>
</form> </form>

View File

@ -7,35 +7,41 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications)
} }
$scope.state = { $scope.state = {
error: '',
uploadInProgress: false uploadInProgress: false
}; };
$scope.formValues = { $scope.formValues = {
TLSCACert: null, SecurityFormData: new EndpointSecurityFormData()
TLSCert: null,
TLSKey: null
}; };
$scope.updateEndpoint = function() { $scope.updateEndpoint = function() {
var ID = $scope.endpoint.Id; var endpoint = $scope.endpoint;
var securityData = $scope.formValues.SecurityFormData;
var TLS = securityData.TLS;
var TLSMode = securityData.TLSMode;
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
var endpointParams = { var endpointParams = {
name: $scope.endpoint.Name, name: endpoint.Name,
URL: $scope.endpoint.URL, URL: endpoint.URL,
PublicURL: $scope.endpoint.PublicURL, PublicURL: endpoint.PublicURL,
TLS: $scope.endpoint.TLS, TLS: TLS,
TLSCACert: $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null, TLSSkipVerify: TLSSkipVerify,
TLSCert: $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null, TLSSkipClientVerify: TLSSkipClientVerify,
TLSKey: $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null, TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert,
TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert,
TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey,
type: $scope.endpointType type: $scope.endpointType
}; };
EndpointService.updateEndpoint(ID, endpointParams) $('updateResourceSpinner').show();
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
.then(function success(data) { .then(function success(data) {
Notifications.success('Endpoint updated', $scope.endpoint.Name); Notifications.success('Endpoint updated', $scope.endpoint.Name);
$state.go('endpoints'); $state.go('endpoints');
}, function error(err) { }, function error(err) {
$scope.state.error = err.msg; Notifications.error('Failure', err, 'Unable to update endpoint');
}, function update(evt) { }, function update(evt) {
if (evt.upload) { if (evt.upload) {
$scope.state.uploadInProgress = evt.upload; $scope.state.uploadInProgress = evt.upload;
@ -43,25 +49,27 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications)
}); });
}; };
function getEndpoint(endpointID) { function initView() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
EndpointService.endpoint($stateParams.id).then(function success(data) { EndpointService.endpoint($stateParams.id)
$('#loadingViewSpinner').hide(); .then(function success(data) {
$scope.endpoint = data; var endpoint = data;
if (data.URL.indexOf('unix://') === 0) { endpoint.URL = $filter('stripprotocol')(endpoint.URL);
$scope.endpoint = endpoint;
if (endpoint.URL.indexOf('unix://') === 0) {
$scope.endpointType = 'local'; $scope.endpointType = 'local';
} else { } else {
$scope.endpointType = 'remote'; $scope.endpointType = 'remote';
} }
$scope.endpoint.URL = $filter('stripprotocol')(data.URL); })
$scope.formValues.TLSCACert = data.TLSCACert; .catch(function error(err) {
$scope.formValues.TLSCert = data.TLSCert;
$scope.formValues.TLSKey = data.TLSKey;
}, function error(err) {
$('#loadingViewSpinner').hide();
Notifications.error('Failure', err, 'Unable to retrieve endpoint details'); Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
}); });
} }
getEndpoint($stateParams.id); initView();
}]); }]);

View File

@ -1,153 +0,0 @@
<div class="page-wrapper">
<!-- simple box -->
<div class="container simple-box">
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
<!-- 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" style="text-align: center;">
<h4>Connect Portainer to a Docker engine or Swarm cluster endpoint</h4>
</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" ng-click="resetErrorMessage()">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" ng-click="resetErrorMessage()">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">
<i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 5px;"></i>
<span class="small text-primary">This feature is not yet available for native Docker Windows containers.</span>
<div class="small text-primary">On Linux and when using Docker for Mac or Docker for Windows or Docker Toolbox, ensure that you have started Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code></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>
<span class="pull-right">
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
<button type="submit" class="btn btn-primary" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
</span>
</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-4 col-lg-3 control-label text-left">Name</label>
<div class="col-sm-8 col-lg-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-4 col-lg-3 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-8 col-lg-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">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLS"><i></i>
</label>
</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>
<span class="pull-right">
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
<button type="submit" class="btn btn-primary" 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>
</span>
</div>
</div>
<!-- !connect button -->
</div>
<!-- !remote-endpoint -->
</form>
<!-- !init-endpoint form -->
</div>
</div>
<!-- !init-endpoint panel -->
</div>
</div>
<!-- !simple box -->
</div>

View File

@ -1,91 +0,0 @@
angular.module('endpointInit', [])
.controller('EndpointInitController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications) {
$scope.state = {
error: '',
uploadInProgress: false
};
$scope.formValues = {
endpointType: 'remote',
Name: '',
URL: '',
TLS: false,
TLSCACert: null,
TLSCert: null,
TLSKey: null
};
if (!_.isEmpty($scope.applicationState.endpoint)) {
$state.go('dashboard');
}
$scope.resetErrorMessage = function() {
$scope.state.error = '';
};
function showErrorMessage(message) {
$scope.state.uploadInProgress = false;
$scope.state.error = message;
}
function updateEndpointState(endpointID) {
EndpointProvider.setEndpointID(endpointID);
StateManager.updateEndpointState(false)
.then(function success(data) {
$state.go('dashboard');
})
.catch(function error(err) {
EndpointService.deleteEndpoint(endpointID)
.then(function success() {
showErrorMessage('Unable to connect to the Docker endpoint');
});
});
}
$scope.createLocalEndpoint = function() {
$('#initEndpointSpinner').show();
$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) {
var endpointID = data.Id;
updateEndpointState(data.Id);
}, function error() {
$scope.state.error = 'Unable to create endpoint';
})
.finally(function final() {
$('#initEndpointSpinner').hide();
});
};
$scope.createRemoteEndpoint = function() {
$('#initEndpointSpinner').show();
$scope.state.error = '';
var name = $scope.formValues.Name;
var URL = $scope.formValues.URL;
var PublicURL = URL.split(':')[0];
var TLS = $scope.formValues.TLS;
var TLSCAFile = $scope.formValues.TLSCACert;
var TLSCertFile = $scope.formValues.TLSCert;
var TLSKeyFile = $scope.formValues.TLSKey;
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success(data) {
var endpointID = data.Id;
updateEndpointState(endpointID);
}, function error(err) {
showErrorMessage(err.msg);
}, function update(evt) {
if (evt.upload) {
$scope.state.uploadInProgress = evt.upload;
}
})
.finally(function final() {
$('#initEndpointSpinner').hide();
});
};
}]);

View File

@ -60,75 +60,21 @@
</div> </div>
</div> </div>
<!-- !endpoint-public-url-input --> <!-- !endpoint-public-url-input -->
<!-- tls-checkbox --> <!-- endpoint-security -->
<por-endpoint-security form-data="formValues.SecurityFormData"></por-endpoint-security>
<!-- !endpoint-security -->
<!-- actions -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<label for="tls" class="control-label text-left"> <button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && ((formValues.TLSVerify && !formValues.TLSCACert) || (formValues.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="addEndpoint()"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</button>
TLS <i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLS"><i></i>
</label>
</div>
</div> </div>
<!-- !tls-checkbox --> </div>
<!-- tls-certs --> <!-- !actions -->
<div ng-if="formValues.TLS"> </form>
<!-- ca-input --> </rd-widget-body>
<div class="form-group"> </rd-widget>
<label class="col-sm-2 control-label text-left">TLS CA certificate</label> </div>
<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>
<div class="row"> <div class="row">
@ -191,7 +137,7 @@
<span ng-if="applicationState.application.authentication"> <span ng-if="applicationState.application.authentication">
<a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a> <a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
</span> </span>
</td> </td>
</tr> </tr>
<tr ng-if="!endpoints"> <tr ng-if="!endpoints">
<td colspan="5" class="text-center text-muted">Loading...</td> <td colspan="5" class="text-center text-muted">Loading...</td>

View File

@ -2,7 +2,6 @@ angular.module('endpoints', [])
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination', .controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination',
function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagination) { function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagination) {
$scope.state = { $scope.state = {
error: '',
uploadInProgress: false, uploadInProgress: false,
selectedItemCount: 0, selectedItemCount: 0,
pagination_count: Pagination.getPaginationCount('endpoints') pagination_count: Pagination.getPaginationCount('endpoints')
@ -14,10 +13,7 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
Name: '', Name: '',
URL: '', URL: '',
PublicURL: '', PublicURL: '',
TLS: false, SecurityFormData: new EndpointSecurityFormData()
TLSCACert: null,
TLSCert: null,
TLSKey: null
}; };
$scope.order = function(sortType) { $scope.order = function(sortType) {
@ -47,23 +43,28 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
}; };
$scope.addEndpoint = function() { $scope.addEndpoint = function() {
$scope.state.error = '';
var name = $scope.formValues.Name; var name = $scope.formValues.Name;
var URL = $scope.formValues.URL; var URL = $scope.formValues.URL;
var PublicURL = $scope.formValues.PublicURL; var PublicURL = $scope.formValues.PublicURL;
if (PublicURL === '') { if (PublicURL === '') {
PublicURL = URL.split(':')[0]; PublicURL = URL.split(':')[0];
} }
var TLS = $scope.formValues.TLS;
var TLSCAFile = $scope.formValues.TLSCACert; var securityData = $scope.formValues.SecurityFormData;
var TLSCertFile = $scope.formValues.TLSCert; var TLS = securityData.TLS;
var TLSKeyFile = $scope.formValues.TLSKey; var TLSMode = securityData.TLSMode;
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, false).then(function success(data) { var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
var TLSCAFile = TLSSkipVerify ? null : securityData.TLSCACert;
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile).then(function success(data) {
Notifications.success('Endpoint created', name); Notifications.success('Endpoint created', name);
$state.reload(); $state.reload();
}, function error(err) { }, function error(err) {
$scope.state.uploadInProgress = false; $scope.state.uploadInProgress = false;
$scope.state.error = err.msg; Notifications.error('Failure', err, 'Unable to create endpoint');
}, function update(evt) { }, function update(evt) {
if (evt.upload) { if (evt.upload) {
$scope.state.uploadInProgress = evt.upload; $scope.state.uploadInProgress = evt.upload;

View File

@ -1,6 +1,6 @@
<rd-header> <rd-header>
<rd-header-title title="Engine overview"> <rd-header-title title="Engine overview">
<a data-toggle="tooltip" title="Refresh" ui-sref="docker" ui-sref-opts="{reload: true}"> <a data-toggle="tooltip" title="Refresh" ui-sref="engine" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i> <i class="fa fa-refresh" aria-hidden="true"></i>
</a> </a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i> <i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>

View File

@ -1,9 +1,7 @@
angular.module('docker', []) angular.module('engine', [])
.controller('DockerController', ['$q', '$scope', 'SystemService', 'Notifications', .controller('EngineController', ['$q', '$scope', 'SystemService', 'Notifications',
function ($q, $scope, SystemService, Notifications) { function ($q, $scope, SystemService, Notifications) {
$scope.info = {};
$scope.version = {};
function initView() { function initView() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
$q.all({ $q.all({
@ -15,6 +13,8 @@ function ($q, $scope, SystemService, Notifications) {
$scope.info = data.info; $scope.info = data.info;
}) })
.catch(function error(err) { .catch(function error(err) {
$scope.info = {};
$scope.version = {};
Notifications.error('Failure', err, 'Unable to retrieve engine details'); Notifications.error('Failure', err, 'Unable to retrieve engine details');
}) })
.finally(function final() { .finally(function final() {

View File

@ -70,7 +70,7 @@
<div class="pull-right"> <div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" /> <input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
</div> </div>
<span class="btn-group btn-group-sm pull-right" style="margin-right: 20px;" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider !== 'VMWARE_VIC'"> <span class="btn-group btn-group-sm pull-right" style="margin-right: 20px;">
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="undefined"> <label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="undefined">
All All
</label> </label>
@ -121,12 +121,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="image in (state.filteredImages = (images | filter:{ Containers: state.containersCountFilter } | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="image in (state.filteredImages = (images | filter:{ ContainerCount: state.containersCountFilter } | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td> <td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
<td> <td>
<a class="monospaced" ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a> <a class="monospaced" ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a>
<span style="margin-left: 10px;" class="label label-warning image-tag" <span style="margin-left: 10px;" class="label label-warning image-tag"
ng-if="::image.Containers === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider !== 'VMWARE_VIC'"> ng-if="::image.ContainerCount === 0">
Unused Unused
</span> </span>
</td> </td>

View File

@ -95,7 +95,7 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService)
$('#loadImagesSpinner').show(); $('#loadImagesSpinner').show();
var endpointProvider = $scope.applicationState.endpoint.mode.provider; var endpointProvider = $scope.applicationState.endpoint.mode.provider;
var apiVersion = $scope.applicationState.endpoint.apiVersion; var apiVersion = $scope.applicationState.endpoint.apiVersion;
ImageService.images(apiVersion >= 1.25 && endpointProvider !== 'DOCKER_SWARM' && endpointProvider !== 'VMWARE_VIC') ImageService.images(true)
.then(function success(data) { .then(function success(data) {
$scope.images = data; $scope.images = data;
}) })

View File

@ -0,0 +1,80 @@
<div class="page-wrapper">
<!-- simple box -->
<div class="container simple-box">
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
<!-- 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 password panel -->
<div class="panel panel-default">
<div class="panel-body">
<!-- init password form -->
<form class="simple-box-form form-horizontal">
<!-- note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
Please create the initial administrator user.
</span>
</div>
</div>
<!-- !note -->
<!-- username-input -->
<div class="form-group">
<label for="username" class="col-sm-4 control-label text-left">
Username
</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="username" ng-model="formValues.Username" placeholder="e.g. admin">
</div>
</div>
<!-- !username-input -->
<!-- new-password-input -->
<div class="form-group">
<label for="password" class="col-sm-4 control-label text-left">Password</label>
<div class="col-sm-8">
<input type="password" class="form-control" ng-model="formValues.Password" id="password" auto-focus>
</div>
</div>
<!-- !new-password-input -->
<!-- confirm-password-input -->
<div class="form-group">
<label for="confirm_password" class="col-sm-4 control-label text-left">Confirm password</label>
<div class="col-sm-8">
<div class="input-group">
<input type="password" class="form-control" ng-model="formValues.ConfirmPassword" id="confirm_password">
<span class="input-group-addon"><i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.Password !== '' && formValues.Password === formValues.ConfirmPassword]" aria-hidden="true"></i></span>
</div>
</div>
</div>
<!-- !confirm-password-input -->
<!-- note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.Password.length >= 8]" aria-hidden="true"></i>
The password must be at least 8 characters long
</span>
</div>
</div>
<!-- !note -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="formValues.Password.length < 8 || formValues.Password !== formValues.ConfirmPassword" ng-click="createAdminUser()"><i class="fa fa-user-plus" aria-hidden="true"></i> Create user</button>
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</form>
<!-- !init password form -->
</div>
</div>
<!-- !init password panel -->
</div>
</div>
<!-- !simple box -->
</div>

View File

@ -0,0 +1,48 @@
angular.module('initAdmin', [])
.controller('InitAdminController', ['$scope', '$state', '$sanitize', 'Notifications', 'Authentication', 'StateManager', 'UserService', 'EndpointService', 'EndpointProvider',
function ($scope, $state, $sanitize, Notifications, Authentication, StateManager, UserService, EndpointService, EndpointProvider) {
$scope.logo = StateManager.getState().application.logo;
$scope.formValues = {
Username: 'admin',
Password: '',
ConfirmPassword: ''
};
$scope.createAdminUser = function() {
$('#createResourceSpinner').show();
var username = $sanitize($scope.formValues.Username);
var password = $sanitize($scope.formValues.Password);
UserService.initAdministrator(username, password)
.then(function success() {
return Authentication.login(username, password);
})
.then(function success() {
return EndpointService.endpoints();
})
.then(function success(data) {
if (data.length === 0) {
$state.go('init.endpoint');
} else {
var endpointID = data[0].Id;
EndpointProvider.setEndpointID(endpointID);
StateManager.updateEndpointState(false)
.then(function success() {
$state.go('dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to Docker environment');
});
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create administrator user');
})
.finally(function final() {
$('#createResourceSpinner').hide();
});
};
}]);

View File

@ -0,0 +1,202 @@
<div class="page-wrapper">
<!-- simple box -->
<div class="container simple-box">
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
<!-- 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="simple-box-form form-horizontal">
<!-- note -->
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted">
Connect Portainer to the Docker environment you want to manage.
</span>
</div>
</div>
<!-- !note -->
<!-- endpoint-type -->
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="local_endpoint" ng-model="formValues.EndpointType" value="local">
<label for="local_endpoint">
<div class="boxselector_header">
<i class="fa fa-bolt" aria-hidden="true" style="margin-right: 2px;"></i>
Local
</div>
<p>Manage the Docker environment where Portainer is running</p>
</label>
</div>
<div>
<input type="radio" id="remote_endpoint" ng-model="formValues.EndpointType" value="remote">
<label for="remote_endpoint">
<div class="boxselector_header">
<i class="fa fa-plug" aria-hidden="true" style="margin-right: 2px;"></i>
Remote
</div>
<p>Manage a remote Docker environment</p>
</label>
</div>
</div>
</div>
<!-- !endpoint-type -->
<!-- local-endpoint -->
<div ng-if="formValues.EndpointType === 'local'">
<div class="form-group">
<div class="col-sm-12">
<span class="small">
<p class="text-muted">
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This feature is not yet available for <u>native Docker Windows containers</u>.
</p>
<p class="text-primary">
Please ensure that you have started the Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code> in order to connect to the local Docker environment.
</p>
</span>
</div>
</div>
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-click="createLocalEndpoint()"><i class="fa fa-bolt" aria-hidden="true"></i> Connect</button>
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</div>
<!-- !local-endpoint -->
<!-- remote-endpoint -->
<div ng-if="formValues.EndpointType === 'remote'">
<!-- name-input -->
<div class="form-group">
<label for="endpoint_name" class="col-sm-4 col-lg-3 control-label text-left">Name</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="endpoint_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-4 col-lg-3 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-8 col-lg-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">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLS"><i></i>
</label>
</div>
</div>
<!-- !tls-checkbox -->
<!-- tls-options -->
<div ng-if="formValues.TLS">
<!-- skip-server-verification -->
<div class="form-group">
<div class="col-sm-10">
<label for="tls_verify" class="control-label text-left">
Skip server verification
<portainer-tooltip position="bottom" message="Enable this option if you need to authenticate server based on given CA."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLSSkipVerify"><i></i>
</label>
</div>
</div>
<!-- !skip-server-verification -->
<!-- skip-client-verification -->
<div class="form-group">
<div class="col-sm-10">
<label for="tls_client_cert" class="control-label text-left">
Skip client verification
<portainer-tooltip position="bottom" message="Enable this option if you need to authenticate with a client certificate."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLSSKipClientVerify"><i></i>
</label>
</div>
</div>
<!-- !skip-client-verification -->
<div class="col-sm-12 form-section-title" ng-if="!formValues.TLSSkipVerify || !formValues.TLSSKipClientVerify">
Required TLS files
</div>
<!-- ca-input -->
<div class="form-group" ng-if="!formValues.TLSSkipVerify">
<label class="col-sm-4 col-lg-3 control-label text-left">TLS CA certificate</label>
<div class="col-sm-8 col-lg-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 -->
<div ng-if="!formValues.TLSSKipClientVerify">
<!-- cert-input -->
<div class="form-group">
<label for="tls_cert" class="col-sm-4 col-lg-3 control-label text-left">TLS certificate</label>
<div class="col-sm-8 col-lg-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-4 col-lg-3 control-label text-left">TLS key</label>
<div class="col-sm-8 col-lg-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>
</div>
<!-- !tls-options -->
<!-- actions -->
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && ((formValues.TLSVerify && !formValues.TLSCACert) || (!formValues.TLSSKipClientVerify && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
<!-- !actions -->
</div>
<!-- !remote-endpoint -->
</form>
<!-- !init-endpoint form -->
</div>
</div>
<!-- !init-endpoint panel -->
</div>
</div>
<!-- !simple box -->
</div>

View File

@ -0,0 +1,81 @@
angular.module('initEndpoint', [])
.controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications) {
if (!_.isEmpty($scope.applicationState.endpoint)) {
$state.go('dashboard');
}
$scope.logo = StateManager.getState().application.logo;
$scope.state = {
uploadInProgress: false
};
$scope.formValues = {
EndpointType: 'remote',
Name: '',
URL: '',
TLS: false,
TLSSkipVerify: false,
TLSSKipClientVerify: false,
TLSCACert: null,
TLSCert: null,
TLSKey: null
};
$scope.createLocalEndpoint = function() {
$('#createResourceSpinner').show();
var name = 'local';
var URL = 'unix:///var/run/docker.sock';
var endpointID = 1;
EndpointService.createLocalEndpoint(name, URL, false, true)
.then(function success(data) {
endpointID = data.Id;
EndpointProvider.setEndpointID(endpointID);
return StateManager.updateEndpointState(false);
})
.then(function success(data) {
$state.go('dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
EndpointService.deleteEndpoint(endpointID);
})
.finally(function final() {
$('#createResourceSpinner').hide();
});
};
$scope.createRemoteEndpoint = function() {
$('#createResourceSpinner').show();
var name = $scope.formValues.Name;
var URL = $scope.formValues.URL;
var PublicURL = URL.split(':')[0];
var TLS = $scope.formValues.TLS;
var TLSSkipVerify = TLS && $scope.formValues.TLSSkipVerify;
var TLSSKipClientVerify = TLS && $scope.formValues.TLSSKipClientVerify;
var TLSCAFile = TLSSkipVerify ? null : $scope.formValues.TLSCACert;
var TLSCertFile = TLSSKipClientVerify ? null : $scope.formValues.TLSCert;
var TLSKeyFile = TLSSKipClientVerify ? null : $scope.formValues.TLSKey;
var endpointID = 1;
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success(data) {
endpointID = data.Id;
EndpointProvider.setEndpointID(endpointID);
return StateManager.updateEndpointState(false);
})
.then(function success(data) {
$state.go('dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
EndpointService.deleteEndpoint(endpointID);
})
.finally(function final() {
$('#createResourceSpinner').hide();
});
};
}]);

View File

@ -48,6 +48,15 @@
</div> </div>
</div> </div>
<!-- access-control-panel -->
<por-access-control-panel
ng-if="network && applicationState.application.authentication"
resource-id="network.Id"
resource-control="network.ResourceControl"
resource-type="'network'">
</por-access-control-panel>
<!-- !access-control-panel -->
<div class="row" ng-if="!(network.Options | emptyobject)"> <div class="row" ng-if="!(network.Options | emptyobject)">
<div class="col-lg-12 col-md-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget> <rd-widget>

View File

@ -1,6 +1,6 @@
angular.module('network', []) angular.module('network', [])
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'Container', 'ContainerHelper', 'Notifications', .controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'NetworkService', 'Container', 'ContainerHelper', 'Notifications',
function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHelper, Notifications) { function ($scope, $state, $stateParams, $filter, Network, NetworkService, Container, ContainerHelper, Notifications) {
$scope.removeNetwork = function removeNetwork(networkId) { $scope.removeNetwork = function removeNetwork(networkId) {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
@ -82,7 +82,7 @@ function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHe
function initView() { function initView() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
Network.get({id: $stateParams.id}).$promise NetworkService.network($stateParams.id)
.then(function success(data) { .then(function success(data) {
$scope.network = data; $scope.network = data;
var endpointProvider = $scope.applicationState.endpoint.mode.provider; var endpointProvider = $scope.applicationState.endpoint.mode.provider;

View File

@ -8,46 +8,6 @@
<rd-header-content>Networks</rd-header-content> <rd-header-content>Networks</rd-header-content>
</rd-header> </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 network">
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<!-- name-input -->
<div class="form-group">
<label for="network_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
</div>
</div>
<!-- !name-input -->
<!-- tag-note -->
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
</div>
</div>
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
<div class="col-sm-12">
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
</div>
</div>
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
<button type="button" class="btn btn-primary btn-sm" ui-sref="actions.create.network">Advanced settings...</button>
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget> <rd-widget>
@ -66,6 +26,7 @@
<rd-widget-taskbar classes="col-lg-12"> <rd-widget-taskbar classes="col-lg-12">
<div class="pull-left"> <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> <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>
<a class="btn btn-primary" type="button" ui-sref="actions.create.network"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add network</a>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" /> <input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
@ -80,54 +41,61 @@
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" /> <input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th> </th>
<th> <th>
<a ui-sref="networks" ng-click="order('Name')"> <a ng-click="order('Name')">
Name Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th> <th>
<a ui-sref="networks" ng-click="order('Id')"> <a ng-click="order('Id')">
Id Id
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th> <th>
<a ui-sref="networks" ng-click="order('Scope')"> <a ng-click="order('Scope')">
Scope Scope
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th> <th>
<a ui-sref="networks" ng-click="order('Driver')"> <a ng-click="order('Driver')">
Driver Driver
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th> <th>
<a ui-sref="networks" ng-click="order('IPAM.Driver')"> <a ng-click="order('IPAM.Driver')">
IPAM Driver IPAM Driver
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th> <th>
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')"> <a ng-click="order('IPAM.Config[0].Subnet')">
IPAM Subnet IPAM Subnet
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th> <th>
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')"> <a ng-click="order('IPAM.Config[0].Gateway')">
IPAM Gateway IPAM Gateway
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -140,12 +108,18 @@
<td>{{ network.IPAM.Driver }}</td> <td>{{ network.IPAM.Driver }}</td>
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td> <td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td> <td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="network.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ network.ResourceControl.Ownership ? network.ResourceControl.Ownership : network.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr> </tr>
<tr ng-if="!networks"> <tr ng-if="!networks">
<td colspan="8" class="text-center text-muted">Loading...</td> <td colspan="9" class="text-center text-muted">Loading...</td>
</tr> </tr>
<tr ng-if="networks.length == 0"> <tr ng-if="networks.length == 0">
<td colspan="8" class="text-center text-muted">No networks available.</td> <td colspan="9" class="text-center text-muted">No networks available.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -1,51 +1,17 @@
angular.module('networks', []) angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'Notifications', 'Pagination', .controller('NetworksController', ['$scope', '$state', 'Network', 'NetworkService', 'Notifications', 'Pagination',
function ($scope, $state, Network, Notifications, Pagination) { function ($scope, $state, Network, NetworkService, Notifications, Pagination) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('networks'); $scope.state.pagination_count = Pagination.getPaginationCount('networks');
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
$scope.state.advancedSettings = false; $scope.state.advancedSettings = false;
$scope.sortType = 'Name'; $scope.sortType = 'Name';
$scope.sortReverse = false; $scope.sortReverse = false;
$scope.config = {
Name: ''
};
$scope.changePaginationCount = function() { $scope.changePaginationCount = function() {
Pagination.setPaginationCount('networks', $scope.state.pagination_count); Pagination.setPaginationCount('networks', $scope.state.pagination_count);
}; };
function prepareNetworkConfiguration() {
var config = angular.copy($scope.config);
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
config.Driver = 'overlay';
// Force IPAM Driver to 'default', should not be required.
// See: https://github.com/docker/docker/issues/25735
config.IPAM = {
Driver: 'default'
};
}
return config;
}
$scope.createNetwork = function() {
$('#createNetworkSpinner').show();
var config = prepareNetworkConfiguration();
Network.create(config, function (d) {
if (d.message) {
$('#createNetworkSpinner').hide();
Notifications.error('Unable to create network', {}, d.message);
} else {
Notifications.success('Network created', d.Id);
$('#createNetworkSpinner').hide();
$state.reload();
}
}, function (e) {
$('#createNetworkSpinner').hide();
Notifications.error('Failure', e, 'Unable to create network');
});
};
$scope.order = function(sortType) { $scope.order = function(sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false; $scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType; $scope.sortType = sortType;
@ -99,13 +65,17 @@ function ($scope, $state, Network, Notifications, Pagination) {
function initView() { function initView() {
$('#loadNetworksSpinner').show(); $('#loadNetworksSpinner').show();
Network.query({}, function (d) {
$scope.networks = d; NetworkService.networks(true, true, true, true)
$('#loadNetworksSpinner').hide(); .then(function success(data) {
}, function (e) { $scope.networks = data;
$('#loadNetworksSpinner').hide(); })
Notifications.error('Failure', e, 'Unable to retrieve networks'); .catch(function error(err) {
$scope.networks = []; $scope.networks = [];
Notifications.error('Failure', err, 'Unable to retrieve networks');
})
.finally(function final() {
$('#loadNetworksSpinner').hide();
}); });
} }

View File

@ -53,3 +53,12 @@
</rd-widget> </rd-widget>
</div> </div>
</div> </div>
<!-- access-control-panel -->
<por-access-control-panel
ng-if="secret && applicationState.application.authentication"
resource-id="secret.Id"
resource-control="secret.ResourceControl"
resource-type="'secret'">
</por-access-control-panel>
<!-- !access-control-panel -->

View File

@ -30,31 +30,44 @@
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" /> <input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th> </th>
<th> <th>
<a ui-sref="secrets" ng-click="order('Name')"> <a ng-click="order('Name')">
Name Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th> <th>
<a ui-sref="secrets" ng-click="order('CreatedAt')"> <a ng-click="order('CreatedAt')">
Created at Created at
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span> <span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span> <span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a> </a>
</th> </th>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="secret in (state.filteredSecrets = ( secrets | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))"> <tr dir-paginate="secret in (state.filteredSecrets = ( secrets | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<td><input type="checkbox" ng-model="secret.Checked" ng-change="selectItem(secret)"/></td> <td><input type="checkbox" ng-model="secret.Checked" ng-change="selectItem(secret)"/></td>
<td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td> <td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
<td>{{ secret.CreatedAt | getisodate }}</td> <td>{{ secret.CreatedAt | getisodate }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="secret.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ secret.ResourceControl.Ownership ? secret.ResourceControl.Ownership : secret.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr> </tr>
<tr ng-if="!secrets"> <tr ng-if="!secrets">
<td colspan="3" class="text-center text-muted">Loading...</td> <td colspan="4" class="text-center text-muted">Loading...</td>
</tr> </tr>
<tr ng-if="secrets.length == 0"> <tr ng-if="secrets.length == 0">
<td colspan="3" class="text-center text-muted">No secrets available.</td> <td colspan="4" class="text-center text-muted">No secrets available.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -6,31 +6,77 @@
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>
<td>CPU limits</td> <td style="vertical-align : middle;">
<td ng-if="service.LimitNanoCPUs"> Memory reservation (MB)
{{ service.LimitNanoCPUs / 1000000000 }}
</td> </td>
<td ng-if="!service.LimitNanoCPUs">None</td> <td>
</tr> <input class="input-sm" type="number" step="0.125" min="0" ng-model="service.ReservationMemoryBytes" ng-change="updateServiceAttribute(service, 'ReservationMemoryBytes')" ng-disabled="isUpdating"/>
<tr> </td>
<td>Memory limits</td> <td style="vertical-align : middle;">
<td ng-if="service.LimitMemoryBytes">{{service.LimitMemoryBytes|humansize}}</td> <p class="small text-muted">
<td ng-if="!service.LimitMemoryBytes">None</td> Minimum memory available on a node to run a task (set to 0 for unlimited)
</tr> </p>
<tr>
<td>CPU reservation</td>
<td ng-if="service.ReservationNanoCPUs">
{{service.ReservationNanoCPUs / 1000000000}}
</td> </td>
<td ng-if="!service.ReservationNanoCPUs">None</td>
</tr> </tr>
<tr> <tr>
<td>Memory reservation</td> <td style="vertical-align : middle;">
<td ng-if="service.ReservationMemoryBytes">{{service.ReservationMemoryBytes|humansize}}</td> Memory limit (MB)
<td ng-if="!service.ReservationMemoryBytes">None</td> </td>
<td>
<input class="input-sm" type="number" step="0.125" min="0" ng-model="service.LimitMemoryBytes" ng-change="updateServiceAttribute(service, 'LimitMemoryBytes')" ng-disabled="isUpdating"/>
</td>
<td style="vertical-align : middle;">
<p class="small text-muted">
Maximum memory usage per task (set to 0 for unlimited)
</p>
</td>
</tr>
<tr>
<td style="vertical-align : middle;">
<div>
CPU reservation
</div>
</td>
<td>
<por-slider model="service.ReservationNanoCPUs" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="service && state.sliderMaxCpu" on-change="updateServiceAttribute(service, 'ReservationNanoCPUs')"></por-slider>
</td>
<td style="vertical-align : middle;">
<p class="small text-muted">
Minimum CPU available on a node to run a task
</p>
</td>
</tr>
<tr>
<td style="vertical-align : middle;">
<div>
CPU limit
</div>
</td>
<td>
<por-slider model="service.LimitNanoCPUs" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="service && state.sliderMaxCpu" on-change="updateServiceAttribute(service, 'LimitNanoCPUs')"></por-slider>
</td>
<td style="vertical-align : middle;">
<p class="small text-muted">
Maximum CPU usage per task
</p>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</rd-widget-body> </rd-widget-body>
<rd-widget-footer>
<div class="btn-toolbar" role="toolbar">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['LimitNanoCPUs', 'LimitMemoryBytes', 'ReservationNanoCPUs', 'ReservationMemoryBytes'])" ng-click="updateService(service)">Apply changes</button>
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a ng-click="cancelChanges(service, ['LimitNanoCPUs', 'LimitMemoryBytes', 'ReservationNanoCPUs', 'ReservationMemoryBytes'])">Reset changes</a></li>
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
</ul>
</div>
</div>
</rd-widget-footer>
</rd-widget> </rd-widget>
</div> </div>

View File

@ -204,14 +204,19 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints); config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints);
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences); config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences);
// Round memory values to 0.125 and convert MB to B
var memoryLimit = (Math.round(service.LimitMemoryBytes * 8) / 8).toFixed(3);
memoryLimit *= 1024 * 1024;
var memoryReservation = (Math.round(service.ReservationMemoryBytes * 8) / 8).toFixed(3);
memoryReservation *= 1024 * 1024;
config.TaskTemplate.Resources = { config.TaskTemplate.Resources = {
Limits: { Limits: {
NanoCPUs: service.LimitNanoCPUs, NanoCPUs: service.LimitNanoCPUs * 1000000000,
MemoryBytes: service.LimitMemoryBytes MemoryBytes: memoryLimit
}, },
Reservations: { Reservations: {
NanoCPUs: service.ReservationNanoCPUs, NanoCPUs: service.ReservationNanoCPUs * 1000000000,
MemoryBytes: service.ReservationMemoryBytes MemoryBytes: memoryReservation
} }
}; };
@ -244,7 +249,11 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
Service.update({ id: service.Id, version: service.Version }, config, function (data) { Service.update({ id: service.Id, version: service.Version }, config, function (data) {
$('#loadingViewSpinner').hide(); $('#loadingViewSpinner').hide();
Notifications.success('Service successfully updated', 'Service updated'); if (data.message && data.message.match(/^rpc error:/)) {
Notifications.error(data.message, 'Error');
} else {
Notifications.success('Service successfully updated', 'Service updated');
}
$scope.cancelChanges({}); $scope.cancelChanges({});
initView(); initView();
}, function (e) { }, function (e) {
@ -288,6 +297,13 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences); service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences);
} }
function transformResources(service) {
service.LimitNanoCPUs = service.LimitNanoCPUs / 1000000000 || 0;
service.ReservationNanoCPUs = service.ReservationNanoCPUs / 1000000000 || 0;
service.LimitMemoryBytes = service.LimitMemoryBytes / 1024 / 1024 || 0;
service.ReservationMemoryBytes = service.ReservationMemoryBytes / 1024 / 1024 || 0;
}
function initView() { function initView() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
var apiVersion = $scope.applicationState.endpoint.apiVersion; var apiVersion = $scope.applicationState.endpoint.apiVersion;
@ -299,6 +315,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
$scope.lastVersion = service.Version; $scope.lastVersion = service.Version;
} }
transformResources(service);
translateServiceArrays(service); translateServiceArrays(service);
$scope.service = service; $scope.service = service;
originalService = angular.copy(service); originalService = angular.copy(service);
@ -314,6 +331,19 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
$scope.nodes = data.nodes; $scope.nodes = data.nodes;
$scope.secrets = data.secrets; $scope.secrets = data.secrets;
// Set max cpu value
var maxCpus = 0;
for (var n in data.nodes) {
if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) {
maxCpus = data.nodes[n].CPUs;
}
}
if (maxCpus > 0) {
$scope.state.sliderMaxCpu = maxCpus / 1000000000;
} else {
$scope.state.sliderMaxCpu = 32;
}
$timeout(function() { $timeout(function() {
$anchorScroll(); $anchorScroll();
}); });

View File

@ -43,14 +43,14 @@
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"> <li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
<a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a> <a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"> <li class="sidebar-list" ng-if="(applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC') && isAdmin">
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a> <a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')"> <li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')">
<a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a> <a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"> <li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
<a ui-sref="docker" ui-sref-active="active">Docker <span class="menu-icon fa fa-th"></span></a> <a ui-sref="engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th"></span></a>
</li> </li>
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader"> <li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
<span>Portainer settings</span> <span>Portainer settings</span>

View File

@ -1,94 +0,0 @@
<rd-header>
<rd-header-title title="Container stats"></rd-header-title>
<rd-header-content>
<a ui-sref="containers">Containers</a> &gt; <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> &gt; Stats
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-body>
<div class="widget-icon grey pull-left">
<i class="fa fa-server"></i>
</div>
<div class="title">{{ container.Name|trimcontainername }}</div>
<div class="comment">
Name
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
<rd-widget-body>
<canvas id="cpu-stats-chart" width="770" height="230"></canvas>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
<rd-widget-body>
<canvas id="memory-stats-chart" width="770" height="230"></canvas>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<rd-widget>
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
<rd-widget-body>
<canvas id="network-stats-chart" width="770" height="230"></canvas>
<div class="comment">
<div id="network-legend" style="margin-bottom: 20px;"></div>
</div>
</rd-widget-body>
</rd-widget>
</div>
<div class="col-lg-6" ng-if="applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
<rd-widget>
<rd-widget-header icon="fa-tasks" title="Processes">
<div class="pull-right">
Items per page:
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<table class="table table-striped">
<thead>
<tr>
<th ng-repeat="title in containerTop.Titles">
<a ui-sref="stats({id: container.Id})" ng-click="order(title)">
{{title}}
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="processInfos in state.filteredProcesses = (containerTop.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
<td ng-repeat="processInfo in processInfos track by $index">{{processInfo}}</td>
</tr>
</tbody>
</table>
<div ng-if="containerTop.Processes" class="pagination-controls">
<dir-pagination-controls></dir-pagination-controls>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -1,220 +0,0 @@
angular.module('stats', [])
.controller('StatsController', ['Pagination', '$scope', 'Notifications', '$timeout', 'Container', 'ContainerTop', '$stateParams', 'humansizeFilter', '$sce', '$document',
function (Pagination, $scope, Notifications, $timeout, Container, ContainerTop, $stateParams, humansizeFilter, $sce, $document) {
// TODO: Force scale to 0-100 for cpu, fix charts on dashboard,
// TODO: Force memory scale to 0 - max memory
$scope.ps_args = '';
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
$scope.sortType = 'CMD';
$scope.sortReverse = false;
$scope.order = function (sortType) {
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
$scope.sortType = sortType;
};
$scope.changePaginationCount = function() {
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
};
$scope.getTop = function () {
ContainerTop.get($stateParams.id, {
ps_args: $scope.ps_args
}, function (data) {
$scope.containerTop = data;
});
};
var destroyed = false;
var timeout;
$document.ready(function(){
var cpuLabels = [];
var cpuData = [];
var memoryLabels = [];
var memoryData = [];
var networkLabels = [];
var networkTxData = [];
var networkRxData = [];
for (var i = 0; i < 20; i++) {
cpuLabels.push('');
cpuData.push(0);
memoryLabels.push('');
memoryData.push(0);
networkLabels.push('');
networkTxData.push(0);
networkRxData.push(0);
}
var cpuDataset = { // CPU Usage
fillColor: 'rgba(151,187,205,0.5)',
strokeColor: 'rgba(151,187,205,1)',
pointColor: 'rgba(151,187,205,1)',
pointStrokeColor: '#fff',
data: cpuData
};
var memoryDataset = {
fillColor: 'rgba(151,187,205,0.5)',
strokeColor: 'rgba(151,187,205,1)',
pointColor: 'rgba(151,187,205,1)',
pointStrokeColor: '#fff',
data: memoryData
};
var networkRxDataset = {
label: 'Rx Bytes',
fillColor: 'rgba(151,187,205,0.5)',
strokeColor: 'rgba(151,187,205,1)',
pointColor: 'rgba(151,187,205,1)',
pointStrokeColor: '#fff',
data: networkRxData
};
var networkTxDataset = {
label: 'Tx Bytes',
fillColor: 'rgba(255,180,174,0.5)',
strokeColor: 'rgba(255,180,174,1)',
pointColor: 'rgba(255,180,174,1)',
pointStrokeColor: '#fff',
data: networkTxData
};
var networkLegendData = [
{
//value: '',
color: 'rgba(151,187,205,0.5)',
title: 'Rx Data'
},
{
//value: '',
color: 'rgba(255,180,174,0.5)',
title: 'Tx Data'
}
];
legend($('#network-legend').get(0), networkLegendData);
Chart.defaults.global.animationSteps = 30; // Lower from 60 to ease CPU load.
var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext('2d')).Line({
labels: cpuLabels,
datasets: [cpuDataset]
}, {
responsive: true
});
var memoryChart = new Chart($('#memory-stats-chart').get(0).getContext('2d')).Line({
labels: memoryLabels,
datasets: [memoryDataset]
},
{
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10), 2);
},
responsive: true
//scaleOverride: true,
//scaleSteps: 10,
//scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10),
//scaleStartValue: 0
});
var networkChart = new Chart($('#network-stats-chart').get(0).getContext('2d')).Line({
labels: networkLabels,
datasets: [networkRxDataset, networkTxDataset]
}, {
scaleLabel: function (valueObj) {
return humansizeFilter(parseInt(valueObj.value, 10), 2);
},
responsive: true
});
$scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend());
function updateStats() {
Container.stats({id: $stateParams.id}, function (d) {
var arr = Object.keys(d).map(function (key) {
return d[key];
});
if (arr.join('').indexOf('no such id') !== -1) {
Notifications.error('Unable to retrieve stats', {}, 'Is this container running?');
return;
}
// Update graph with latest data
$scope.data = d;
updateCpuChart(d);
updateMemoryChart(d);
updateNetworkChart(d);
setUpdateStatsTimeout();
}, function () {
Notifications.error('Unable to retrieve stats', {}, 'Is this container running?');
setUpdateStatsTimeout();
});
}
$scope.$on('$destroy', function () {
destroyed = true;
$timeout.cancel(timeout);
});
updateStats();
function updateCpuChart(data) {
cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString());
cpuChart.removeData();
}
function updateMemoryChart(data) {
memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString());
memoryChart.removeData();
}
var lastRxBytes = 0, lastTxBytes = 0;
function updateNetworkChart(data) {
// 1.9+ contains an object of networks, for now we'll just show stats for the first network
// TODO: Show graphs for all networks
if (data.networks) {
$scope.networkName = Object.keys(data.networks)[0];
data.network = data.networks[$scope.networkName];
}
if(data.network) {
var rxBytes = 0, txBytes = 0;
if (lastRxBytes !== 0 || lastTxBytes !== 0) {
// These will be zero on first call, ignore to prevent large graph spike
rxBytes = data.network.rx_bytes - lastRxBytes;
txBytes = data.network.tx_bytes - lastTxBytes;
}
lastRxBytes = data.network.rx_bytes;
lastTxBytes = data.network.tx_bytes;
networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString());
networkChart.removeData();
}
}
function calculateCPUPercent(stats) {
// Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208
var prevCpu = stats.precpu_stats;
var curCpu = stats.cpu_stats;
var cpuPercent = 0.0;
// calculate the change for the cpu usage of the container in between readings
var cpuDelta = curCpu.cpu_usage.total_usage - prevCpu.cpu_usage.total_usage;
// calculate the change for the entire system between readings
var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage;
if (systemDelta > 0.0 && cpuDelta > 0.0) {
cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0;
}
return cpuPercent;
}
function setUpdateStatsTimeout() {
if(!destroyed) {
timeout = $timeout(updateStats, 5000);
}
}
});
Container.get({id: $stateParams.id}, function (d) {
$scope.container = d;
}, function (e) {
Notifications.error('Failure', e, 'Unable to retrieve container info');
});
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
if (endpointProvider !== 'VMWARE_VIC') {
$scope.getTop();
}
}]);

View File

@ -58,6 +58,13 @@
<td>Go version</td> <td>Go version</td>
<td>{{ docker.GoVersion }}</td> <td>{{ docker.GoVersion }}</td>
</tr> </tr>
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
<td colspan="2">
<div class="btn-group" role="group" aria-label="...">
<a class="btn btn-outline-secondary" type="button" ui-sref="swarm.visualizer"><i class="fa fa-object-group space-right" aria-hidden="true"></i>Go to cluster visualizer</a>
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</rd-widget-body> </rd-widget-body>
@ -216,7 +223,10 @@
</thead> </thead>
<tbody> <tbody>
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))"> <tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
<td><a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a></td> <td>
<a ui-sref="node({id: node.Id})" ng-if="isAdmin">{{ node.Hostname }}</a>
<span ng-if="!isAdmin">{{ node.Hostname }}</span>
</td>
<td>{{ node.Role }}</td> <td>{{ node.Role }}</td>
<td>{{ node.CPUs / 1000000000 }}</td> <td>{{ node.CPUs / 1000000000 }}</td>
<td>{{ node.Memory|humansize }}</td> <td>{{ node.Memory|humansize }}</td>

View File

@ -1,6 +1,6 @@
angular.module('swarm', []) angular.module('swarm', [])
.controller('SwarmController', ['$q', '$scope', 'SystemService', 'NodeService', 'Pagination', 'Notifications', .controller('SwarmController', ['$q', '$scope', 'SystemService', 'NodeService', 'Pagination', 'Notifications', 'StateManager', 'Authentication',
function ($q, $scope, SystemService, NodeService, Pagination, Notifications) { function ($q, $scope, SystemService, NodeService, Pagination, Notifications, StateManager, Authentication) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes'); $scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
$scope.sortType = 'Spec.Role'; $scope.sortType = 'Spec.Role';
@ -73,6 +73,13 @@ function ($q, $scope, SystemService, NodeService, Pagination, Notifications) {
function initView() { function initView() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
if (StateManager.getState().application.authentication) {
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true: false;
$scope.isAdmin = isAdmin;
}
var provider = $scope.applicationState.endpoint.mode.provider; var provider = $scope.applicationState.endpoint.mode.provider;
$q.all({ $q.all({
version: SystemService.version(), version: SystemService.version(),

View File

@ -0,0 +1,91 @@
<rd-header>
<rd-header-title title="Swarm visualizer">
<a data-toggle="tooltip" title="Refresh" ui-sref="swarm.visualizer" ui-sref-opts="{reload: true}">
<i class="fa fa-refresh" aria-hidden="true"></i>
</a>
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</rd-header-title>
<rd-header-content>
<a ui-sref="swarm">Swarm</a> &gt; <a ui-sref="swarm.visualizer">Cluster visualizer</a>
</rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Cluster information">
<div class="pull-right">
<button type="button" class="btn btn-sm btn-primary" ng-click="state.ShowInformationPanel = true;" ng-if="!state.ShowInformationPanel">Show</button>
<button type="button" class="btn btn-sm btn-primary" ng-click="state.ShowInformationPanel = false;" ng-if="state.ShowInformationPanel">Hide</button>
</div>
</rd-widget-header>
<rd-widget-body ng-if="state.ShowInformationPanel">
<table class="table">
<tbody>
<tr>
<td>Nodes</td>
<td>{{ nodes.length }}</td>
</tr>
<tr>
<td>Services</td>
<td>{{ services.length }}</td>
</tr>
<tr>
<td>Tasks</td>
<td>{{ tasks.length }}</td>
</tr>
</tbody>
</table>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
Filters
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Only display running tasks
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="state.DisplayOnlyRunningTasks"><i></i>
</label>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row" ng-if="visualizerData">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-object-group" title="Cluster visualizer"></rd-widget-header>
<rd-widget-body>
<div class="visualizer_container">
<div class="node" ng-repeat="node in visualizerData.nodes track by $index">
<div class="node_info">
<div>
<div>
<b>{{ node.Hostname }}</b>
<span class="node_platform">
<i class="fa fa-linux" aria-hidden="true" ng-if="node.PlatformOS === 'linux'"></i>
<i class="fa fa-windows" aria-hidden="true" ng-if="node.PlatformOS === 'windows'"></i>
</span>
</div>
</div>
<div>{{ node.Role }}</div>
</div>
<div class="tasks">
<div class="task task_{{ task.Status.State | visualizerTask }}" ng-repeat="task in node.Tasks | filter: (state.DisplayOnlyRunningTasks || '') && { Status: { State: 'running' } }">
<div class="service_name">{{ task.ServiceName }}</div>
<div>Image: {{ task.Spec.ContainerSpec.Image | hideshasum }}</div>
<div>Status: {{ task.Status.State }}</div>
<div>Update: {{ task.Updated | getisodate }}</div>
</div>
</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -0,0 +1,74 @@
angular.module('swarmVisualizer', [])
.controller('SwarmVisualizerController', ['$q', '$scope', '$document', 'NodeService', 'ServiceService', 'TaskService', 'Notifications',
function ($q, $scope, $document, NodeService, ServiceService, TaskService, Notifications) {
$scope.state = {
ShowInformationPanel: true,
DisplayOnlyRunningTasks: false
};
function assignServiceName(services, tasks) {
for (var i = 0; i < services.length; i++) {
var service = services[i];
for (var j = 0; j < tasks.length; j++) {
var task = tasks[j];
if (task.ServiceId === service.Id) {
task.ServiceName = service.Name;
}
}
}
}
function assignTasksToNode(nodes, tasks) {
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
node.Tasks = [];
for (var j = 0; j < tasks.length; j++) {
var task = tasks[j];
if (task.NodeId === node.Id) {
node.Tasks.push(task);
}
}
}
}
function prepareVisualizerData(nodes, services, tasks) {
var visualizerData = {};
assignServiceName(services, tasks);
assignTasksToNode(nodes, tasks);
visualizerData.nodes = nodes;
$scope.visualizerData = visualizerData;
}
function initView() {
$('#loadingViewSpinner').show();
$q.all({
nodes: NodeService.nodes(),
services: ServiceService.services(),
tasks: TaskService.tasks()
})
.then(function success(data) {
var nodes = data.nodes;
$scope.nodes = nodes;
var services = data.services;
$scope.services = services;
var tasks = data.tasks;
$scope.tasks = tasks;
prepareVisualizerData(nodes, services, tasks);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to initialize cluster visualizer');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initView();
}]);

View File

@ -27,7 +27,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<span class="template-note" ng-bind-html="state.selectedTemplate.Note"></span> <div class="template-note" ng-if="state.selectedTemplate.Note" ng-bind-html="state.selectedTemplate.Note"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -151,7 +151,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer
$q.all({ $q.all({
templates: TemplateService.getTemplates(templatesKey), templates: TemplateService.getTemplates(templatesKey),
containers: ContainerService.getContainers(0), containers: ContainerService.containers(0),
volumes: VolumeService.getVolumes(), volumes: VolumeService.getVolumes(),
networks: NetworkService.networks( networks: NetworkService.networks(
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',

View File

@ -0,0 +1,14 @@
angular
.module('portainer')
.directive('autoFocus', ['$timeout', function porAutoFocus($timeout) {
var directive = {
restrict: 'A',
link: function($scope, $element) {
$timeout(function() {
$element[0].focus();
});
}
};
return directive;
}]);

View File

@ -0,0 +1,12 @@
angular.module('portainer').component('porEndpointSecurity', {
templateUrl: 'app/directives/endpointSecurity/porEndpointSecurity.html',
controller: 'porEndpointSecurityController',
bindings: {
// This object will be populated with the form data.
// Model reference in endpointSecurityModel.js
formData: '=',
// The component will use this object to initialize the default values
// if present.
endpoint: '<'
}
});

View File

@ -0,0 +1,126 @@
<div>
<!-- tls-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to connect to the Docker endpoint with TLS."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="$ctrl.formData.TLS"><i></i>
</label>
</div>
</div>
<!-- !tls-checkbox -->
<div class="col-sm-12 form-section-title" ng-if="$ctrl.formData.TLS">
TLS mode
</div>
<!-- note -->
<div class="form-group" ng-if="$ctrl.formData.TLS">
<div class="col-sm-12">
<span class="small text-muted">
You can find out more information about how to protect a Docker environment with TLS in the <a href="https://docs.docker.com/engine/security/https/" target="_blank">Docker documentation</a>.
</span>
</div>
</div>
<div class="form-group"></div>
<!-- endpoint-tls-mode -->
<div class="form-group" style="margin-bottom: 0" ng-if="$ctrl.formData.TLS">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="tls_client_ca" ng-model="$ctrl.formData.TLSMode" value="tls_client_ca">
<label for="tls_client_ca">
<div class="boxselector_header">
<i class="fa fa-shield" aria-hidden="true" style="margin-right: 2px;"></i>
TLS with server and client verification
</div>
<p>Use client certificates and server verification</p>
</label>
</div>
<div>
<input type="radio" id="tls_client_noca" ng-model="$ctrl.formData.TLSMode" value="tls_client_noca">
<label for="tls_client_noca">
<div class="boxselector_header">
<i class="fa fa-shield" aria-hidden="true" style="margin-right: 2px;"></i>
TLS with client verification only
</div>
<p>Use client certificates without server verification</p>
</label>
</div>
<div>
<input type="radio" id="tls_ca" ng-model="$ctrl.formData.TLSMode" value="tls_ca">
<label for="tls_ca">
<div class="boxselector_header">
<i class="fa fa-shield" aria-hidden="true" style="margin-right: 2px;"></i>
TLS with server verification only
</div>
<p>Only verify the server certificate</p>
</label>
</div>
<div>
<input type="radio" id="tls_only" ng-model="$ctrl.formData.TLSMode" value="tls_only">
<label for="tls_only">
<div class="boxselector_header">
<i class="fa fa-shield" aria-hidden="true" style="margin-right: 2px;"></i>
TLS only
</div>
<p>No server/client verification</p>
</label>
</div>
</div>
</div>
<!-- !endpoint-tls-mode -->
<div class="col-sm-12 form-section-title" ng-if="$ctrl.formData.TLS && $ctrl.formData.TLSMode !== 'tls_only'">
Required TLS files
</div>
<!-- tls-file-upload -->
<div ng-if="$ctrl.formData.TLS">
<!-- tls-file-ca -->
<div class="form-group" ng-if="$ctrl.formData.TLSMode === 'tls_client_ca' || $ctrl.formData.TLSMode === 'tls_ca'">
<label class="col-sm-3 col-lg-2 control-label text-left">TLS CA certificate</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSCACert">Select file</button>
<span style="margin-left: 5px;">
{{ $ctrl.formData.TLSCACert.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSCACert && $ctrl.formData.TLSCACert === $ctrl.endpoint.TLSConfig.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !tls-file-ca -->
<!-- tls-files-cert-key -->
<div ng-if="$ctrl.formData.TLSMode === 'tls_client_ca' || $ctrl.formData.TLSMode === 'tls_client_noca'">
<!-- tls-file-cert -->
<div class="form-group">
<label for="tls_cert" class="col-sm-3 col-lg-2 control-label text-left">TLS certificate</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSCert">Select file</button>
<span style="margin-left: 5px;">
{{ $ctrl.formData.TLSCert.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSCert && $ctrl.formData.TLSCert === $ctrl.endpoint.TLSConfig.TLSCert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSCert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !tls-file-cert -->
<!-- tls-file-key -->
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">TLS key</label>
<div class="col-sm-9 col-lg-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.formData.TLSKey">Select file</button>
<span style="margin-left: 5px;">
{{ $ctrl.formData.TLSKey.name }}
<i class="fa fa-check green-icon" ng-if="$ctrl.formData.TLSKey && $ctrl.formData.TLSKey === $ctrl.endpoint.TLSConfig.TLSKey" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!$ctrl.formData.TLSKey" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !tls-file-key -->
</div>
<!-- tls-files-cert-key -->
</div>
<!-- !tls-file-upload -->
</div>

View File

@ -0,0 +1,32 @@
angular.module('portainer')
.controller('porEndpointSecurityController', [function () {
var ctrl = this;
function initComponent() {
if (ctrl.endpoint) {
var endpoint = ctrl.endpoint;
var TLS = endpoint.TLSConfig.TLS;
ctrl.formData.TLS = TLS;
var CACert = endpoint.TLSConfig.TLSCACert;
ctrl.formData.TLSCACert = CACert;
var cert = endpoint.TLSConfig.TLSCert;
ctrl.formData.TLSCert = cert;
var key = endpoint.TLSConfig.TLSKey;
ctrl.formData.TLSKey = key;
if (TLS) {
if (CACert && cert && key) {
ctrl.formData.TLSMode = 'tls_client_ca';
} else if (cert && key) {
ctrl.formData.TLSMode = 'tls_client_noca';
} else if (CACert) {
ctrl.formData.TLSMode = 'tls_ca';
} else {
ctrl.formData.TLSMode = 'tls_only';
}
}
}
}
initComponent();
}]);

View File

@ -0,0 +1,7 @@
function EndpointSecurityFormData() {
this.TLS = false;
this.TLSMode = 'tls_client_ca';
this.TLSCACert = null;
this.TLSCert = null;
this.TLSKey = null;
}

View File

@ -0,0 +1,12 @@
angular.module('portainer').component('porSlider', {
templateUrl: 'app/directives/slider/porSlider.html',
controller: 'porSliderController',
bindings: {
model: '=',
onChange: '&',
floor: '<',
ceil: '<',
step: '<',
precision: '<'
}
});

View File

@ -0,0 +1,3 @@
<div>
<rzslider rz-slider-options="$ctrl.options" rz-slider-model="$ctrl.model"></rzslider>
</div>

View File

@ -0,0 +1,22 @@
angular.module('portainer')
.controller('porSliderController', function () {
var ctrl = this;
ctrl.options = {
floor: ctrl.floor,
ceil: ctrl.ceil,
step: ctrl.step,
precision: ctrl.precision,
showSelectionBar: true,
translate: function(value, sliderId, label) {
if (label === 'floor' || value === 0) {
return 'unlimited';
}
return value;
},
onChange: function() {
ctrl.onChange();
}
};
});

View File

@ -37,6 +37,20 @@ angular.module('portainer.filters', [])
} }
}; };
}) })
.filter('visualizerTask', function () {
'use strict';
return function (text) {
var status = _.toLower(text);
if (includeString(status, ['new', 'allocated', 'assigned', 'accepted', 'complete', 'preparing'])) {
return 'info';
} else if (includeString(status, ['pending'])) {
return 'warning';
} else if (includeString(status, ['shutdown', 'failed', 'rejected'])) {
return 'stopped';
}
return 'running';
};
})
.filter('taskstatusbadge', function () { .filter('taskstatusbadge', function () {
'use strict'; 'use strict';
return function (text) { return function (text) {

View File

@ -119,6 +119,5 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
} }
return []; return [];
} }
}; };
}]); }]);

View File

@ -16,11 +16,20 @@ function TemplateViewModel(data) {
this.Volumes = []; this.Volumes = [];
if (data.volumes) { if (data.volumes) {
this.Volumes = data.volumes.map(function (v) { this.Volumes = data.volumes.map(function (v) {
return { // @DEPRECATED: New volume definition introduced
readOnly: false, // via https://github.com/portainer/portainer/pull/1154
containerPath: v, var volume = {
readOnly: v.readonly || false,
containerPath: v.container || v,
type: 'auto' type: 'auto'
}; };
if (v.bind) {
volume.name = v.bind;
volume.type = 'bind';
}
return volume;
}); });
} }
this.Ports = []; this.Ports = [];

View File

@ -0,0 +1,12 @@
function ContainerStatsViewModel(data) {
this.Date = data.read;
this.MemoryUsage = data.memory_stats.usage;
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
this.CurrentCPUSystemUsage = data.cpu_stats.system_cpu_usage;
if (data.cpu_stats.cpu_usage.percpu_usage) {
this.CPUCores = data.cpu_stats.cpu_usage.percpu_usage.length;
}
this.Networks = _.values(data.networks);
}

View File

@ -3,8 +3,8 @@ function ImageViewModel(data) {
this.Tag = data.Tag; this.Tag = data.Tag;
this.Repository = data.Repository; this.Repository = data.Repository;
this.Created = data.Created; this.Created = data.Created;
this.Containers = data.dataUsage ? data.dataUsage.Containers : 0;
this.Checked = false; this.Checked = false;
this.RepoTags = data.RepoTags; this.RepoTags = data.RepoTags;
this.VirtualSize = data.VirtualSize; this.VirtualSize = data.VirtualSize;
this.ContainerCount = data.ContainerCount;
} }

View File

@ -0,0 +1,16 @@
function NetworkViewModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Scope = data.Scope;
this.Driver = data.Driver;
this.Attachable = data.Attachable;
this.IPAM = data.IPAM;
this.Containers = data.Containers;
this.Options = data.Options;
if (data.Portainer) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
}
}

View File

@ -5,4 +5,10 @@ function SecretViewModel(data) {
this.Version = data.Version.Index; this.Version = data.Version.Index;
this.Name = data.Spec.Name; this.Name = data.Spec.Name;
this.Labels = data.Spec.Labels; this.Labels = data.Spec.Labels;
if (data.Portainer) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
}
} }

View File

@ -13,7 +13,14 @@ angular.module('portainer.rest')
kill: {method: 'POST', params: {id: '@id', action: 'kill'}}, kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
pause: {method: 'POST', params: {id: '@id', action: 'pause'}}, pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}}, unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000}, stats: {
method: 'GET', params: { id: '@id', stream: false, action: 'stats' },
timeout: 4500
},
top: {
method: 'GET', params: { id: '@id', action: 'top' },
timeout: 4500
},
start: { start: {
method: 'POST', params: {id: '@id', action: 'start'}, method: 'POST', params: {id: '@id', action: 'start'},
transformResponse: genericHandler transformResponse: genericHandler

View File

@ -1,17 +0,0 @@
angular.module('portainer.rest')
.factory('ContainerTop', ['$http', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function ($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return {
get: function (id, params, callback, errorCallback) {
$http({
method: 'GET',
url: API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker/containers/' + id + '/top',
params: {
ps_args: params.ps_args
}
}).success(callback).error(function (data, status, headers, config) {
console.log(data);
});
}
};
}]);

View File

@ -20,6 +20,8 @@ angular.module('portainer.services')
name: endpointParams.name, name: endpointParams.name,
PublicURL: endpointParams.PublicURL, PublicURL: endpointParams.PublicURL,
TLS: endpointParams.TLS, TLS: endpointParams.TLS,
TLSSkipVerify: endpointParams.TLSSkipVerify,
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
authorizedUsers: endpointParams.authorizedUsers authorizedUsers: endpointParams.authorizedUsers
}; };
if (endpointParams.type && endpointParams.URL) { if (endpointParams.type && endpointParams.URL) {
@ -55,18 +57,20 @@ angular.module('portainer.services')
return Endpoints.create({}, endpoint).$promise; return Endpoints.create({}, endpoint).$promise;
}; };
service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile) { service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
var endpoint = { var endpoint = {
Name: name, Name: name,
URL: 'tcp://' + URL, URL: 'tcp://' + URL,
PublicURL: PublicURL, PublicURL: PublicURL,
TLS: TLS TLS: TLS,
TLSSkipVerify: TLSSkipVerify,
TLSSkipClientVerify: TLSSkipClientVerify
}; };
var deferred = $q.defer(); var deferred = $q.defer();
Endpoints.create({}, endpoint).$promise Endpoints.create({}, endpoint).$promise
.then(function success(data) { .then(function success(data) {
var endpointID = data.Id; var endpointID = data.Id;
if (TLS) { if (!TLSSkipVerify || !TLSSkipClientVerify) {
deferred.notify({upload: true}); deferred.notify({upload: true});
FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile) FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success() { .then(function success() {

View File

@ -134,5 +134,26 @@ angular.module('portainer.services')
return deferred.promise; return deferred.promise;
}; };
service.initAdministrator = function(username, password) {
return Users.initAdminUser({ Username: username, Password: password }).$promise;
};
service.administratorExists = function() {
var deferred = $q.defer();
Users.checkAdminUser({}).$promise
.then(function success(data) {
deferred.resolve(true);
})
.catch(function error(err) {
if (err.status === 404) {
deferred.resolve(false);
}
deferred.reject({ msg: 'Unable to verify administrator account existence', err: err });
});
return deferred.promise;
};
return service; return service;
}]); }]);

View File

@ -0,0 +1,251 @@
angular.module('portainer.services')
.factory('ChartService', [function ChartService() {
'use strict';
// Max. number of items to display on a chart
var CHART_LIMIT = 600;
var service = {};
service.CreateCPUChart = function(context) {
return new Chart(context, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'CPU',
data: [],
fill: true,
backgroundColor: 'rgba(151,187,205,0.4)',
borderColor: 'rgba(151,187,205,0.6)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointBorderColor: 'rgba(151,187,205,1)',
pointRadius: 2,
borderWidth: 2
}
]
},
options: {
animation: {
duration: 0
},
responsiveAnimationDuration: 0,
responsive: true,
tooltips: {
mode: 'index',
intersect: false,
position: 'nearest',
callbacks: {
label: function(tooltipItem, data) {
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
return percentageBasedTooltipLabel(datasetLabel, tooltipItem.yLabel);
}
}
},
hover: {
animationDuration: 0
},
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
callback: percentageBasedAxisLabel
}
}
]
}
}
});
};
service.CreateMemoryChart = function(context) {
return new Chart(context, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Memory',
data: [],
fill: true,
backgroundColor: 'rgba(151,187,205,0.4)',
borderColor: 'rgba(151,187,205,0.6)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointBorderColor: 'rgba(151,187,205,1)',
pointRadius: 2,
borderWidth: 2
}
]
},
options: {
animation: {
duration: 0
},
responsiveAnimationDuration: 0,
responsive: true,
tooltips: {
mode: 'index',
intersect: false,
position: 'nearest',
callbacks: {
label: function(tooltipItem, data) {
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
return byteBasedTooltipLabel(datasetLabel, tooltipItem.yLabel);
}
}
},
hover: {
animationDuration: 0
},
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
callback: byteBasedAxisLabel
}
}
]
}
}
});
};
service.CreateNetworkChart = function(context) {
return new Chart(context, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'RX on eth0',
data: [],
fill: false,
backgroundColor: 'rgba(151,187,205,0.4)',
borderColor: 'rgba(151,187,205,0.6)',
pointBackgroundColor: 'rgba(151,187,205,1)',
pointBorderColor: 'rgba(151,187,205,1)',
pointRadius: 2,
borderWidth: 2
},
{
label: 'TX on eth0',
data: [],
fill: false,
backgroundColor: 'rgba(255,180,174,0.5)',
borderColor: 'rgba(255,180,174,0.7)',
pointBackgroundColor: 'rgba(255,180,174,1)',
pointBorderColor: 'rgba(255,180,174,1)',
pointRadius: 2,
borderWidth: 2
}
]
},
options: {
animation: {
duration: 0
},
responsiveAnimationDuration: 0,
responsive: true,
tooltips: {
mode: 'index',
intersect: false,
position: 'average',
callbacks: {
label: function(tooltipItem, data) {
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label;
return byteBasedTooltipLabel(datasetLabel, tooltipItem.yLabel);
}
}
},
hover: {
animationDuration: 0
},
scales: {
yAxes: [{
ticks: {
beginAtZero: true,
callback: byteBasedAxisLabel
}
}]
}
}
});
};
service.UpdateMemoryChart = function(label, value, chart) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(value);
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
chart.data.labels.pop();
chart.data.datasets[0].data.pop();
}
chart.update(0);
};
service.UpdateCPUChart = function(label, value, chart) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(value);
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
chart.data.labels.pop();
chart.data.datasets[0].data.pop();
}
chart.update(0);
};
service.UpdateNetworkChart = function(label, rx, tx, chart) {
chart.data.labels.push(label);
chart.data.datasets[0].data.push(rx);
chart.data.datasets[1].data.push(tx);
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
chart.data.labels.pop();
chart.data.datasets[0].data.pop();
chart.data.datasets[1].data.pop();
}
chart.update(0);
};
function byteBasedTooltipLabel(label, value) {
var processedValue = 0;
if (value > 5) {
processedValue = filesize(value, {base: 10, round: 1});
} else {
processedValue = value.toFixed(1) + 'B';
}
return label + ': ' + processedValue;
}
function byteBasedAxisLabel(value, index, values) {
if (value > 5) {
return filesize(value, {base: 10, round: 1});
}
return value.toFixed(1) + 'B';
}
function percentageBasedAxisLabel(value, index, values) {
if (value > 1) {
return Math.round(value) + '%';
}
return value.toFixed(1) + '%';
}
function percentageBasedTooltipLabel(label, value) {
var processedValue = 0;
if (value > 1) {
processedValue = Math.round(value);
} else {
processedValue = value.toFixed(1);
}
return label + ': ' + processedValue + '%';
}
return service;
}]);

View File

@ -3,7 +3,22 @@ angular.module('portainer.services')
'use strict'; 'use strict';
var service = {}; var service = {};
service.getContainers = function (all) { service.container = function(id) {
var deferred = $q.defer();
Container.get({ id: id }).$promise
.then(function success(data) {
var container = new ContainerDetailsViewModel(data);
deferred.resolve(container);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve container information', err: err });
});
return deferred.promise;
};
service.containers = function(all) {
var deferred = $q.defer(); var deferred = $q.defer();
Container.query({ all: all }).$promise Container.query({ all: all }).$promise
.then(function success(data) { .then(function success(data) {
@ -11,7 +26,7 @@ angular.module('portainer.services')
deferred.resolve(containers); deferred.resolve(containers);
}) })
.catch(function error(err) { .catch(function error(err) {
deferred.reject({ msg: 'Unable to retriever containers', err: err }); deferred.reject({ msg: 'Unable to retrieve containers', err: err });
}); });
return deferred.promise; return deferred.promise;
}; };
@ -105,5 +120,35 @@ angular.module('portainer.services')
return deferred.promise; return deferred.promise;
}; };
service.containerStats = function(id) {
var deferred = $q.defer();
Container.stats({id: id}).$promise
.then(function success(data) {
var containerStats = new ContainerStatsViewModel(data);
deferred.resolve(containerStats);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
service.containerTop = function(id) {
var deferred = $q.defer();
Container.top({id: id}).$promise
.then(function success(data) {
var containerTop = data;
deferred.resolve(containerTop);
})
.catch(function error(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service; return service;
}]); }]);

View File

@ -1,5 +1,5 @@
angular.module('portainer.services') angular.module('portainer.services')
.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', 'SystemService', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, SystemService) { .factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', 'ContainerService', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, ContainerService) {
'use strict'; 'use strict';
var service = {}; var service = {};
@ -24,17 +24,23 @@ angular.module('portainer.services')
var deferred = $q.defer(); var deferred = $q.defer();
$q.all({ $q.all({
dataUsage: withUsage ? SystemService.dataUsage() : { Images: [] }, containers: withUsage ? ContainerService.containers(1) : [],
images: Image.query({}).$promise images: Image.query({}).$promise
}) })
.then(function success(data) { .then(function success(data) {
var images = data.images.map(function(item) { var containers = data.containers;
item.dataUsage = data.dataUsage.Images.find(function(usage) {
return item.Id === usage.Id;
});
var images = data.images.map(function(item) {
item.ContainerCount = 0;
for (var i = 0; i < containers.length; i++) {
var container = containers[i];
if (container.ImageID === item.Id) {
item.ContainerCount++;
}
}
return new ImageViewModel(item); return new ImageViewModel(item);
}); });
deferred.resolve(images); deferred.resolve(images);
}) })
.catch(function error(err) { .catch(function error(err) {

View File

@ -3,6 +3,35 @@ angular.module('portainer.services')
'use strict'; 'use strict';
var service = {}; var service = {};
service.create = function(networkConfiguration) {
var deferred = $q.defer();
Network.create(networkConfiguration).$promise
.then(function success(data) {
deferred.resolve(data);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to create network', err: err });
});
return deferred.promise;
};
service.network = function(id) {
var deferred = $q.defer();
Network.get({id: id}).$promise
.then(function success(data) {
var network = new NetworkViewModel(data);
deferred.resolve(network);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve network details', err: err});
});
return deferred.promise;
};
service.networks = function(localNetworks, swarmNetworks, swarmAttachableNetworks, globalNetworks) { service.networks = function(localNetworks, swarmNetworks, swarmAttachableNetworks, globalNetworks) {
var deferred = $q.defer(); var deferred = $q.defer();
@ -23,6 +52,8 @@ angular.module('portainer.services')
if (globalNetworks && network.Scope === 'global') { if (globalNetworks && network.Scope === 'global') {
return network; return network;
} }
}).map(function (item) {
return new NetworkViewModel(item);
}); });
deferred.resolve(filteredNetworks); deferred.resolve(filteredNetworks);

View File

@ -3,7 +3,7 @@ angular.module('portainer.services')
'use strict'; 'use strict';
var service = {}; var service = {};
service.nodes = function(id) { service.nodes = function() {
var deferred = $q.defer(); var deferred = $q.defer();
Node.query({}).$promise Node.query({}).$promise

Some files were not shown because too many files have changed in this diff Show More