mirror of https://github.com/portainer/portainer
commit
1bccd521f8
|
@ -2,7 +2,7 @@ package bolt
|
|||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
func (m *Migrator) updateSettingsToVersion3() error {
|
||||
func (m *Migrator) updateSettingsToDBVersion3() error {
|
||||
legacySettings, err := m.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -30,7 +30,7 @@ func NewMigrator(store *Store, version int) *Migrator {
|
|||
func (m *Migrator) Migrate() error {
|
||||
|
||||
// Portainer < 1.12
|
||||
if m.CurrentDBVersion == 0 {
|
||||
if m.CurrentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -38,7 +38,7 @@ func (m *Migrator) Migrate() error {
|
|||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.CurrentDBVersion == 1 {
|
||||
if m.CurrentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -50,8 +50,16 @@ func (m *Migrator) Migrate() error {
|
|||
}
|
||||
|
||||
// Portainer 1.13.x
|
||||
if m.CurrentDBVersion == 2 {
|
||||
err := m.updateSettingsToVersion3()
|
||||
if m.CurrentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.CurrentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
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(),
|
||||
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(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||
|
|
|
@ -191,12 +191,15 @@ func main() {
|
|||
}
|
||||
if len(endpoints) == 0 {
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSSkipVerify: false,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
|
@ -245,7 +248,7 @@ func main() {
|
|||
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()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -22,6 +22,16 @@ type (
|
|||
endpointsToUpdate []*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 (
|
||||
|
@ -55,6 +65,28 @@ func isValidEndpoint(endpoint *portainer.Endpoint) bool {
|
|||
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 {
|
||||
for idx, v := range endpoints {
|
||||
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 {
|
||||
var endpoint *portainer.Endpoint
|
||||
if original.URL != updated.URL || original.TLS != updated.TLS ||
|
||||
(updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) ||
|
||||
(updated.TLS && original.TLSCertPath != updated.TLSCertPath) ||
|
||||
(updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) {
|
||||
if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) {
|
||||
endpoint = original
|
||||
endpoint.URL = updated.URL
|
||||
if updated.TLS {
|
||||
endpoint.TLS = true
|
||||
endpoint.TLSCACertPath = updated.TLSCACertPath
|
||||
endpoint.TLSCertPath = updated.TLSCertPath
|
||||
endpoint.TLSKeyPath = updated.TLSKeyPath
|
||||
if updated.TLSConfig.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify
|
||||
endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath
|
||||
endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath
|
||||
endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath
|
||||
} else {
|
||||
endpoint.TLS = false
|
||||
endpoint.TLSCACertPath = ""
|
||||
endpoint.TLSCertPath = ""
|
||||
endpoint.TLSKeyPath = ""
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
}
|
||||
}
|
||||
return endpoint
|
||||
|
@ -141,7 +176,7 @@ func (job endpointSyncJob) Sync() error {
|
|||
return err
|
||||
}
|
||||
|
||||
var fileEndpoints []portainer.Endpoint
|
||||
var fileEndpoints []fileEndpoint
|
||||
err = json.Unmarshal(data, &fileEndpoints)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
return err
|
||||
|
@ -156,7 +191,9 @@ func (job endpointSyncJob) Sync() error {
|
|||
return err
|
||||
}
|
||||
|
||||
sync := job.prepareSyncData(storedEndpoints, fileEndpoints)
|
||||
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
|
||||
|
||||
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
||||
if sync.requireSync() {
|
||||
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
|
|
|
@ -4,31 +4,38 @@ import (
|
|||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// 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 != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
TLSConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
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 != "" {
|
||||
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
|
||||
return TLSConfig, nil
|
||||
}
|
||||
|
|
|
@ -13,8 +13,10 @@ const (
|
|||
const (
|
||||
ErrUserNotFound = Error("User not found")
|
||||
ErrUserAlreadyExists = Error("User already exists")
|
||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed.")
|
||||
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
|
||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
||||
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.
|
||||
|
|
|
@ -95,7 +95,7 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
|
|||
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 {
|
||||
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
||||
err := os.RemoveAll(storePath)
|
||||
|
@ -105,6 +105,29 @@ func (service *Service) DeleteTLSFiles(folder string) error {
|
|||
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.
|
||||
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
|
||||
path := path.Join(service.fileStorePath, name)
|
||||
|
|
|
@ -57,10 +57,12 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag
|
|||
|
||||
type (
|
||||
postEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
TLSSkipClientVerify bool
|
||||
}
|
||||
|
||||
postEndpointsResponse struct {
|
||||
|
@ -73,10 +75,12 @@ type (
|
|||
}
|
||||
|
||||
putEndpointsRequest struct {
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
PublicURL string `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{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
PublicURL: req.PublicURL,
|
||||
TLS: req.TLS,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
PublicURL: req.PublicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: req.TLS,
|
||||
TLSSkipVerify: req.TLSSkipVerify,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
|
@ -139,12 +146,19 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
|||
|
||||
if req.TLS {
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
|
||||
if !req.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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))
|
||||
if req.TLS {
|
||||
endpoint.TLS = true
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = req.TLSSkipVerify
|
||||
if !req.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
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 {
|
||||
endpoint.TLS = false
|
||||
endpoint.TLSCACertPath = ""
|
||||
endpoint.TLSCertPath = ""
|
||||
endpoint.TLSKeyPath = ""
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = true
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(folder)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
|
@ -350,7 +379,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
|||
return
|
||||
}
|
||||
|
||||
if endpoint.TLS {
|
||||
if endpoint.TLSConfig.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
|
|
|
@ -30,6 +30,7 @@ func NewFileHandler(assetPath string) *FileHandler {
|
|||
"/js": true,
|
||||
"/images": true,
|
||||
"/fonts": true,
|
||||
"/ico": true,
|
||||
},
|
||||
}
|
||||
return h
|
||||
|
|
|
@ -78,6 +78,10 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht
|
|||
resourceControlType = portainer.ServiceResourceControl
|
||||
case "volume":
|
||||
resourceControlType = portainer.VolumeResourceControl
|
||||
case "network":
|
||||
resourceControlType = portainer.NetworkResourceControl
|
||||
case "secret":
|
||||
resourceControlType = portainer.SecretResourceControl
|
||||
default:
|
||||
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
|
|
|
@ -82,6 +82,7 @@ type (
|
|||
}
|
||||
|
||||
postAdminInitRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
@ -358,10 +359,14 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(users) == 0 {
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Username: req.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
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)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -396,6 +397,22 @@ func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Requ
|
|||
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))
|
||||
|
||||
if err == portainer.ErrUserNotFound {
|
||||
|
|
|
@ -71,8 +71,8 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
|||
|
||||
// Should not be managed here
|
||||
var tlsConfig *tls.Config
|
||||
if endpoint.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false)
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||
return
|
||||
|
|
|
@ -82,6 +82,54 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer
|
|||
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{} {
|
||||
metadata := make(map[string]interface{})
|
||||
metadata["ResourceControl"] = resourceControl
|
||||
|
|
|
@ -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) {
|
||||
u.Scheme = "https"
|
||||
proxy := factory.createReverseProxy(u)
|
||||
config, err := crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false)
|
||||
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -110,3 +110,76 @@ func filterServiceList(serviceData []interface{}, resourceControls []portainer.R
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
|
|||
}
|
||||
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLS {
|
||||
if endpoint.TLSConfig.TLS {
|
||||
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -34,6 +34,9 @@ func (proxy *socketProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -53,17 +53,26 @@ func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Resp
|
|||
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||
path := request.URL.Path
|
||||
|
||||
if strings.HasPrefix(path, "/containers") {
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/containers"):
|
||||
return p.proxyContainerRequest(request)
|
||||
} else if strings.HasPrefix(path, "/services") {
|
||||
case strings.HasPrefix(path, "/services"):
|
||||
return p.proxyServiceRequest(request)
|
||||
} else if strings.HasPrefix(path, "/volumes") {
|
||||
case strings.HasPrefix(path, "/volumes"):
|
||||
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)
|
||||
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) {
|
||||
|
@ -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) {
|
||||
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
|
||||
// before executing the original request.
|
||||
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
||||
|
|
|
@ -52,7 +52,7 @@ func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearc
|
|||
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -155,16 +155,20 @@ type (
|
|||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it.
|
||||
Endpoint struct {
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
TLS bool `json:"TLS"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
|
||||
// Deprecated fields
|
||||
// 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.
|
||||
|
@ -172,20 +176,18 @@ type (
|
|||
|
||||
// ResourceControl represent a reference to a Docker resource with specific access controls
|
||||
ResourceControl struct {
|
||||
ID ResourceControlID `json:"Id"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
SubResourceIDs []string `json:"SubResourceIds"`
|
||||
Type ResourceControlType `json:"Type"`
|
||||
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||
|
||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||
ID ResourceControlID `json:"Id"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
SubResourceIDs []string `json:"SubResourceIds"`
|
||||
Type ResourceControlType `json:"Type"`
|
||||
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated: OwnerID field is deprecated in DBVersion == 2
|
||||
OwnerID UserID `json:"OwnerId"`
|
||||
// Deprecated: AccessLevel field is deprecated in DBVersion == 2
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
||||
// Deprecated in DBVersion == 2
|
||||
OwnerID UserID `json:"OwnerId,omitempty"`
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service).
|
||||
|
@ -325,6 +327,7 @@ type (
|
|||
FileService interface {
|
||||
StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error
|
||||
GetPathForTLSFile(folder string, fileType TLSFileType) (string, error)
|
||||
DeleteTLSFile(folder string, fileType TLSFileType) error
|
||||
DeleteTLSFiles(folder string) error
|
||||
}
|
||||
|
||||
|
@ -342,9 +345,9 @@ type (
|
|||
|
||||
const (
|
||||
// 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 = 3
|
||||
DBVersion = 4
|
||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
)
|
||||
|
@ -396,4 +399,8 @@ const (
|
|||
ServiceResourceControl
|
||||
// VolumeResourceControl represents a resource control associated to a Docker volume
|
||||
VolumeResourceControl
|
||||
// NetworkResourceControl represents a resource control associated to a Docker network
|
||||
NetworkResourceControl
|
||||
// SecretResourceControl represents a resource control associated to a Docker secret
|
||||
SecretResourceControl
|
||||
)
|
||||
|
|
386
api/swagger.yaml
386
api/swagger.yaml
|
@ -1,27 +1,62 @@
|
|||
---
|
||||
swagger: "2.0"
|
||||
info:
|
||||
description: "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.\nYou can find out more about Portainer at [http://portainer.io](http://portainer.io)\
|
||||
\ and get some support on [Slack](http://portainer.io/slack/).\n\n# Authentication\n\
|
||||
\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\
|
||||
\ and thus requires you to provide a token in the **Authorization** header of\
|
||||
\ each request\nwith the **Bearer** authentication mechanism.\n\nExample:\n```\n\
|
||||
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE\n\
|
||||
```\n\n# Security\n\nEach API endpoint has an associated access policy, it is\
|
||||
\ documented in the description of each endpoint.\n\nDifferent access policies\
|
||||
\ are available:\n* Public access\n* Authenticated access\n* Restricted access\n\
|
||||
* 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\
|
||||
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\
|
||||
\ 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\
|
||||
\ with this access policy.\n"
|
||||
version: "1.14.0"
|
||||
description: |
|
||||
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.
|
||||
Examples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8
|
||||
You can find out more about Portainer at [http://portainer.io](http://portainer.io) and get some support on [Slack](http://portainer.io/slack/).
|
||||
|
||||
# Authentication
|
||||
|
||||
Most of the API endpoints require to be authenticated as well as some level of authorization to be used.
|
||||
Portainer API uses JSON Web Token to manage authentication and thus requires you to provide a token in the **Authorization** header of each request
|
||||
with the **Bearer** authentication mechanism.
|
||||
|
||||
Example:
|
||||
```
|
||||
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE
|
||||
```
|
||||
|
||||
# Security
|
||||
|
||||
Each API endpoint has an associated access policy, it is documented in the description of each endpoint.
|
||||
|
||||
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"
|
||||
contact:
|
||||
email: "info@portainer.io"
|
||||
|
@ -63,8 +98,9 @@ paths:
|
|||
tags:
|
||||
- "auth"
|
||||
summary: "Authenticate a user"
|
||||
description: "Use this endpoint to authenticate against Portainer using a username\
|
||||
\ and password. \n**Access policy**: public\n"
|
||||
description: |
|
||||
Use this endpoint to authenticate against Portainer using a username and password.
|
||||
**Access policy**: public
|
||||
operationId: "AuthenticateUser"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -105,8 +141,9 @@ paths:
|
|||
tags:
|
||||
- "dockerhub"
|
||||
summary: "Retrieve DockerHub information"
|
||||
description: "Use this endpoint to retrieve the information used to connect\
|
||||
\ to the DockerHub \n**Access policy**: authenticated\n"
|
||||
description: |
|
||||
Use this endpoint to retrieve the information used to connect to the DockerHub
|
||||
**Access policy**: authenticated
|
||||
operationId: "DockerHubInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -124,8 +161,9 @@ paths:
|
|||
tags:
|
||||
- "dockerhub"
|
||||
summary: "Update DockerHub information"
|
||||
description: "Use this endpoint to update the information used to connect to\
|
||||
\ the DockerHub \n**Access policy**: administrator\n"
|
||||
description: |
|
||||
Use this endpoint to update the information used to connect to the DockerHub
|
||||
**Access policy**: administrator
|
||||
operationId: "DockerHubUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -157,9 +195,11 @@ paths:
|
|||
tags:
|
||||
- "endpoints"
|
||||
summary: "List endpoints"
|
||||
description: "List all endpoints based on the current user authorizations. Will\n\
|
||||
return all endpoints if using an administrator account otherwise it will\n\
|
||||
only return authorized endpoints. \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
List all endpoints based on the current user authorizations. Will
|
||||
return all endpoints if using an administrator account otherwise it will
|
||||
only return authorized endpoints.
|
||||
**Access policy**: restricted
|
||||
operationId: "EndpointList"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -177,8 +217,9 @@ paths:
|
|||
tags:
|
||||
- "endpoints"
|
||||
summary: "Create a new endpoint"
|
||||
description: "Create a new endpoint that will be used to manage a Docker environment.\
|
||||
\ \n**Access policy**: administrator\n"
|
||||
description: |
|
||||
Create a new endpoint that will be used to manage a Docker environment.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -219,8 +260,9 @@ paths:
|
|||
tags:
|
||||
- "endpoints"
|
||||
summary: "Inspect an endpoint"
|
||||
description: "Retrieve details abount an endpoint. \n**Access policy**: administrator\
|
||||
\ \n"
|
||||
description: |
|
||||
Retrieve details abount an endpoint.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -257,7 +299,9 @@ paths:
|
|||
tags:
|
||||
- "endpoints"
|
||||
summary: "Update an endpoint"
|
||||
description: "Update an endpoint. \n**Access policy**: administrator\n"
|
||||
description: |
|
||||
Update an endpoint.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -307,7 +351,9 @@ paths:
|
|||
tags:
|
||||
- "endpoints"
|
||||
summary: "Remove an endpoint"
|
||||
description: "Remove an endpoint. \n**Access policy**: administrator \n"
|
||||
description: |
|
||||
Remove an endpoint.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
|
@ -348,8 +394,9 @@ paths:
|
|||
tags:
|
||||
- "endpoints"
|
||||
summary: "Manage accesses to an endpoint"
|
||||
description: "Manage user and team accesses to an endpoint. \n**Access policy**:\
|
||||
\ administrator \n"
|
||||
description: |
|
||||
Manage user and team accesses to an endpoint.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointAccessUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -388,15 +435,17 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/registries:
|
||||
get:
|
||||
tags:
|
||||
- "registries"
|
||||
summary: "List registries"
|
||||
description: "List all registries based on the current user authorizations.\n\
|
||||
Will return all registries if using an administrator account otherwise it\n\
|
||||
will only return authorized registries. \n**Access policy**: restricted \
|
||||
\ \n"
|
||||
description: |
|
||||
List all registries based on the current user authorizations.
|
||||
Will return all registries if using an administrator account otherwise it
|
||||
will only return authorized registries.
|
||||
**Access policy**: restricted
|
||||
operationId: "RegistryList"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -414,8 +463,9 @@ paths:
|
|||
tags:
|
||||
- "registries"
|
||||
summary: "Create a new registry"
|
||||
description: "Create a new registry. \n**Access policy**: administrator \
|
||||
\ \n"
|
||||
description: |
|
||||
Create a new registry.
|
||||
**Access policy**: administrator
|
||||
operationId: "RegistryCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -456,8 +506,9 @@ paths:
|
|||
tags:
|
||||
- "registries"
|
||||
summary: "Inspect a registry"
|
||||
description: "Retrieve details about a registry. \n**Access policy**: administrator\
|
||||
\ \n"
|
||||
description: |
|
||||
Retrieve details about a registry.
|
||||
**Access policy**: administrator
|
||||
operationId: "RegistryInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -494,7 +545,9 @@ paths:
|
|||
tags:
|
||||
- "registries"
|
||||
summary: "Update a registry"
|
||||
description: "Update a registry. \n**Access policy**: administrator \n"
|
||||
description: |
|
||||
Update a registry.
|
||||
**Access policy**: administrator
|
||||
operationId: "RegistryUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -551,8 +604,9 @@ paths:
|
|||
tags:
|
||||
- "registries"
|
||||
summary: "Remove a registry"
|
||||
description: "Remove a registry. \n**Access policy**: administrator \
|
||||
\ \n"
|
||||
description: |
|
||||
Remove a registry.
|
||||
**Access policy**: administrator
|
||||
operationId: "RegistryDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
|
@ -586,8 +640,9 @@ paths:
|
|||
tags:
|
||||
- "registries"
|
||||
summary: "Manage accesses to a registry"
|
||||
description: "Manage user and team accesses to a registry. \n**Access policy**:\
|
||||
\ administrator \n"
|
||||
description: |
|
||||
Manage user and team accesses to a registry.
|
||||
**Access policy**: administrator
|
||||
operationId: "RegistryAccessUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -631,8 +686,9 @@ paths:
|
|||
tags:
|
||||
- "resource_controls"
|
||||
summary: "Create a new resource control"
|
||||
description: "Create a new resource control to restrict access to a Docker resource.\
|
||||
\ \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
Create a new resource control to restrict access to a Docker resource.
|
||||
**Access policy**: restricted
|
||||
operationId: "ResourceControlCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -678,8 +734,9 @@ paths:
|
|||
tags:
|
||||
- "resource_controls"
|
||||
summary: "Update a resource control"
|
||||
description: "Update a resource control. \n**Access policy**: restricted \
|
||||
\ \n"
|
||||
description: |
|
||||
Update a resource control.
|
||||
**Access policy**: restricted
|
||||
operationId: "ResourceControlUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -729,8 +786,9 @@ paths:
|
|||
tags:
|
||||
- "resource_controls"
|
||||
summary: "Remove a resource control"
|
||||
description: "Remove a resource control. \n**Access policy**: restricted \
|
||||
\ \n"
|
||||
description: |
|
||||
Remove a resource control.
|
||||
**Access policy**: restricted
|
||||
operationId: "ResourceControlDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
|
@ -771,8 +829,9 @@ paths:
|
|||
tags:
|
||||
- "settings"
|
||||
summary: "Retrieve Portainer settings"
|
||||
description: "Retrieve Portainer settings. \n**Access policy**: administrator\
|
||||
\ \n"
|
||||
description: |
|
||||
Retrieve Portainer settings.
|
||||
**Access policy**: administrator
|
||||
operationId: "SettingsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -790,8 +849,9 @@ paths:
|
|||
tags:
|
||||
- "settings"
|
||||
summary: "Update Portainer settings"
|
||||
description: "Update Portainer settings. \n**Access policy**: administrator\
|
||||
\ \n"
|
||||
description: |
|
||||
Update Portainer settings.
|
||||
**Access policy**: administrator
|
||||
operationId: "SettingsUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -823,9 +883,9 @@ paths:
|
|||
tags:
|
||||
- "settings"
|
||||
summary: "Retrieve Portainer public settings"
|
||||
description: "Retrieve public settings. Returns a small set of settings that\
|
||||
\ are not reserved to administrators only. \n**Access policy**: public \
|
||||
\ \n"
|
||||
description: |
|
||||
Retrieve public settings. Returns a small set of settings that are not reserved to administrators only.
|
||||
**Access policy**: public
|
||||
operationId: "PublicSettingsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -844,8 +904,9 @@ paths:
|
|||
tags:
|
||||
- "settings"
|
||||
summary: "Test LDAP connectivity"
|
||||
description: "Test LDAP connectivity using LDAP details. \n**Access policy**:\
|
||||
\ administrator \n"
|
||||
description: |
|
||||
Test LDAP connectivity using LDAP details.
|
||||
**Access policy**: administrator
|
||||
operationId: "SettingsLDAPCheck"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -877,8 +938,9 @@ paths:
|
|||
tags:
|
||||
- "status"
|
||||
summary: "Check Portainer status"
|
||||
description: "Retrieve Portainer status. \n**Access policy**: public \
|
||||
\ \n"
|
||||
description: |
|
||||
Retrieve Portainer status.
|
||||
**Access policy**: public
|
||||
operationId: "StatusInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -897,9 +959,9 @@ paths:
|
|||
tags:
|
||||
- "users"
|
||||
summary: "List users"
|
||||
description: "List Portainer users. Non-administrator users will only be able\
|
||||
\ to list other non-administrator user accounts. \n**Access policy**: restricted\
|
||||
\ \n"
|
||||
description: |
|
||||
List Portainer users. Non-administrator users will only be able to list other non-administrator user accounts.
|
||||
**Access policy**: restricted
|
||||
operationId: "UserList"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -917,9 +979,10 @@ paths:
|
|||
tags:
|
||||
- "users"
|
||||
summary: "Create a new user"
|
||||
description: "Create a new Portainer user. Only team leaders and administrators\
|
||||
\ can create users. Only administrators can\ncreate an administrator user\
|
||||
\ account. \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
Create a new Portainer user. Only team leaders and administrators can create users. Only administrators can
|
||||
create an administrator user account.
|
||||
**Access policy**: restricted
|
||||
operationId: "UserCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -967,8 +1030,9 @@ paths:
|
|||
tags:
|
||||
- "users"
|
||||
summary: "Inspect a user"
|
||||
description: "Retrieve details about a user. \n**Access policy**: administrator\
|
||||
\ \n"
|
||||
description: |
|
||||
Retrieve details about a user.
|
||||
**Access policy**: administrator
|
||||
operationId: "UserInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -1005,8 +1069,9 @@ paths:
|
|||
tags:
|
||||
- "users"
|
||||
summary: "Update a user"
|
||||
description: "Update user details. A regular user account can only update his\
|
||||
\ details. \n**Access policy**: authenticated \n"
|
||||
description: |
|
||||
Update user details. A regular user account can only update his details.
|
||||
**Access policy**: authenticated
|
||||
operationId: "UserUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -1056,7 +1121,9 @@ paths:
|
|||
tags:
|
||||
- "users"
|
||||
summary: "Remove a user"
|
||||
description: "Remove a user. \n**Access policy**: administrator \n"
|
||||
description: |
|
||||
Remove a user.
|
||||
**Access policy**: administrator
|
||||
operationId: "UserDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
|
@ -1090,8 +1157,9 @@ paths:
|
|||
tags:
|
||||
- "users"
|
||||
summary: "Inspect a user memberships"
|
||||
description: "Inspect a user memberships. \n**Access policy**: authenticated\
|
||||
\ \n"
|
||||
description: |
|
||||
Inspect a user memberships.
|
||||
**Access policy**: authenticated
|
||||
operationId: "UserMembershipsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -1124,13 +1192,15 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/users/{id}/passwd:
|
||||
post:
|
||||
tags:
|
||||
- "users"
|
||||
summary: "Check password validity for a user"
|
||||
description: "Check if the submitted password is valid for the specified user.\
|
||||
\ \n**Access policy**: authenticated \n"
|
||||
description: |
|
||||
Check if the submitted password is valid for the specified user.
|
||||
**Access policy**: authenticated
|
||||
operationId: "UserPasswordCheck"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -1171,13 +1241,15 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/users/admin/check:
|
||||
get:
|
||||
tags:
|
||||
- "users"
|
||||
summary: "Check administrator account existence"
|
||||
description: "Check if an administrator account exists in the database.\n**Access\
|
||||
\ policy**: public \n"
|
||||
description: |
|
||||
Check if an administrator account exists in the database.
|
||||
**Access policy**: public
|
||||
operationId: "UserAdminCheck"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -1198,13 +1270,15 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/users/admin/init:
|
||||
post:
|
||||
tags:
|
||||
- "users"
|
||||
summary: "Initialize administrator account"
|
||||
description: "Initialize the 'admin' user account.\n**Access policy**: public\
|
||||
\ \n"
|
||||
description: |
|
||||
Initialize the 'admin' user account.
|
||||
**Access policy**: public
|
||||
operationId: "UserAdminInit"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -1238,34 +1312,35 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/upload/tls/{certificate}:
|
||||
post:
|
||||
tags:
|
||||
- "upload"
|
||||
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"
|
||||
consumes:
|
||||
- "multipart/form-data"
|
||||
- multipart/form-data
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "certificate"
|
||||
in: "path"
|
||||
- in: "path"
|
||||
name: "certificate"
|
||||
description: "TLS file type. Valid values are 'ca', 'cert' or 'key'."
|
||||
required: true
|
||||
type: "string"
|
||||
- name: "folder"
|
||||
in: "query"
|
||||
description: "Folder where the TLS file will be stored. Will be created if\
|
||||
\ not existing."
|
||||
- in: "query"
|
||||
name: "folder"
|
||||
description: "Folder where the TLS file will be stored. Will be created if not existing."
|
||||
required: true
|
||||
type: "string"
|
||||
- name: "file"
|
||||
in: "formData"
|
||||
description: "The file to upload."
|
||||
required: false
|
||||
- in: "formData"
|
||||
name: "file"
|
||||
type: "file"
|
||||
description: "The file to upload."
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
|
@ -1280,13 +1355,15 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/teams:
|
||||
get:
|
||||
tags:
|
||||
- "teams"
|
||||
summary: "List teams"
|
||||
description: "List teams. For non-administrator users, will only list the teams\
|
||||
\ they are member of. \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
List teams. For non-administrator users, will only list the teams they are member of.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamList"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -1304,8 +1381,9 @@ paths:
|
|||
tags:
|
||||
- "teams"
|
||||
summary: "Create a new team"
|
||||
description: "Create a new team. \n**Access policy**: administrator \
|
||||
\ \n"
|
||||
description: |
|
||||
Create a new team.
|
||||
**Access policy**: administrator
|
||||
operationId: "TeamCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -1353,8 +1431,9 @@ paths:
|
|||
tags:
|
||||
- "teams"
|
||||
summary: "Inspect a team"
|
||||
description: "Retrieve details about a team. Access is only available for administrator\
|
||||
\ and leaders of that team. \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
Retrieve details about a team. Access is only available for administrator and leaders of that team.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -1398,8 +1477,9 @@ paths:
|
|||
tags:
|
||||
- "teams"
|
||||
summary: "Update a team"
|
||||
description: "Update a team. \n**Access policy**: administrator \
|
||||
\ \n"
|
||||
description: |
|
||||
Update a team.
|
||||
**Access policy**: administrator
|
||||
operationId: "TeamUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -1442,7 +1522,9 @@ paths:
|
|||
tags:
|
||||
- "teams"
|
||||
summary: "Remove a team"
|
||||
description: "Remove a team. \n**Access policy**: administrator \n"
|
||||
description: |
|
||||
Remove a team.
|
||||
**Access policy**: administrator
|
||||
operationId: "TeamDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
|
@ -1471,13 +1553,15 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/teams/{id}/memberships:
|
||||
get:
|
||||
tags:
|
||||
- "teams"
|
||||
summary: "Inspect a team memberships"
|
||||
description: "Inspect a team memberships. Access is only available for administrator\
|
||||
\ and leaders of that team. \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
Inspect a team memberships. Access is only available for administrator and leaders of that team.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamMembershipsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -1510,13 +1594,15 @@ paths:
|
|||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/team_memberships:
|
||||
get:
|
||||
tags:
|
||||
- "team_memberships"
|
||||
summary: "List team memberships"
|
||||
description: "List team memberships. Access is only available to administrators\
|
||||
\ and team leaders. \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
List team memberships. Access is only available to administrators and team leaders.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamMembershipList"
|
||||
produces:
|
||||
- "application/json"
|
||||
|
@ -1541,8 +1627,9 @@ paths:
|
|||
tags:
|
||||
- "team_memberships"
|
||||
summary: "Create a new team membership"
|
||||
description: "Create a new team memberships. Access is only available to administrators\
|
||||
\ leaders of the associated team. \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
Create a new team memberships. Access is only available to administrators leaders of the associated team.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamMembershipCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -1590,9 +1677,9 @@ paths:
|
|||
tags:
|
||||
- "team_memberships"
|
||||
summary: "Update a team membership"
|
||||
description: "Update a team membership. Access is only available to administrators\
|
||||
\ leaders of the associated team. \n**Access policy**: restricted \
|
||||
\ \n"
|
||||
description: |
|
||||
Update a team membership. Access is only available to administrators leaders of the associated team.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamMembershipUpdate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -1642,8 +1729,9 @@ paths:
|
|||
tags:
|
||||
- "team_memberships"
|
||||
summary: "Remove a team membership"
|
||||
description: "Remove a team membership. Access is only available to administrators\
|
||||
\ leaders of the associated team. \n**Access policy**: restricted \n"
|
||||
description: |
|
||||
Remove a team membership. Access is only available to administrators leaders of the associated team.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamMembershipDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
|
@ -1684,17 +1772,18 @@ paths:
|
|||
tags:
|
||||
- "templates"
|
||||
summary: "Retrieve App templates"
|
||||
description: "Retrieve App templates. \nYou can find more information about\
|
||||
\ the format at http://portainer.readthedocs.io/en/stable/templates.html \
|
||||
\ \n**Access policy**: authenticated \n"
|
||||
description: |
|
||||
Retrieve App templates.
|
||||
You can find more information about the format at http://portainer.readthedocs.io/en/stable/templates.html
|
||||
**Access policy**: authenticated
|
||||
operationId: "TemplateList"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- name: "key"
|
||||
in: "query"
|
||||
description: "Templates key. Valid values are 'container' or 'linuxserver.io'."
|
||||
required: true
|
||||
description: "Templates key. Valid values are 'container' or 'linuxserver.io'."
|
||||
type: "string"
|
||||
responses:
|
||||
200:
|
||||
|
@ -1780,7 +1869,7 @@ definitions:
|
|||
description: "Is analytics enabled"
|
||||
Version:
|
||||
type: "string"
|
||||
example: "1.14.0"
|
||||
example: "1.14.1"
|
||||
description: "Portainer API version"
|
||||
PublicSettingsInspectResponse:
|
||||
type: "object"
|
||||
|
@ -1799,8 +1888,8 @@ definitions:
|
|||
AuthenticationMethod:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Active authentication method for the Portainer instance. Valid\
|
||||
\ values are: 1 for managed or 2 for LDAP."
|
||||
description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
|
||||
|
||||
TLSConfiguration:
|
||||
type: "object"
|
||||
properties:
|
||||
|
@ -1824,14 +1913,14 @@ definitions:
|
|||
type: "string"
|
||||
example: "/data/tls/key.pem"
|
||||
description: "Path to the TLS client key file"
|
||||
|
||||
LDAPSearchSettings:
|
||||
type: "object"
|
||||
properties:
|
||||
BaseDN:
|
||||
type: "string"
|
||||
example: "dc=ldap,dc=domain,dc=tld"
|
||||
description: "The distinguished name of the element from which the LDAP server\
|
||||
\ will search for users"
|
||||
description: "The distinguished name of the element from which the LDAP server will search for users"
|
||||
Filter:
|
||||
type: "string"
|
||||
example: "(objectClass=account)"
|
||||
|
@ -1840,6 +1929,7 @@ definitions:
|
|||
type: "string"
|
||||
example: "uid"
|
||||
description: "LDAP attribute which denotes the username"
|
||||
|
||||
LDAPSettings:
|
||||
type: "object"
|
||||
properties:
|
||||
|
@ -1865,6 +1955,7 @@ definitions:
|
|||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/LDAPSearchSettings"
|
||||
|
||||
Settings:
|
||||
type: "object"
|
||||
properties:
|
||||
|
@ -1893,8 +1984,7 @@ definitions:
|
|||
AuthenticationMethod:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Active authentication method for the Portainer instance. Valid\
|
||||
\ values are: 1 for managed or 2 for LDAP."
|
||||
description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
|
||||
LDAPSettings:
|
||||
$ref: "#/definitions/LDAPSettings"
|
||||
Settings_BlackListedLabels:
|
||||
|
@ -2060,6 +2150,14 @@ definitions:
|
|||
type: "boolean"
|
||||
example: true
|
||||
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:
|
||||
type: "object"
|
||||
properties:
|
||||
|
@ -2091,6 +2189,14 @@ definitions:
|
|||
type: "boolean"
|
||||
example: true
|
||||
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:
|
||||
type: "object"
|
||||
properties:
|
||||
|
@ -2257,8 +2363,8 @@ definitions:
|
|||
SettingsUpdateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "AuthenticationMethod"
|
||||
- "TemplatesURL"
|
||||
- "AuthenticationMethod"
|
||||
properties:
|
||||
TemplatesURL:
|
||||
type: "string"
|
||||
|
@ -2285,8 +2391,7 @@ definitions:
|
|||
AuthenticationMethod:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Active authentication method for the Portainer instance. Valid\
|
||||
\ values are: 1 for managed or 2 for LDAP."
|
||||
description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
|
||||
LDAPSettings:
|
||||
$ref: "#/definitions/LDAPSettings"
|
||||
UserCreateRequest:
|
||||
|
@ -2383,12 +2488,13 @@ definitions:
|
|||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/TeamMembership"
|
||||
|
||||
TeamMembershipCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "Role"
|
||||
- "TeamID"
|
||||
- "UserID"
|
||||
- "TeamID"
|
||||
- "Role"
|
||||
properties:
|
||||
UserID:
|
||||
type: "integer"
|
||||
|
@ -2401,8 +2507,7 @@ definitions:
|
|||
Role:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Role for the user inside the team (1 for leader and 2 for regular\
|
||||
\ member)"
|
||||
description: "Role for the user inside the team (1 for leader and 2 for regular member)"
|
||||
TeamMembershipCreateResponse:
|
||||
type: "object"
|
||||
properties:
|
||||
|
@ -2417,9 +2522,9 @@ definitions:
|
|||
TeamMembershipUpdateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "Role"
|
||||
- "TeamID"
|
||||
- "UserID"
|
||||
- "TeamID"
|
||||
- "Role"
|
||||
properties:
|
||||
UserID:
|
||||
type: "integer"
|
||||
|
@ -2432,8 +2537,7 @@ definitions:
|
|||
Role:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Role for the user inside the team (1 for leader and 2 for regular\
|
||||
\ member)"
|
||||
description: "Role for the user inside the team (1 for leader and 2 for regular member)"
|
||||
SettingsLDAPCheckRequest:
|
||||
type: "object"
|
||||
properties:
|
||||
|
|
77
app/app.js
77
app/app.js
|
@ -23,6 +23,7 @@ angular.module('portainer', [
|
|||
'container',
|
||||
'containerConsole',
|
||||
'containerLogs',
|
||||
'containerStats',
|
||||
'serviceLogs',
|
||||
'containers',
|
||||
'createContainer',
|
||||
|
@ -31,14 +32,15 @@ angular.module('portainer', [
|
|||
'createSecret',
|
||||
'createService',
|
||||
'createVolume',
|
||||
'docker',
|
||||
'engine',
|
||||
'endpoint',
|
||||
'endpointAccess',
|
||||
'endpointInit',
|
||||
'endpoints',
|
||||
'events',
|
||||
'image',
|
||||
'images',
|
||||
'initAdmin',
|
||||
'initEndpoint',
|
||||
'main',
|
||||
'network',
|
||||
'networks',
|
||||
|
@ -53,8 +55,8 @@ angular.module('portainer', [
|
|||
'settings',
|
||||
'settingsAuthentication',
|
||||
'sidebar',
|
||||
'stats',
|
||||
'swarm',
|
||||
'swarmVisualizer',
|
||||
'task',
|
||||
'team',
|
||||
'teams',
|
||||
|
@ -63,7 +65,8 @@ angular.module('portainer', [
|
|||
'users',
|
||||
'userSettings',
|
||||
'volume',
|
||||
'volumes'])
|
||||
'volumes',
|
||||
'rzModule'])
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
|
||||
'use strict';
|
||||
|
||||
|
@ -157,8 +160,8 @@ angular.module('portainer', [
|
|||
url: '^/containers/:id/stats',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/stats/stats.html',
|
||||
controller: 'StatsController'
|
||||
templateUrl: 'app/components/containerStats/containerStats.html',
|
||||
controller: 'ContainerStatsController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
|
@ -321,12 +324,39 @@ angular.module('portainer', [
|
|||
}
|
||||
}
|
||||
})
|
||||
.state('docker', {
|
||||
url: '/docker/',
|
||||
.state('init', {
|
||||
abstract: true,
|
||||
url: '/init',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/docker/docker.html',
|
||||
controller: 'DockerController'
|
||||
template: '<div ui-view="content@"></div>'
|
||||
}
|
||||
}
|
||||
})
|
||||
.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@': {
|
||||
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', {
|
||||
url: '/events/',
|
||||
views: {
|
||||
|
@ -716,7 +737,7 @@ angular.module('portainer', [
|
|||
}
|
||||
})
|
||||
.state('swarm', {
|
||||
url: '/swarm/',
|
||||
url: '/swarm',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/swarm/swarm.html',
|
||||
|
@ -727,7 +748,21 @@ angular.module('portainer', [
|
|||
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) {
|
||||
EndpointProvider.initialize();
|
||||
|
|
|
@ -1,92 +1,38 @@
|
|||
<div class="page-wrapper">
|
||||
<!-- login box -->
|
||||
<div class="container simple-box">
|
||||
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
||||
<div class="col-sm-6 col-sm-offset-3">
|
||||
<!-- login 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">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
</div>
|
||||
<!-- !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 -->
|
||||
<div class="panel panel-default" ng-if="!initPassword">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- login form -->
|
||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
||||
<form class="simple-box-form form-horizontal">
|
||||
<!-- username input -->
|
||||
<div class="input-group">
|
||||
<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>
|
||||
<!-- !username input -->
|
||||
<!-- password input -->
|
||||
<div class="input-group">
|
||||
<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>
|
||||
<!-- !password input -->
|
||||
<!-- login button -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }}
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
||||
<div class="col-sm-12">
|
||||
<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>
|
||||
<span class="pull-left" style="margin: 5px;" ng-if="state.AuthenticationError">
|
||||
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<span class="small text-danger">{{ state.AuthenticationError }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !login button -->
|
||||
|
|
|
@ -1,113 +1,110 @@
|
|||
angular.module('auth', [])
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) {
|
||||
|
||||
$scope.authData = {
|
||||
username: 'admin',
|
||||
password: '',
|
||||
error: ''
|
||||
};
|
||||
$scope.initPasswordData = {
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
error: false
|
||||
};
|
||||
.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, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) {
|
||||
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
||||
if (!$scope.applicationState.application.authentication) {
|
||||
EndpointService.endpoints()
|
||||
.then(function success(data) {
|
||||
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.formValues = {
|
||||
Username: '',
|
||||
Password: ''
|
||||
};
|
||||
|
||||
$scope.authenticateUser = function() {
|
||||
$scope.authenticationError = false;
|
||||
var username = $sanitize($scope.authData.username);
|
||||
var password = $sanitize($scope.authData.password);
|
||||
Authentication.login(username, password)
|
||||
$scope.state = {
|
||||
AuthenticationError: ''
|
||||
};
|
||||
|
||||
function setActiveEndpointAndRedirectToDashboard(endpoint) {
|
||||
var endpointID = EndpointProvider.endpointID();
|
||||
if (!endpointID) {
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
}
|
||||
StateManager.updateEndpointState(true)
|
||||
.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) {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
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 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.';
|
||||
var endpoints = data;
|
||||
if (endpoints.length > 0) {
|
||||
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||
} else {
|
||||
$state.go('init.endpoint');
|
||||
}
|
||||
})
|
||||
.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();
|
||||
}]);
|
||||
|
|
|
@ -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="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="recreate()"><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-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()" 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>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -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> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > 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>
|
|
@ -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();
|
||||
}]);
|
|
@ -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-up"></span>
|
||||
</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>
|
||||
<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 }}">{{ container.Status }}</span>
|
||||
</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|containername|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: truncate_size}}</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="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
angular.module('containers', [])
|
||||
.controller('ContainersController', ['$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) {
|
||||
.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, LocalStorage) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
|
||||
$scope.state.displayAll = true;
|
||||
$scope.state.displayAll = LocalStorage.getFilterContainerShowAll();
|
||||
$scope.state.displayIP = false;
|
||||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = false;
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.truncate_size = 40;
|
||||
$scope.showMore = true;
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
|
@ -130,6 +133,7 @@ angular.module('containers', [])
|
|||
};
|
||||
|
||||
$scope.toggleGetAll = function () {
|
||||
LocalStorage.storeFilterContainerShowAll($scope.state.displayAll);
|
||||
update({all: $scope.state.displayAll ? 1 : 0});
|
||||
};
|
||||
|
||||
|
@ -161,6 +165,12 @@ angular.module('containers', [])
|
|||
batch($scope.containers, Container.remove, 'Removed');
|
||||
};
|
||||
|
||||
|
||||
$scope.truncateMore = function(size) {
|
||||
$scope.truncate_size = 80;
|
||||
$scope.showMore = false;
|
||||
};
|
||||
|
||||
$scope.confirmRemoveAction = function () {
|
||||
var isOneContainerRunning = false;
|
||||
angular.forEach($scope.containers, function (c) {
|
||||
|
@ -205,7 +215,7 @@ angular.module('containers', [])
|
|||
|
||||
if(container.Status === 'paused') {
|
||||
$scope.state.noPausedItemsSelected = false;
|
||||
} else if(container.Status === 'stopped' ||
|
||||
} else if(container.Status === 'stopped' ||
|
||||
container.Status === 'created') {
|
||||
$scope.state.noStoppedItemsSelected = false;
|
||||
} else if(container.Status === 'running') {
|
||||
|
|
|
@ -403,7 +403,6 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper,
|
|||
}
|
||||
|
||||
function loadFromContainerImageConfig(d) {
|
||||
// If no registry found, we let default DockerHub and let full image path
|
||||
var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image);
|
||||
RegistryService.retrieveRegistryFromRepository($scope.config.Image)
|
||||
.then(function success(data) {
|
||||
|
|
|
@ -21,24 +21,30 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Image configuration
|
||||
</div>
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry>
|
||||
<div ng-if="!formValues.Registry && fromContainer">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
|
||||
<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>
|
||||
<!-- !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 ng-if="formValues.Registry || !fromContainer">
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry>
|
||||
</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>
|
||||
<!-- !always-pull -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Ports configuration
|
||||
</div>
|
||||
|
@ -106,7 +112,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
angular.module('createNetwork', [])
|
||||
.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network', 'LabelHelper',
|
||||
function ($scope, $state, Notifications, Network, LabelHelper) {
|
||||
.controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
|
||||
function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
|
||||
|
||||
$scope.formValues = {
|
||||
DriverOptions: [],
|
||||
Subnet: '',
|
||||
Gateway: '',
|
||||
Labels: []
|
||||
Labels: [],
|
||||
AccessControlData: new AccessControlFormData()
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
formValidationError: ''
|
||||
};
|
||||
|
||||
$scope.availableNetworkDrivers = [];
|
||||
|
||||
$scope.config = {
|
||||
Driver: 'bridge',
|
||||
CheckDuplicate: true,
|
||||
|
@ -37,23 +45,6 @@ function ($scope, $state, Notifications, Network, LabelHelper) {
|
|||
$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) {
|
||||
if ($scope.formValues.Subnet) {
|
||||
var ipamConfig = {};
|
||||
|
@ -85,8 +76,66 @@ function ($scope, $state, Notifications, Network, LabelHelper) {
|
|||
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 () {
|
||||
var config = prepareConfiguration();
|
||||
createNetwork(config);
|
||||
$('#createResourceSpinner').show();
|
||||
|
||||
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();
|
||||
}]);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<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>
|
||||
<a ui-sref="networks">Networks</a> > Add network
|
||||
</rd-header-content>
|
||||
|
@ -39,8 +41,11 @@
|
|||
<!-- driver-input -->
|
||||
<div class="form-group">
|
||||
<label for="network_driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName">
|
||||
<div class="col-sm-11">
|
||||
<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>
|
||||
<!-- !driver-input -->
|
||||
|
@ -116,6 +121,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !internal -->
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
|
@ -124,7 +132,8 @@
|
|||
<div class="col-sm-12">
|
||||
<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>
|
||||
<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>
|
||||
<!-- !actions -->
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
angular.module('createSecret', [])
|
||||
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper',
|
||||
function ($scope, $state, Notifications, SecretService, LabelHelper) {
|
||||
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
|
||||
function ($scope, $state, Notifications, SecretService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
Data: '',
|
||||
Labels: [],
|
||||
encodeSecret: true
|
||||
encodeSecret: true,
|
||||
AccessControlData: new AccessControlFormData()
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
formValidationError: ''
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
|
@ -36,10 +42,38 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
|
|||
return config;
|
||||
}
|
||||
|
||||
function createSecret(config) {
|
||||
$('#createSecretSpinner').show();
|
||||
SecretService.create(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 () {
|
||||
$('#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) {
|
||||
var secretIdentifier = data.ID;
|
||||
var userId = userDetails.ID;
|
||||
return ResourceControlService.applyResourceControl('secret', secretIdentifier, userId, accessControlData, []);
|
||||
})
|
||||
.then(function success() {
|
||||
Notifications.success('Secret successfully created');
|
||||
$state.go('secrets', {}, {reload: true});
|
||||
})
|
||||
|
@ -47,12 +81,7 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
|
|||
Notifications.error('Failure', err, 'Unable to create secret');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#createSecretSpinner').hide();
|
||||
$('#createResourceSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
var config = prepareConfiguration();
|
||||
createSecret(config);
|
||||
};
|
||||
}]);
|
||||
|
|
|
@ -66,6 +66,9 @@
|
|||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels-->
|
||||
<!-- access-control -->
|
||||
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||
<!-- !access-control -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
|
@ -74,7 +77,8 @@
|
|||
<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>
|
||||
<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>
|
||||
<!-- !actions -->
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
|
||||
// See app/components/templates/templatesController.js as a reference.
|
||||
angular.module('createService', [])
|
||||
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper',
|
||||
function ($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, $timeout, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService) {
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
|
@ -28,13 +28,25 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
|||
UpdateOrder: 'stop-first',
|
||||
FailureAction: 'pause',
|
||||
Secrets: [],
|
||||
AccessControlData: new AccessControlFormData()
|
||||
AccessControlData: new AccessControlFormData(),
|
||||
CpuLimit: 0,
|
||||
CpuReservation: 0,
|
||||
MemoryLimit: 0,
|
||||
MemoryReservation: 0,
|
||||
MemoryLimitUnit: 'MB',
|
||||
MemoryReservationUnit: 'MB'
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
formValidationError: ''
|
||||
};
|
||||
|
||||
$scope.refreshSlider = function () {
|
||||
$timeout(function () {
|
||||
$scope.$broadcast('rzSliderForceRender');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addPortBinding = function() {
|
||||
$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() {
|
||||
var input = $scope.formValues;
|
||||
var config = {
|
||||
|
@ -232,7 +276,11 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
|||
ContainerSpec: {
|
||||
Mounts: []
|
||||
},
|
||||
Placement: {}
|
||||
Placement: {},
|
||||
Resources: {
|
||||
Limits: {},
|
||||
Reservations: {}
|
||||
}
|
||||
},
|
||||
Mode: {},
|
||||
EndpointSpec: {}
|
||||
|
@ -248,6 +296,8 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
|||
prepareUpdateConfig(config, input);
|
||||
prepareSecretConfig(config, input);
|
||||
preparePlacementConfig(config, input);
|
||||
prepareResourcesCpuConfig(config, input);
|
||||
prepareResourcesMemoryConfig(config, input);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
@ -305,16 +355,30 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
|||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
|
||||
$q.all({
|
||||
volumes: VolumeService.volumes(),
|
||||
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) {
|
||||
$scope.availableVolumes = data.volumes;
|
||||
$scope.availableNetworks = data.networks;
|
||||
$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) {
|
||||
Notifications.error('Failure', err, 'Unable to initialize view');
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
<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" 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>
|
||||
<!-- tab-content -->
|
||||
<div class="tab-content">
|
||||
|
@ -442,9 +442,9 @@
|
|||
<!-- tab-secrets -->
|
||||
<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-placement -->
|
||||
<div class="tab-pane" id="placement" ng-include="'app/components/createService/includes/placement.html'"></div>
|
||||
<!-- !tab-placement -->
|
||||
<!-- tab-resources-placement -->
|
||||
<div class="tab-pane" id="resources-placement" ng-include="'app/components/createService/includes/resources-placement.html'"></div>
|
||||
<!-- !tab-resources-placement -->
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -125,9 +125,6 @@
|
|||
<div class="widget-icon blue pull-left">
|
||||
<i class="fa fa-cubes"></i>
|
||||
</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="comment">Volumes</div>
|
||||
</rd-widget-body>
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Configuration
|
||||
</div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||
|
@ -42,73 +45,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-public-url-input -->
|
||||
<!-- tls-checkbox -->
|
||||
<div class="form-group" ng-if="endpointType === 'remote'">
|
||||
<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="endpoint.TLS"><i></i>
|
||||
</label>
|
||||
<!-- endpoint-security -->
|
||||
<div ng-if="endpointType === 'remote'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Security
|
||||
</div>
|
||||
<por-endpoint-security form-data="formValues.SecurityFormData" endpoint="endpoint"></por-endpoint-security>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-certs -->
|
||||
<div ng-if="endpoint.TLS">
|
||||
<!-- ca-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSCACert !== endpoint.TLSCACert">{{ formValues.TLSCACert.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSCACert && formValues.TLSCACert === endpoint.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ca-input -->
|
||||
<!-- cert-input -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSCert !== endpoint.TLSCert">{{ formValues.TLSCert.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSCert && formValues.TLSCert === endpoint.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cert-input -->
|
||||
<!-- key-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
<span ng-if="formValues.TLSKey !== endpoint.TLSKey">{{ formValues.TLSKey.name }}</span>
|
||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSKey && formValues.TLSKey === endpoint.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !key-input -->
|
||||
</div>
|
||||
<!-- !tls-certs -->
|
||||
<!-- !endpoint-security -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="updateEndpoint()">Update endpoint</button>
|
||||
<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>
|
||||
<i id="updateEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</span>
|
||||
<i id="updateResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -7,35 +7,41 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications)
|
|||
}
|
||||
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
TLSCACert: null,
|
||||
TLSCert: null,
|
||||
TLSKey: null
|
||||
SecurityFormData: new EndpointSecurityFormData()
|
||||
};
|
||||
|
||||
$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 = {
|
||||
name: $scope.endpoint.Name,
|
||||
URL: $scope.endpoint.URL,
|
||||
PublicURL: $scope.endpoint.PublicURL,
|
||||
TLS: $scope.endpoint.TLS,
|
||||
TLSCACert: $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null,
|
||||
TLSCert: $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null,
|
||||
TLSKey: $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null,
|
||||
name: endpoint.Name,
|
||||
URL: endpoint.URL,
|
||||
PublicURL: endpoint.PublicURL,
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||
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
|
||||
};
|
||||
|
||||
EndpointService.updateEndpoint(ID, endpointParams)
|
||||
$('updateResourceSpinner').show();
|
||||
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||
$state.go('endpoints');
|
||||
}, function error(err) {
|
||||
$scope.state.error = err.msg;
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint');
|
||||
}, function update(evt) {
|
||||
if (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();
|
||||
EndpointService.endpoint($stateParams.id).then(function success(data) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
$scope.endpoint = data;
|
||||
if (data.URL.indexOf('unix://') === 0) {
|
||||
EndpointService.endpoint($stateParams.id)
|
||||
.then(function success(data) {
|
||||
var endpoint = data;
|
||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||
$scope.endpoint = endpoint;
|
||||
|
||||
if (endpoint.URL.indexOf('unix://') === 0) {
|
||||
$scope.endpointType = 'local';
|
||||
} else {
|
||||
$scope.endpointType = 'remote';
|
||||
}
|
||||
$scope.endpoint.URL = $filter('stripprotocol')(data.URL);
|
||||
$scope.formValues.TLSCACert = data.TLSCACert;
|
||||
$scope.formValues.TLSCert = data.TLSCert;
|
||||
$scope.formValues.TLSKey = data.TLSKey;
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
getEndpoint($stateParams.id);
|
||||
initView();
|
||||
}]);
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
}]);
|
|
@ -60,75 +60,21 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !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="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>
|
||||
<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>
|
||||
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
<!-- !tls-checkbox -->
|
||||
<!-- tls-certs -->
|
||||
<div ng-if="formValues.TLS">
|
||||
<!-- ca-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCACert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !ca-input -->
|
||||
<!-- cert-input -->
|
||||
<div class="form-group">
|
||||
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSCert.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !cert-input -->
|
||||
<!-- key-input -->
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label text-left">TLS key</label>
|
||||
<div class="col-sm-10">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ formValues.TLSKey.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !key-input -->
|
||||
</div>
|
||||
<!-- !tls-certs -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="addEndpoint()">Add endpoint</button>
|
||||
<i id="createEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
@ -191,7 +137,7 @@
|
|||
<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>
|
||||
</span>
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!endpoints">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
|
|
|
@ -2,7 +2,6 @@ angular.module('endpoints', [])
|
|||
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination',
|
||||
function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagination) {
|
||||
$scope.state = {
|
||||
error: '',
|
||||
uploadInProgress: false,
|
||||
selectedItemCount: 0,
|
||||
pagination_count: Pagination.getPaginationCount('endpoints')
|
||||
|
@ -14,10 +13,7 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
|
|||
Name: '',
|
||||
URL: '',
|
||||
PublicURL: '',
|
||||
TLS: false,
|
||||
TLSCACert: null,
|
||||
TLSCert: null,
|
||||
TLSKey: null
|
||||
SecurityFormData: new EndpointSecurityFormData()
|
||||
};
|
||||
|
||||
$scope.order = function(sortType) {
|
||||
|
@ -47,23 +43,28 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
|
|||
};
|
||||
|
||||
$scope.addEndpoint = function() {
|
||||
$scope.state.error = '';
|
||||
var name = $scope.formValues.Name;
|
||||
var URL = $scope.formValues.URL;
|
||||
var PublicURL = $scope.formValues.PublicURL;
|
||||
if (PublicURL === '') {
|
||||
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, false).then(function success(data) {
|
||||
|
||||
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 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);
|
||||
$state.reload();
|
||||
}, function error(err) {
|
||||
$scope.state.uploadInProgress = false;
|
||||
$scope.state.error = err.msg;
|
||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||
}, function update(evt) {
|
||||
if (evt.upload) {
|
||||
$scope.state.uploadInProgress = evt.upload;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<rd-header>
|
||||
<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>
|
||||
</a>
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
|
@ -1,9 +1,7 @@
|
|||
angular.module('docker', [])
|
||||
.controller('DockerController', ['$q', '$scope', 'SystemService', 'Notifications',
|
||||
angular.module('engine', [])
|
||||
.controller('EngineController', ['$q', '$scope', 'SystemService', 'Notifications',
|
||||
function ($q, $scope, SystemService, Notifications) {
|
||||
$scope.info = {};
|
||||
$scope.version = {};
|
||||
|
||||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all({
|
||||
|
@ -15,6 +13,8 @@ function ($q, $scope, SystemService, Notifications) {
|
|||
$scope.info = data.info;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.info = {};
|
||||
$scope.version = {};
|
||||
Notifications.error('Failure', err, 'Unable to retrieve engine details');
|
||||
})
|
||||
.finally(function final() {
|
|
@ -70,7 +70,7 @@
|
|||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</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">
|
||||
All
|
||||
</label>
|
||||
|
@ -121,12 +121,12 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<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"
|
||||
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
|
||||
</span>
|
||||
</td>
|
||||
|
|
|
@ -95,7 +95,7 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService)
|
|||
$('#loadImagesSpinner').show();
|
||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
ImageService.images(apiVersion >= 1.25 && endpointProvider !== 'DOCKER_SWARM' && endpointProvider !== 'VMWARE_VIC')
|
||||
ImageService.images(true)
|
||||
.then(function success(data) {
|
||||
$scope.images = data;
|
||||
})
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
}]);
|
|
@ -48,6 +48,15 @@
|
|||
</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="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('network', [])
|
||||
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'Container', 'ContainerHelper', 'Notifications',
|
||||
function ($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, NetworkService, Container, ContainerHelper, Notifications) {
|
||||
|
||||
$scope.removeNetwork = function removeNetwork(networkId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
|
@ -82,7 +82,7 @@ function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHe
|
|||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.get({id: $stateParams.id}).$promise
|
||||
NetworkService.network($stateParams.id)
|
||||
.then(function success(data) {
|
||||
$scope.network = data;
|
||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||
|
|
|
@ -8,46 +8,6 @@
|
|||
<rd-header-content>Networks</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title="Add a 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="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
|
@ -66,6 +26,7 @@
|
|||
<rd-widget-taskbar classes="col-lg-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
<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 class="pull-right">
|
||||
<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)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Name')">
|
||||
<a ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Id')">
|
||||
<a ng-click="order('Id')">
|
||||
Id
|
||||
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Scope')">
|
||||
<a ng-click="order('Scope')">
|
||||
Scope
|
||||
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('Driver')">
|
||||
<a ng-click="order('Driver')">
|
||||
Driver
|
||||
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Driver')">
|
||||
<a ng-click="order('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-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')">
|
||||
<a ng-click="order('IPAM.Config[0].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-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
|
||||
<a ng-click="order('IPAM.Config[0].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-up"></span>
|
||||
</a>
|
||||
</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -140,12 +108,18 @@
|
|||
<td>{{ network.IPAM.Driver }}</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 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 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 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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -1,51 +1,17 @@
|
|||
angular.module('networks', [])
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Notifications', 'Pagination',
|
||||
function ($scope, $state, Network, Notifications, Pagination) {
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'NetworkService', 'Notifications', 'Pagination',
|
||||
function ($scope, $state, Network, NetworkService, Notifications, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.state.advancedSettings = false;
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = false;
|
||||
$scope.config = {
|
||||
Name: ''
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
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.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
|
@ -99,13 +65,17 @@ function ($scope, $state, Network, Notifications, Pagination) {
|
|||
|
||||
function initView() {
|
||||
$('#loadNetworksSpinner').show();
|
||||
Network.query({}, function (d) {
|
||||
$scope.networks = d;
|
||||
$('#loadNetworksSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadNetworksSpinner').hide();
|
||||
Notifications.error('Failure', e, 'Unable to retrieve networks');
|
||||
|
||||
NetworkService.networks(true, true, true, true)
|
||||
.then(function success(data) {
|
||||
$scope.networks = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.networks = [];
|
||||
Notifications.error('Failure', err, 'Unable to retrieve networks');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadNetworksSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -53,3 +53,12 @@
|
|||
</rd-widget>
|
||||
</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 -->
|
||||
|
|
|
@ -30,31 +30,44 @@
|
|||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="secrets" ng-click="order('Name')">
|
||||
<a ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="secrets" ng-click="order('CreatedAt')">
|
||||
<a ng-click="order('CreatedAt')">
|
||||
Created at
|
||||
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</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>
|
||||
<tbody>
|
||||
<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><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></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 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 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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -6,31 +6,77 @@
|
|||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>CPU limits</td>
|
||||
<td ng-if="service.LimitNanoCPUs">
|
||||
{{ service.LimitNanoCPUs / 1000000000 }}
|
||||
<td style="vertical-align : middle;">
|
||||
Memory reservation (MB)
|
||||
</td>
|
||||
<td ng-if="!service.LimitNanoCPUs">None</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory limits</td>
|
||||
<td ng-if="service.LimitMemoryBytes">{{service.LimitMemoryBytes|humansize}}</td>
|
||||
<td ng-if="!service.LimitMemoryBytes">None</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU reservation</td>
|
||||
<td ng-if="service.ReservationNanoCPUs">
|
||||
{{service.ReservationNanoCPUs / 1000000000}}
|
||||
<td>
|
||||
<input class="input-sm" type="number" step="0.125" min="0" ng-model="service.ReservationMemoryBytes" ng-change="updateServiceAttribute(service, 'ReservationMemoryBytes')" ng-disabled="isUpdating"/>
|
||||
</td>
|
||||
<td style="vertical-align : middle;">
|
||||
<p class="small text-muted">
|
||||
Minimum memory available on a node to run a task (set to 0 for unlimited)
|
||||
</p>
|
||||
</td>
|
||||
<td ng-if="!service.ReservationNanoCPUs">None</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory reservation</td>
|
||||
<td ng-if="service.ReservationMemoryBytes">{{service.ReservationMemoryBytes|humansize}}</td>
|
||||
<td ng-if="!service.ReservationMemoryBytes">None</td>
|
||||
<td style="vertical-align : middle;">
|
||||
Memory limit (MB)
|
||||
</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>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</div>
|
||||
|
|
|
@ -204,14 +204,19 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
|||
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints);
|
||||
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 = {
|
||||
Limits: {
|
||||
NanoCPUs: service.LimitNanoCPUs,
|
||||
MemoryBytes: service.LimitMemoryBytes
|
||||
NanoCPUs: service.LimitNanoCPUs * 1000000000,
|
||||
MemoryBytes: memoryLimit
|
||||
},
|
||||
Reservations: {
|
||||
NanoCPUs: service.ReservationNanoCPUs,
|
||||
MemoryBytes: service.ReservationMemoryBytes
|
||||
NanoCPUs: service.ReservationNanoCPUs * 1000000000,
|
||||
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) {
|
||||
$('#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({});
|
||||
initView();
|
||||
}, function (e) {
|
||||
|
@ -288,6 +297,13 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
|||
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() {
|
||||
$('#loadingViewSpinner').show();
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
|
@ -299,6 +315,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
|||
$scope.lastVersion = service.Version;
|
||||
}
|
||||
|
||||
transformResources(service);
|
||||
translateServiceArrays(service);
|
||||
$scope.service = service;
|
||||
originalService = angular.copy(service);
|
||||
|
@ -314,6 +331,19 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
|||
$scope.nodes = data.nodes;
|
||||
$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() {
|
||||
$anchorScroll();
|
||||
});
|
||||
|
|
|
@ -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'">
|
||||
<a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a>
|
||||
</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>
|
||||
</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')">
|
||||
<a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
||||
</li>
|
||||
<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 class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
||||
<span>Portainer settings</span>
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Container stats"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > 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>
|
|
@ -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();
|
||||
}
|
||||
}]);
|
|
@ -58,6 +58,13 @@
|
|||
<td>Go version</td>
|
||||
<td>{{ docker.GoVersion }}</td>
|
||||
</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>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
|
@ -216,7 +223,10 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<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.CPUs / 1000000000 }}</td>
|
||||
<td>{{ node.Memory|humansize }}</td>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('swarm', [])
|
||||
.controller('SwarmController', ['$q', '$scope', 'SystemService', 'NodeService', 'Pagination', 'Notifications',
|
||||
function ($q, $scope, SystemService, NodeService, Pagination, Notifications) {
|
||||
.controller('SwarmController', ['$q', '$scope', 'SystemService', 'NodeService', 'Pagination', 'Notifications', 'StateManager', 'Authentication',
|
||||
function ($q, $scope, SystemService, NodeService, Pagination, Notifications, StateManager, Authentication) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
|
||||
$scope.sortType = 'Spec.Role';
|
||||
|
@ -73,6 +73,13 @@ function ($q, $scope, SystemService, NodeService, Pagination, Notifications) {
|
|||
|
||||
function initView() {
|
||||
$('#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;
|
||||
$q.all({
|
||||
version: SystemService.version(),
|
||||
|
|
|
@ -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> > <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>
|
|
@ -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();
|
||||
}]);
|
|
@ -27,7 +27,7 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
|
|
|
@ -151,7 +151,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer
|
|||
|
||||
$q.all({
|
||||
templates: TemplateService.getTemplates(templatesKey),
|
||||
containers: ContainerService.getContainers(0),
|
||||
containers: ContainerService.containers(0),
|
||||
volumes: VolumeService.getVolumes(),
|
||||
networks: NetworkService.networks(
|
||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||
|
|
|
@ -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;
|
||||
}]);
|
|
@ -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: '<'
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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();
|
||||
}]);
|
|
@ -0,0 +1,7 @@
|
|||
function EndpointSecurityFormData() {
|
||||
this.TLS = false;
|
||||
this.TLSMode = 'tls_client_ca';
|
||||
this.TLSCACert = null;
|
||||
this.TLSCert = null;
|
||||
this.TLSKey = null;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
angular.module('portainer').component('porSlider', {
|
||||
templateUrl: 'app/directives/slider/porSlider.html',
|
||||
controller: 'porSliderController',
|
||||
bindings: {
|
||||
model: '=',
|
||||
onChange: '&',
|
||||
floor: '<',
|
||||
ceil: '<',
|
||||
step: '<',
|
||||
precision: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
<div>
|
||||
<rzslider rz-slider-options="$ctrl.options" rz-slider-model="$ctrl.model"></rzslider>
|
||||
</div>
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
});
|
|
@ -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 () {
|
||||
'use strict';
|
||||
return function (text) {
|
||||
|
|
|
@ -119,6 +119,5 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
|
|||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
};
|
||||
}]);
|
||||
|
|
|
@ -16,11 +16,20 @@ function TemplateViewModel(data) {
|
|||
this.Volumes = [];
|
||||
if (data.volumes) {
|
||||
this.Volumes = data.volumes.map(function (v) {
|
||||
return {
|
||||
readOnly: false,
|
||||
containerPath: v,
|
||||
// @DEPRECATED: New volume definition introduced
|
||||
// via https://github.com/portainer/portainer/pull/1154
|
||||
var volume = {
|
||||
readOnly: v.readonly || false,
|
||||
containerPath: v.container || v,
|
||||
type: 'auto'
|
||||
};
|
||||
|
||||
if (v.bind) {
|
||||
volume.name = v.bind;
|
||||
volume.type = 'bind';
|
||||
}
|
||||
|
||||
return volume;
|
||||
});
|
||||
}
|
||||
this.Ports = [];
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -3,8 +3,8 @@ function ImageViewModel(data) {
|
|||
this.Tag = data.Tag;
|
||||
this.Repository = data.Repository;
|
||||
this.Created = data.Created;
|
||||
this.Containers = data.dataUsage ? data.dataUsage.Containers : 0;
|
||||
this.Checked = false;
|
||||
this.RepoTags = data.RepoTags;
|
||||
this.VirtualSize = data.VirtualSize;
|
||||
this.ContainerCount = data.ContainerCount;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,4 +5,10 @@ function SecretViewModel(data) {
|
|||
this.Version = data.Version.Index;
|
||||
this.Name = data.Spec.Name;
|
||||
this.Labels = data.Spec.Labels;
|
||||
|
||||
if (data.Portainer) {
|
||||
if (data.Portainer.ResourceControl) {
|
||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,14 @@ angular.module('portainer.rest')
|
|||
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
||||
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
||||
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: {
|
||||
method: 'POST', params: {id: '@id', action: 'start'},
|
||||
transformResponse: genericHandler
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
|
@ -20,6 +20,8 @@ angular.module('portainer.services')
|
|||
name: endpointParams.name,
|
||||
PublicURL: endpointParams.PublicURL,
|
||||
TLS: endpointParams.TLS,
|
||||
TLSSkipVerify: endpointParams.TLSSkipVerify,
|
||||
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
|
||||
authorizedUsers: endpointParams.authorizedUsers
|
||||
};
|
||||
if (endpointParams.type && endpointParams.URL) {
|
||||
|
@ -55,18 +57,20 @@ angular.module('portainer.services')
|
|||
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 = {
|
||||
Name: name,
|
||||
URL: 'tcp://' + URL,
|
||||
PublicURL: PublicURL,
|
||||
TLS: TLS
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify
|
||||
};
|
||||
var deferred = $q.defer();
|
||||
Endpoints.create({}, endpoint).$promise
|
||||
.then(function success(data) {
|
||||
var endpointID = data.Id;
|
||||
if (TLS) {
|
||||
if (!TLSSkipVerify || !TLSSkipClientVerify) {
|
||||
deferred.notify({upload: true});
|
||||
FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
.then(function success() {
|
||||
|
|
|
@ -134,5 +134,26 @@ angular.module('portainer.services')
|
|||
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;
|
||||
}]);
|
||||
|
|
|
@ -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;
|
||||
}]);
|
|
@ -3,7 +3,22 @@ angular.module('portainer.services')
|
|||
'use strict';
|
||||
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();
|
||||
Container.query({ all: all }).$promise
|
||||
.then(function success(data) {
|
||||
|
@ -11,7 +26,7 @@ angular.module('portainer.services')
|
|||
deferred.resolve(containers);
|
||||
})
|
||||
.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;
|
||||
};
|
||||
|
@ -105,5 +120,35 @@ angular.module('portainer.services')
|
|||
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;
|
||||
}]);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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';
|
||||
var service = {};
|
||||
|
||||
|
@ -24,17 +24,23 @@ angular.module('portainer.services')
|
|||
var deferred = $q.defer();
|
||||
|
||||
$q.all({
|
||||
dataUsage: withUsage ? SystemService.dataUsage() : { Images: [] },
|
||||
containers: withUsage ? ContainerService.containers(1) : [],
|
||||
images: Image.query({}).$promise
|
||||
})
|
||||
.then(function success(data) {
|
||||
var images = data.images.map(function(item) {
|
||||
item.dataUsage = data.dataUsage.Images.find(function(usage) {
|
||||
return item.Id === usage.Id;
|
||||
});
|
||||
var containers = data.containers;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
deferred.resolve(images);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -3,6 +3,35 @@ angular.module('portainer.services')
|
|||
'use strict';
|
||||
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) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
|
@ -23,6 +52,8 @@ angular.module('portainer.services')
|
|||
if (globalNetworks && network.Scope === 'global') {
|
||||
return network;
|
||||
}
|
||||
}).map(function (item) {
|
||||
return new NetworkViewModel(item);
|
||||
});
|
||||
|
||||
deferred.resolve(filteredNetworks);
|
||||
|
|
|
@ -3,7 +3,7 @@ angular.module('portainer.services')
|
|||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.nodes = function(id) {
|
||||
service.nodes = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Node.query({}).$promise
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue