mirror of https://github.com/portainer/portainer
commit
1bccd521f8
|
@ -2,7 +2,7 @@ package bolt
|
||||||
|
|
||||||
import "github.com/portainer/portainer"
|
import "github.com/portainer/portainer"
|
||||||
|
|
||||||
func (m *Migrator) updateSettingsToVersion3() error {
|
func (m *Migrator) updateSettingsToDBVersion3() error {
|
||||||
legacySettings, err := m.SettingsService.Settings()
|
legacySettings, err := m.SettingsService.Settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -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 {
|
func (m *Migrator) Migrate() error {
|
||||||
|
|
||||||
// Portainer < 1.12
|
// Portainer < 1.12
|
||||||
if m.CurrentDBVersion == 0 {
|
if m.CurrentDBVersion < 1 {
|
||||||
err := m.updateAdminUserToDBVersion1()
|
err := m.updateAdminUserToDBVersion1()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -38,7 +38,7 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Portainer 1.12.x
|
// Portainer 1.12.x
|
||||||
if m.CurrentDBVersion == 1 {
|
if m.CurrentDBVersion < 2 {
|
||||||
err := m.updateResourceControlsToDBVersion2()
|
err := m.updateResourceControlsToDBVersion2()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -50,8 +50,16 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Portainer 1.13.x
|
// Portainer 1.13.x
|
||||||
if m.CurrentDBVersion == 2 {
|
if m.CurrentDBVersion < 3 {
|
||||||
err := m.updateSettingsToVersion3()
|
err := m.updateSettingsToDBVersion3()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Portainer 1.14.0
|
||||||
|
if m.CurrentDBVersion < 4 {
|
||||||
|
err := m.updateEndpointsToDBVersion4()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||||
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
||||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(),
|
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
|
||||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||||
|
|
|
@ -191,12 +191,15 @@ func main() {
|
||||||
}
|
}
|
||||||
if len(endpoints) == 0 {
|
if len(endpoints) == 0 {
|
||||||
endpoint := &portainer.Endpoint{
|
endpoint := &portainer.Endpoint{
|
||||||
Name: "primary",
|
Name: "primary",
|
||||||
URL: *flags.Endpoint,
|
URL: *flags.Endpoint,
|
||||||
TLS: *flags.TLSVerify,
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLSCACertPath: *flags.TLSCacert,
|
TLS: *flags.TLSVerify,
|
||||||
TLSCertPath: *flags.TLSCert,
|
TLSSkipVerify: false,
|
||||||
TLSKeyPath: *flags.TLSKey,
|
TLSCACertPath: *flags.TLSCacert,
|
||||||
|
TLSCertPath: *flags.TLSCert,
|
||||||
|
TLSKeyPath: *flags.TLSKey,
|
||||||
|
},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
}
|
}
|
||||||
|
@ -245,7 +248,7 @@ func main() {
|
||||||
SSLKey: *flags.SSLKey,
|
SSLKey: *flags.SSLKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||||
err = server.Start()
|
err = server.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|
|
@ -22,6 +22,16 @@ type (
|
||||||
endpointsToUpdate []*portainer.Endpoint
|
endpointsToUpdate []*portainer.Endpoint
|
||||||
endpointsToDelete []*portainer.Endpoint
|
endpointsToDelete []*portainer.Endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileEndpoint struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
URL string `json:"URL"`
|
||||||
|
TLS bool `json:"TLS,omitempty"`
|
||||||
|
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
|
||||||
|
TLSCACert string `json:"TLSCACert,omitempty"`
|
||||||
|
TLSCert string `json:"TLSCert,omitempty"`
|
||||||
|
TLSKey string `json:"TLSKey,omitempty"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -55,6 +65,28 @@ func isValidEndpoint(endpoint *portainer.Endpoint) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint {
|
||||||
|
convertedEndpoints := make([]portainer.Endpoint, 0)
|
||||||
|
|
||||||
|
for _, e := range fileEndpoints {
|
||||||
|
endpoint := portainer.Endpoint{
|
||||||
|
Name: e.Name,
|
||||||
|
URL: e.URL,
|
||||||
|
TLSConfig: portainer.TLSConfiguration{},
|
||||||
|
}
|
||||||
|
if e.TLS {
|
||||||
|
endpoint.TLSConfig.TLS = true
|
||||||
|
endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify
|
||||||
|
endpoint.TLSConfig.TLSCACertPath = e.TLSCACert
|
||||||
|
endpoint.TLSConfig.TLSCertPath = e.TLSCert
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = e.TLSKey
|
||||||
|
}
|
||||||
|
convertedEndpoints = append(convertedEndpoints, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertedEndpoints
|
||||||
|
}
|
||||||
|
|
||||||
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
|
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
|
||||||
for idx, v := range endpoints {
|
for idx, v := range endpoints {
|
||||||
if endpoint.Name == v.Name && isValidEndpoint(&v) {
|
if endpoint.Name == v.Name && isValidEndpoint(&v) {
|
||||||
|
@ -66,22 +98,25 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint
|
||||||
|
|
||||||
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
|
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
|
||||||
var endpoint *portainer.Endpoint
|
var endpoint *portainer.Endpoint
|
||||||
if original.URL != updated.URL || original.TLS != updated.TLS ||
|
if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS ||
|
||||||
(updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) ||
|
(updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) ||
|
||||||
(updated.TLS && original.TLSCertPath != updated.TLSCertPath) ||
|
(updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) ||
|
||||||
(updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) {
|
(updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) ||
|
||||||
|
(updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) {
|
||||||
endpoint = original
|
endpoint = original
|
||||||
endpoint.URL = updated.URL
|
endpoint.URL = updated.URL
|
||||||
if updated.TLS {
|
if updated.TLSConfig.TLS {
|
||||||
endpoint.TLS = true
|
endpoint.TLSConfig.TLS = true
|
||||||
endpoint.TLSCACertPath = updated.TLSCACertPath
|
endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify
|
||||||
endpoint.TLSCertPath = updated.TLSCertPath
|
endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath
|
||||||
endpoint.TLSKeyPath = updated.TLSKeyPath
|
endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath
|
||||||
} else {
|
} else {
|
||||||
endpoint.TLS = false
|
endpoint.TLSConfig.TLS = false
|
||||||
endpoint.TLSCACertPath = ""
|
endpoint.TLSConfig.TLSSkipVerify = false
|
||||||
endpoint.TLSCertPath = ""
|
endpoint.TLSConfig.TLSCACertPath = ""
|
||||||
endpoint.TLSKeyPath = ""
|
endpoint.TLSConfig.TLSCertPath = ""
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return endpoint
|
return endpoint
|
||||||
|
@ -141,7 +176,7 @@ func (job endpointSyncJob) Sync() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileEndpoints []portainer.Endpoint
|
var fileEndpoints []fileEndpoint
|
||||||
err = json.Unmarshal(data, &fileEndpoints)
|
err = json.Unmarshal(data, &fileEndpoints)
|
||||||
if endpointSyncError(err, job.logger) {
|
if endpointSyncError(err, job.logger) {
|
||||||
return err
|
return err
|
||||||
|
@ -156,7 +191,9 @@ func (job endpointSyncJob) Sync() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sync := job.prepareSyncData(storedEndpoints, fileEndpoints)
|
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
|
||||||
|
|
||||||
|
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
||||||
if sync.requireSync() {
|
if sync.requireSync() {
|
||||||
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
||||||
if endpointSyncError(err, job.logger) {
|
if endpointSyncError(err, job.logger) {
|
||||||
|
|
|
@ -4,31 +4,38 @@ import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
||||||
func CreateTLSConfiguration(caCertPath, certPath, keyPath string, skipTLSVerify bool) (*tls.Config, error) {
|
func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) {
|
||||||
|
TLSConfig := &tls.Config{}
|
||||||
|
|
||||||
config := &tls.Config{}
|
if config.TLS {
|
||||||
|
if config.TLSCertPath != "" && config.TLSKeyPath != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(config.TLSCertPath, config.TLSKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if certPath != "" && keyPath != "" {
|
TLSConfig.Certificates = []tls.Certificate{cert}
|
||||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
config.Certificates = []tls.Certificate{cert}
|
|
||||||
|
if !config.TLSSkipVerify {
|
||||||
|
caCert, err := ioutil.ReadFile(config.TLSCACertPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
caCertPool.AppendCertsFromPEM(caCert)
|
||||||
|
|
||||||
|
TLSConfig.RootCAs = caCertPool
|
||||||
|
}
|
||||||
|
|
||||||
|
TLSConfig.InsecureSkipVerify = config.TLSSkipVerify
|
||||||
}
|
}
|
||||||
|
|
||||||
if caCertPath != "" {
|
return TLSConfig, nil
|
||||||
caCert, err := ioutil.ReadFile(caCertPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
caCertPool := x509.NewCertPool()
|
|
||||||
caCertPool.AppendCertsFromPEM(caCert)
|
|
||||||
config.RootCAs = caCertPool
|
|
||||||
}
|
|
||||||
|
|
||||||
config.InsecureSkipVerify = skipTLSVerify
|
|
||||||
return config, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,10 @@ const (
|
||||||
const (
|
const (
|
||||||
ErrUserNotFound = Error("User not found")
|
ErrUserNotFound = Error("User not found")
|
||||||
ErrUserAlreadyExists = Error("User already exists")
|
ErrUserAlreadyExists = Error("User already exists")
|
||||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed.")
|
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
||||||
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
|
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
|
||||||
|
ErrCannotRemoveAdmin = Error("Cannot remove the default administrator account")
|
||||||
|
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Team errors.
|
// Team errors.
|
||||||
|
|
|
@ -95,7 +95,7 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
|
||||||
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
|
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
|
// DeleteTLSFiles deletes a folder in the TLS store path.
|
||||||
func (service *Service) DeleteTLSFiles(folder string) error {
|
func (service *Service) DeleteTLSFiles(folder string) error {
|
||||||
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
||||||
err := os.RemoveAll(storePath)
|
err := os.RemoveAll(storePath)
|
||||||
|
@ -105,6 +105,29 @@ func (service *Service) DeleteTLSFiles(folder string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteTLSFile deletes a specific TLS file from a folder.
|
||||||
|
func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileType) error {
|
||||||
|
var fileName string
|
||||||
|
switch fileType {
|
||||||
|
case portainer.TLSFileCA:
|
||||||
|
fileName = TLSCACertFile
|
||||||
|
case portainer.TLSFileCert:
|
||||||
|
fileName = TLSCertFile
|
||||||
|
case portainer.TLSFileKey:
|
||||||
|
fileName = TLSKeyFile
|
||||||
|
default:
|
||||||
|
return portainer.ErrUndefinedTLSFileType
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
|
||||||
|
|
||||||
|
err := os.Remove(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
|
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
|
||||||
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
|
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
|
||||||
path := path.Join(service.fileStorePath, name)
|
path := path.Join(service.fileStorePath, name)
|
||||||
|
|
|
@ -57,10 +57,12 @@ func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManag
|
||||||
|
|
||||||
type (
|
type (
|
||||||
postEndpointsRequest struct {
|
postEndpointsRequest struct {
|
||||||
Name string `valid:"required"`
|
Name string `valid:"required"`
|
||||||
URL string `valid:"required"`
|
URL string `valid:"required"`
|
||||||
PublicURL string `valid:"-"`
|
PublicURL string `valid:"-"`
|
||||||
TLS bool
|
TLS bool
|
||||||
|
TLSSkipVerify bool
|
||||||
|
TLSSkipClientVerify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
postEndpointsResponse struct {
|
postEndpointsResponse struct {
|
||||||
|
@ -73,10 +75,12 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
putEndpointsRequest struct {
|
putEndpointsRequest struct {
|
||||||
Name string `valid:"-"`
|
Name string `valid:"-"`
|
||||||
URL string `valid:"-"`
|
URL string `valid:"-"`
|
||||||
PublicURL string `valid:"-"`
|
PublicURL string `valid:"-"`
|
||||||
TLS bool `valid:"-"`
|
TLS bool `valid:"-"`
|
||||||
|
TLSSkipVerify bool `valid:"-"`
|
||||||
|
TLSSkipClientVerify bool `valid:"-"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -123,10 +127,13 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint := &portainer.Endpoint{
|
endpoint := &portainer.Endpoint{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
URL: req.URL,
|
URL: req.URL,
|
||||||
PublicURL: req.PublicURL,
|
PublicURL: req.PublicURL,
|
||||||
TLS: req.TLS,
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
|
TLS: req.TLS,
|
||||||
|
TLSSkipVerify: req.TLSSkipVerify,
|
||||||
|
},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
}
|
}
|
||||||
|
@ -139,12 +146,19 @@ func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *ht
|
||||||
|
|
||||||
if req.TLS {
|
if req.TLS {
|
||||||
folder := strconv.Itoa(int(endpoint.ID))
|
folder := strconv.Itoa(int(endpoint.ID))
|
||||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
|
||||||
endpoint.TLSCACertPath = caCertPath
|
if !req.TLSSkipVerify {
|
||||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||||
endpoint.TLSCertPath = certPath
|
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
}
|
||||||
endpoint.TLSKeyPath = keyPath
|
|
||||||
|
if !req.TLSSkipClientVerify {
|
||||||
|
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||||
|
endpoint.TLSConfig.TLSCertPath = certPath
|
||||||
|
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
@ -284,18 +298,33 @@ func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http
|
||||||
|
|
||||||
folder := strconv.Itoa(int(endpoint.ID))
|
folder := strconv.Itoa(int(endpoint.ID))
|
||||||
if req.TLS {
|
if req.TLS {
|
||||||
endpoint.TLS = true
|
endpoint.TLSConfig.TLS = true
|
||||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
endpoint.TLSConfig.TLSSkipVerify = req.TLSSkipVerify
|
||||||
endpoint.TLSCACertPath = caCertPath
|
if !req.TLSSkipVerify {
|
||||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||||
endpoint.TLSCertPath = certPath
|
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
} else {
|
||||||
endpoint.TLSKeyPath = keyPath
|
endpoint.TLSConfig.TLSCACertPath = ""
|
||||||
|
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.TLSSkipClientVerify {
|
||||||
|
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||||
|
endpoint.TLSConfig.TLSCertPath = certPath
|
||||||
|
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||||
|
} else {
|
||||||
|
endpoint.TLSConfig.TLSCertPath = ""
|
||||||
|
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = ""
|
||||||
|
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
endpoint.TLS = false
|
endpoint.TLSConfig.TLS = false
|
||||||
endpoint.TLSCACertPath = ""
|
endpoint.TLSConfig.TLSSkipVerify = true
|
||||||
endpoint.TLSCertPath = ""
|
endpoint.TLSConfig.TLSCACertPath = ""
|
||||||
endpoint.TLSKeyPath = ""
|
endpoint.TLSConfig.TLSCertPath = ""
|
||||||
|
endpoint.TLSConfig.TLSKeyPath = ""
|
||||||
err = handler.FileService.DeleteTLSFiles(folder)
|
err = handler.FileService.DeleteTLSFiles(folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
@ -350,7 +379,7 @@ func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *h
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpoint.TLS {
|
if endpoint.TLSConfig.TLS {
|
||||||
err = handler.FileService.DeleteTLSFiles(id)
|
err = handler.FileService.DeleteTLSFiles(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
|
|
@ -30,6 +30,7 @@ func NewFileHandler(assetPath string) *FileHandler {
|
||||||
"/js": true,
|
"/js": true,
|
||||||
"/images": true,
|
"/images": true,
|
||||||
"/fonts": true,
|
"/fonts": true,
|
||||||
|
"/ico": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return h
|
return h
|
||||||
|
|
|
@ -78,6 +78,10 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht
|
||||||
resourceControlType = portainer.ServiceResourceControl
|
resourceControlType = portainer.ServiceResourceControl
|
||||||
case "volume":
|
case "volume":
|
||||||
resourceControlType = portainer.VolumeResourceControl
|
resourceControlType = portainer.VolumeResourceControl
|
||||||
|
case "network":
|
||||||
|
resourceControlType = portainer.NetworkResourceControl
|
||||||
|
case "secret":
|
||||||
|
resourceControlType = portainer.SecretResourceControl
|
||||||
default:
|
default:
|
||||||
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
|
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
|
||||||
return
|
return
|
||||||
|
|
|
@ -82,6 +82,7 @@ type (
|
||||||
}
|
}
|
||||||
|
|
||||||
postAdminInitRequest struct {
|
postAdminInitRequest struct {
|
||||||
|
Username string `valid:"required"`
|
||||||
Password string `valid:"required"`
|
Password string `valid:"required"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -358,10 +359,14 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := handler.UserService.UserByUsername("admin")
|
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||||
if err == portainer.ErrUserNotFound {
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
user := &portainer.User{
|
user := &portainer.User{
|
||||||
Username: "admin",
|
Username: req.Username,
|
||||||
Role: portainer.AdministratorRole,
|
Role: portainer.AdministratorRole,
|
||||||
}
|
}
|
||||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||||
|
@ -375,11 +380,7 @@ func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.R
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if user != nil {
|
|
||||||
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
|
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -396,6 +397,22 @@ func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Requ
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userID == 1 {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveAdmin, http.StatusForbidden, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.ID == portainer.UserID(userID) {
|
||||||
|
httperror.WriteErrorResponse(w, portainer.ErrAdminCannotRemoveSelf, http.StatusForbidden, handler.Logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||||
|
|
||||||
if err == portainer.ErrUserNotFound {
|
if err == portainer.ErrUserNotFound {
|
||||||
|
|
|
@ -71,8 +71,8 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||||
|
|
||||||
// Should not be managed here
|
// Should not be managed here
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
if endpoint.TLS {
|
if endpoint.TLSConfig.TLS {
|
||||||
tlsConfig, err = crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false)
|
tlsConfig, err = crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -82,6 +82,54 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer
|
||||||
return decoratedServiceData, nil
|
return decoratedServiceData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decorateNetworkList loops through all networks and will decorate any network with an existing resource control.
|
||||||
|
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||||
|
func decorateNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||||
|
decoratedNetworkData := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, network := range networkData {
|
||||||
|
|
||||||
|
networkObject := network.(map[string]interface{})
|
||||||
|
if networkObject[networkIdentifier] == nil {
|
||||||
|
return nil, ErrDockerNetworkIdentifierNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
networkID := networkObject[networkIdentifier].(string)
|
||||||
|
resourceControl := getResourceControlByResourceID(networkID, resourceControls)
|
||||||
|
if resourceControl != nil {
|
||||||
|
networkObject = decorateObject(networkObject, resourceControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoratedNetworkData = append(decoratedNetworkData, networkObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoratedNetworkData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decorateSecretList loops through all secrets and will decorate any secret with an existing resource control.
|
||||||
|
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||||
|
func decorateSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||||
|
decoratedSecretData := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, secret := range secretData {
|
||||||
|
|
||||||
|
secretObject := secret.(map[string]interface{})
|
||||||
|
if secretObject[secretIdentifier] == nil {
|
||||||
|
return nil, ErrDockerSecretIdentifierNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
secretID := secretObject[secretIdentifier].(string)
|
||||||
|
resourceControl := getResourceControlByResourceID(secretID, resourceControls)
|
||||||
|
if resourceControl != nil {
|
||||||
|
secretObject = decorateObject(secretObject, resourceControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoratedSecretData = append(decoratedSecretData, secretObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoratedSecretData, nil
|
||||||
|
}
|
||||||
|
|
||||||
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
||||||
metadata := make(map[string]interface{})
|
metadata := make(map[string]interface{})
|
||||||
metadata["ResourceControl"] = resourceControl
|
metadata["ResourceControl"] = resourceControl
|
||||||
|
|
|
@ -24,7 +24,7 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||||
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
proxy := factory.createReverseProxy(u)
|
proxy := factory.createReverseProxy(u)
|
||||||
config, err := crypto.CreateTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath, false)
|
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,3 +110,76 @@ func filterServiceList(serviceData []interface{}, resourceControls []portainer.R
|
||||||
|
|
||||||
return filteredServiceData, nil
|
return filteredServiceData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterNetworkList loops through all networks, filters networks without any resource control (public resources) or with
|
||||||
|
// any resource control giving access to the user (these networks will be decorated).
|
||||||
|
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||||
|
func filterNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
|
||||||
|
filteredNetworkData := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, network := range networkData {
|
||||||
|
networkObject := network.(map[string]interface{})
|
||||||
|
if networkObject[networkIdentifier] == nil {
|
||||||
|
return nil, ErrDockerNetworkIdentifierNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
networkID := networkObject[networkIdentifier].(string)
|
||||||
|
resourceControl := getResourceControlByResourceID(networkID, resourceControls)
|
||||||
|
if resourceControl == nil {
|
||||||
|
filteredNetworkData = append(filteredNetworkData, networkObject)
|
||||||
|
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
|
||||||
|
networkObject = decorateObject(networkObject, resourceControl)
|
||||||
|
filteredNetworkData = append(filteredNetworkData, networkObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredNetworkData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterSecretList loops through all secrets, filters secrets without any resource control (public resources) or with
|
||||||
|
// any resource control giving access to the user (these secrets will be decorated).
|
||||||
|
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||||
|
func filterSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
|
||||||
|
filteredSecretData := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, secret := range secretData {
|
||||||
|
secretObject := secret.(map[string]interface{})
|
||||||
|
if secretObject[secretIdentifier] == nil {
|
||||||
|
return nil, ErrDockerSecretIdentifierNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
secretID := secretObject[secretIdentifier].(string)
|
||||||
|
resourceControl := getResourceControlByResourceID(secretID, resourceControls)
|
||||||
|
if resourceControl == nil {
|
||||||
|
filteredSecretData = append(filteredSecretData, secretObject)
|
||||||
|
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
|
||||||
|
secretObject = decorateObject(secretObject, resourceControl)
|
||||||
|
filteredSecretData = append(filteredSecretData, secretObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredSecretData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterTaskList loops through all tasks, filters tasks without any resource control (public resources) or with
|
||||||
|
// any resource control giving access to the user based on the associated service identifier.
|
||||||
|
// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||||
|
func filterTaskList(taskData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
|
||||||
|
filteredTaskData := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, task := range taskData {
|
||||||
|
taskObject := task.(map[string]interface{})
|
||||||
|
if taskObject[taskServiceIdentifier] == nil {
|
||||||
|
return nil, ErrDockerTaskServiceIdentifierNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceID := taskObject[taskServiceIdentifier].(string)
|
||||||
|
|
||||||
|
resourceControl := getResourceControlByResourceID(serviceID, resourceControls)
|
||||||
|
if resourceControl == nil || (resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl)) {
|
||||||
|
filteredTaskData = append(filteredTaskData, taskObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredTaskData, nil
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpointURL.Scheme == "tcp" {
|
if endpointURL.Scheme == "tcp" {
|
||||||
if endpoint.TLS {
|
if endpoint.TLSConfig.TLS {
|
||||||
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -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.Header().Add(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(res.StatusCode)
|
||||||
|
|
||||||
if _, err := io.Copy(w, res.Body); err != nil {
|
if _, err := io.Copy(w, res.Body); err != nil {
|
||||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
|
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||||
path := request.URL.Path
|
path := request.URL.Path
|
||||||
|
|
||||||
if strings.HasPrefix(path, "/containers") {
|
switch {
|
||||||
|
case strings.HasPrefix(path, "/containers"):
|
||||||
return p.proxyContainerRequest(request)
|
return p.proxyContainerRequest(request)
|
||||||
} else if strings.HasPrefix(path, "/services") {
|
case strings.HasPrefix(path, "/services"):
|
||||||
return p.proxyServiceRequest(request)
|
return p.proxyServiceRequest(request)
|
||||||
} else if strings.HasPrefix(path, "/volumes") {
|
case strings.HasPrefix(path, "/volumes"):
|
||||||
return p.proxyVolumeRequest(request)
|
return p.proxyVolumeRequest(request)
|
||||||
} else if strings.HasPrefix(path, "/swarm") {
|
case strings.HasPrefix(path, "/networks"):
|
||||||
|
return p.proxyNetworkRequest(request)
|
||||||
|
case strings.HasPrefix(path, "/secrets"):
|
||||||
|
return p.proxySecretRequest(request)
|
||||||
|
case strings.HasPrefix(path, "/swarm"):
|
||||||
return p.proxySwarmRequest(request)
|
return p.proxySwarmRequest(request)
|
||||||
|
case strings.HasPrefix(path, "/nodes"):
|
||||||
|
return p.proxyNodeRequest(request)
|
||||||
|
case strings.HasPrefix(path, "/tasks"):
|
||||||
|
return p.proxyTaskRequest(request)
|
||||||
|
default:
|
||||||
|
return p.executeDockerRequest(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
@ -145,10 +154,67 @@ func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Respon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/networks/create":
|
||||||
|
return p.executeDockerRequest(request)
|
||||||
|
|
||||||
|
case "/networks":
|
||||||
|
return p.rewriteOperation(request, networkListOperation)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// assume /networks/{id}
|
||||||
|
if request.Method == http.MethodGet {
|
||||||
|
return p.rewriteOperation(request, networkInspectOperation)
|
||||||
|
}
|
||||||
|
networkID := path.Base(requestPath)
|
||||||
|
return p.restrictedOperation(request, networkID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/secrets/create":
|
||||||
|
return p.executeDockerRequest(request)
|
||||||
|
|
||||||
|
case "/secrets":
|
||||||
|
return p.rewriteOperation(request, secretListOperation)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// assume /secrets/{id}
|
||||||
|
if request.Method == http.MethodGet {
|
||||||
|
return p.rewriteOperation(request, secretInspectOperation)
|
||||||
|
}
|
||||||
|
secretID := path.Base(requestPath)
|
||||||
|
return p.restrictedOperation(request, secretID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *proxyTransport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
requestPath := request.URL.Path
|
||||||
|
|
||||||
|
// assume /nodes/{id}
|
||||||
|
if path.Base(requestPath) != "nodes" {
|
||||||
|
return p.administratorOperation(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
|
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
|
||||||
return p.administratorOperation(request)
|
return p.administratorOperation(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/tasks":
|
||||||
|
return p.rewriteOperation(request, taskListOperation)
|
||||||
|
default:
|
||||||
|
// assume /tasks/{id}
|
||||||
|
return p.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// restrictedOperation ensures that the current user has the required authorizations
|
// restrictedOperation ensures that the current user has the required authorizations
|
||||||
// before executing the original request.
|
// before executing the original request.
|
||||||
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
||||||
|
|
|
@ -52,7 +52,7 @@ func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearc
|
||||||
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
|
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
|
||||||
|
|
||||||
if settings.TLSConfig.TLS || settings.StartTLS {
|
if settings.TLSConfig.TLS || settings.StartTLS {
|
||||||
config, err := crypto.CreateTLSConfiguration(settings.TLSConfig.TLSCACertPath, "", "", settings.TLSConfig.TLSSkipVerify)
|
config, err := crypto.CreateTLSConfiguration(&settings.TLSConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,16 +155,20 @@ type (
|
||||||
// Endpoint represents a Docker endpoint with all the info required
|
// Endpoint represents a Docker endpoint with all the info required
|
||||||
// to connect to it.
|
// to connect to it.
|
||||||
Endpoint struct {
|
Endpoint struct {
|
||||||
ID EndpointID `json:"Id"`
|
ID EndpointID `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
URL string `json:"URL"`
|
URL string `json:"URL"`
|
||||||
PublicURL string `json:"PublicURL"`
|
PublicURL string `json:"PublicURL"`
|
||||||
TLS bool `json:"TLS"`
|
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
|
||||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
// Deprecated fields
|
||||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
// Deprecated in DBVersion == 4
|
||||||
|
TLS bool `json:"TLS,omitempty"`
|
||||||
|
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||||
|
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||||
|
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControlID represents a resource control identifier.
|
// ResourceControlID represents a resource control identifier.
|
||||||
|
@ -172,20 +176,18 @@ type (
|
||||||
|
|
||||||
// ResourceControl represent a reference to a Docker resource with specific access controls
|
// ResourceControl represent a reference to a Docker resource with specific access controls
|
||||||
ResourceControl struct {
|
ResourceControl struct {
|
||||||
ID ResourceControlID `json:"Id"`
|
ID ResourceControlID `json:"Id"`
|
||||||
ResourceID string `json:"ResourceId"`
|
ResourceID string `json:"ResourceId"`
|
||||||
SubResourceIDs []string `json:"SubResourceIds"`
|
SubResourceIDs []string `json:"SubResourceIds"`
|
||||||
Type ResourceControlType `json:"Type"`
|
Type ResourceControlType `json:"Type"`
|
||||||
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||||
|
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated: OwnerID field is deprecated in DBVersion == 2
|
// Deprecated in DBVersion == 2
|
||||||
OwnerID UserID `json:"OwnerId"`
|
OwnerID UserID `json:"OwnerId,omitempty"`
|
||||||
// Deprecated: AccessLevel field is deprecated in DBVersion == 2
|
AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
|
||||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service).
|
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service).
|
||||||
|
@ -325,6 +327,7 @@ type (
|
||||||
FileService interface {
|
FileService interface {
|
||||||
StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error
|
StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error
|
||||||
GetPathForTLSFile(folder string, fileType TLSFileType) (string, error)
|
GetPathForTLSFile(folder string, fileType TLSFileType) (string, error)
|
||||||
|
DeleteTLSFile(folder string, fileType TLSFileType) error
|
||||||
DeleteTLSFiles(folder string) error
|
DeleteTLSFiles(folder string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,9 +345,9 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API.
|
// APIVersion is the version number of the Portainer API.
|
||||||
APIVersion = "1.14.0"
|
APIVersion = "1.14.1"
|
||||||
// DBVersion is the version number of the Portainer database.
|
// DBVersion is the version number of the Portainer database.
|
||||||
DBVersion = 3
|
DBVersion = 4
|
||||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||||
)
|
)
|
||||||
|
@ -396,4 +399,8 @@ const (
|
||||||
ServiceResourceControl
|
ServiceResourceControl
|
||||||
// VolumeResourceControl represents a resource control associated to a Docker volume
|
// VolumeResourceControl represents a resource control associated to a Docker volume
|
||||||
VolumeResourceControl
|
VolumeResourceControl
|
||||||
|
// NetworkResourceControl represents a resource control associated to a Docker network
|
||||||
|
NetworkResourceControl
|
||||||
|
// SecretResourceControl represents a resource control associated to a Docker secret
|
||||||
|
SecretResourceControl
|
||||||
)
|
)
|
||||||
|
|
386
api/swagger.yaml
386
api/swagger.yaml
|
@ -1,27 +1,62 @@
|
||||||
---
|
---
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
info:
|
info:
|
||||||
description: "Portainer API is an HTTP API served by Portainer. It is used by the\
|
description: |
|
||||||
\ Portainer UI and everything you can do with the UI can be done using the HTTP\
|
Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API.
|
||||||
\ API.\nYou can find out more about Portainer at [http://portainer.io](http://portainer.io)\
|
Examples are available at https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8
|
||||||
\ and get some support on [Slack](http://portainer.io/slack/).\n\n# Authentication\n\
|
You can find out more about Portainer at [http://portainer.io](http://portainer.io) and get some support on [Slack](http://portainer.io/slack/).
|
||||||
\nMost of the API endpoints require to be authenticated as well as some level\
|
|
||||||
\ of authorization to be used.\nPortainer API uses JSON Web Token to manage authentication\
|
# Authentication
|
||||||
\ and thus requires you to provide a token in the **Authorization** header of\
|
|
||||||
\ each request\nwith the **Bearer** authentication mechanism.\n\nExample:\n```\n\
|
Most of the API endpoints require to be authenticated as well as some level of authorization to be used.
|
||||||
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE\n\
|
Portainer API uses JSON Web Token to manage authentication and thus requires you to provide a token in the **Authorization** header of each request
|
||||||
```\n\n# Security\n\nEach API endpoint has an associated access policy, it is\
|
with the **Bearer** authentication mechanism.
|
||||||
\ documented in the description of each endpoint.\n\nDifferent access policies\
|
|
||||||
\ are available:\n* Public access\n* Authenticated access\n* Restricted access\n\
|
Example:
|
||||||
* Administrator access\n\n### Public access\n\nNo authentication is required to\
|
```
|
||||||
\ access the endpoints with this access policy.\n\n### Authenticated access\n\n\
|
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE
|
||||||
Authentication is required to access the endpoints with this access policy.\n\n\
|
```
|
||||||
### Restricted access\n\nAuthentication is required to access the endpoints with\
|
|
||||||
\ this access policy.\nExtra-checks might be added to ensure access to the resource\
|
# Security
|
||||||
\ is granted. Returned data might also be filtered.\n\n### Administrator access\n\
|
|
||||||
\nAuthentication as well as an administrator role are required to access the endpoints\
|
Each API endpoint has an associated access policy, it is documented in the description of each endpoint.
|
||||||
\ with this access policy.\n"
|
|
||||||
version: "1.14.0"
|
Different access policies are available:
|
||||||
|
* Public access
|
||||||
|
* Authenticated access
|
||||||
|
* Restricted access
|
||||||
|
* Administrator access
|
||||||
|
|
||||||
|
### Public access
|
||||||
|
|
||||||
|
No authentication is required to access the endpoints with this access policy.
|
||||||
|
|
||||||
|
### Authenticated access
|
||||||
|
|
||||||
|
Authentication is required to access the endpoints with this access policy.
|
||||||
|
|
||||||
|
### Restricted access
|
||||||
|
|
||||||
|
Authentication is required to access the endpoints with this access policy.
|
||||||
|
Extra-checks might be added to ensure access to the resource is granted. Returned data might also be filtered.
|
||||||
|
|
||||||
|
### Administrator access
|
||||||
|
|
||||||
|
Authentication as well as an administrator role are required to access the endpoints with this access policy.
|
||||||
|
|
||||||
|
# Execute Docker requests
|
||||||
|
|
||||||
|
Portainer **DO NOT** expose specific endpoints to manage your Docker resources (create a container, remove a volume, etc...).
|
||||||
|
|
||||||
|
Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you can execute Docker requests **via** the Portainer HTTP API.
|
||||||
|
|
||||||
|
To do so, you can use the `/endpoints/{id}/docker` Portainer API endpoint (which is not documented below due to Swagger limitations). This
|
||||||
|
endpoint has a restricted access policy so you still need to be authenticated to be able to query this endpoint. Any query on this endpoint will be proxied to the
|
||||||
|
Docker API of the associated endpoint (requests and responses objects are the same as documented in the Docker API).
|
||||||
|
|
||||||
|
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||||
|
|
||||||
|
version: "1.14.1"
|
||||||
title: "Portainer API"
|
title: "Portainer API"
|
||||||
contact:
|
contact:
|
||||||
email: "info@portainer.io"
|
email: "info@portainer.io"
|
||||||
|
@ -63,8 +98,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "auth"
|
- "auth"
|
||||||
summary: "Authenticate a user"
|
summary: "Authenticate a user"
|
||||||
description: "Use this endpoint to authenticate against Portainer using a username\
|
description: |
|
||||||
\ and password. \n**Access policy**: public\n"
|
Use this endpoint to authenticate against Portainer using a username and password.
|
||||||
|
**Access policy**: public
|
||||||
operationId: "AuthenticateUser"
|
operationId: "AuthenticateUser"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -105,8 +141,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "dockerhub"
|
- "dockerhub"
|
||||||
summary: "Retrieve DockerHub information"
|
summary: "Retrieve DockerHub information"
|
||||||
description: "Use this endpoint to retrieve the information used to connect\
|
description: |
|
||||||
\ to the DockerHub \n**Access policy**: authenticated\n"
|
Use this endpoint to retrieve the information used to connect to the DockerHub
|
||||||
|
**Access policy**: authenticated
|
||||||
operationId: "DockerHubInspect"
|
operationId: "DockerHubInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -124,8 +161,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "dockerhub"
|
- "dockerhub"
|
||||||
summary: "Update DockerHub information"
|
summary: "Update DockerHub information"
|
||||||
description: "Use this endpoint to update the information used to connect to\
|
description: |
|
||||||
\ the DockerHub \n**Access policy**: administrator\n"
|
Use this endpoint to update the information used to connect to the DockerHub
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "DockerHubUpdate"
|
operationId: "DockerHubUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -157,9 +195,11 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "endpoints"
|
- "endpoints"
|
||||||
summary: "List endpoints"
|
summary: "List endpoints"
|
||||||
description: "List all endpoints based on the current user authorizations. Will\n\
|
description: |
|
||||||
return all endpoints if using an administrator account otherwise it will\n\
|
List all endpoints based on the current user authorizations. Will
|
||||||
only return authorized endpoints. \n**Access policy**: restricted \n"
|
return all endpoints if using an administrator account otherwise it will
|
||||||
|
only return authorized endpoints.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "EndpointList"
|
operationId: "EndpointList"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -177,8 +217,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "endpoints"
|
- "endpoints"
|
||||||
summary: "Create a new endpoint"
|
summary: "Create a new endpoint"
|
||||||
description: "Create a new endpoint that will be used to manage a Docker environment.\
|
description: |
|
||||||
\ \n**Access policy**: administrator\n"
|
Create a new endpoint that will be used to manage a Docker environment.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "EndpointCreate"
|
operationId: "EndpointCreate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -219,8 +260,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "endpoints"
|
- "endpoints"
|
||||||
summary: "Inspect an endpoint"
|
summary: "Inspect an endpoint"
|
||||||
description: "Retrieve details abount an endpoint. \n**Access policy**: administrator\
|
description: |
|
||||||
\ \n"
|
Retrieve details abount an endpoint.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "EndpointInspect"
|
operationId: "EndpointInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -257,7 +299,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "endpoints"
|
- "endpoints"
|
||||||
summary: "Update an endpoint"
|
summary: "Update an endpoint"
|
||||||
description: "Update an endpoint. \n**Access policy**: administrator\n"
|
description: |
|
||||||
|
Update an endpoint.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "EndpointUpdate"
|
operationId: "EndpointUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -307,7 +351,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "endpoints"
|
- "endpoints"
|
||||||
summary: "Remove an endpoint"
|
summary: "Remove an endpoint"
|
||||||
description: "Remove an endpoint. \n**Access policy**: administrator \n"
|
description: |
|
||||||
|
Remove an endpoint.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "EndpointDelete"
|
operationId: "EndpointDelete"
|
||||||
parameters:
|
parameters:
|
||||||
- name: "id"
|
- name: "id"
|
||||||
|
@ -348,8 +394,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "endpoints"
|
- "endpoints"
|
||||||
summary: "Manage accesses to an endpoint"
|
summary: "Manage accesses to an endpoint"
|
||||||
description: "Manage user and team accesses to an endpoint. \n**Access policy**:\
|
description: |
|
||||||
\ administrator \n"
|
Manage user and team accesses to an endpoint.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "EndpointAccessUpdate"
|
operationId: "EndpointAccessUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -388,15 +435,17 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
|
||||||
/registries:
|
/registries:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- "registries"
|
- "registries"
|
||||||
summary: "List registries"
|
summary: "List registries"
|
||||||
description: "List all registries based on the current user authorizations.\n\
|
description: |
|
||||||
Will return all registries if using an administrator account otherwise it\n\
|
List all registries based on the current user authorizations.
|
||||||
will only return authorized registries. \n**Access policy**: restricted \
|
Will return all registries if using an administrator account otherwise it
|
||||||
\ \n"
|
will only return authorized registries.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "RegistryList"
|
operationId: "RegistryList"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -414,8 +463,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "registries"
|
- "registries"
|
||||||
summary: "Create a new registry"
|
summary: "Create a new registry"
|
||||||
description: "Create a new registry. \n**Access policy**: administrator \
|
description: |
|
||||||
\ \n"
|
Create a new registry.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "RegistryCreate"
|
operationId: "RegistryCreate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -456,8 +506,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "registries"
|
- "registries"
|
||||||
summary: "Inspect a registry"
|
summary: "Inspect a registry"
|
||||||
description: "Retrieve details about a registry. \n**Access policy**: administrator\
|
description: |
|
||||||
\ \n"
|
Retrieve details about a registry.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "RegistryInspect"
|
operationId: "RegistryInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -494,7 +545,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "registries"
|
- "registries"
|
||||||
summary: "Update a registry"
|
summary: "Update a registry"
|
||||||
description: "Update a registry. \n**Access policy**: administrator \n"
|
description: |
|
||||||
|
Update a registry.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "RegistryUpdate"
|
operationId: "RegistryUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -551,8 +604,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "registries"
|
- "registries"
|
||||||
summary: "Remove a registry"
|
summary: "Remove a registry"
|
||||||
description: "Remove a registry. \n**Access policy**: administrator \
|
description: |
|
||||||
\ \n"
|
Remove a registry.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "RegistryDelete"
|
operationId: "RegistryDelete"
|
||||||
parameters:
|
parameters:
|
||||||
- name: "id"
|
- name: "id"
|
||||||
|
@ -586,8 +640,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "registries"
|
- "registries"
|
||||||
summary: "Manage accesses to a registry"
|
summary: "Manage accesses to a registry"
|
||||||
description: "Manage user and team accesses to a registry. \n**Access policy**:\
|
description: |
|
||||||
\ administrator \n"
|
Manage user and team accesses to a registry.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "RegistryAccessUpdate"
|
operationId: "RegistryAccessUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -631,8 +686,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "resource_controls"
|
- "resource_controls"
|
||||||
summary: "Create a new resource control"
|
summary: "Create a new resource control"
|
||||||
description: "Create a new resource control to restrict access to a Docker resource.\
|
description: |
|
||||||
\ \n**Access policy**: restricted \n"
|
Create a new resource control to restrict access to a Docker resource.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "ResourceControlCreate"
|
operationId: "ResourceControlCreate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -678,8 +734,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "resource_controls"
|
- "resource_controls"
|
||||||
summary: "Update a resource control"
|
summary: "Update a resource control"
|
||||||
description: "Update a resource control. \n**Access policy**: restricted \
|
description: |
|
||||||
\ \n"
|
Update a resource control.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "ResourceControlUpdate"
|
operationId: "ResourceControlUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -729,8 +786,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "resource_controls"
|
- "resource_controls"
|
||||||
summary: "Remove a resource control"
|
summary: "Remove a resource control"
|
||||||
description: "Remove a resource control. \n**Access policy**: restricted \
|
description: |
|
||||||
\ \n"
|
Remove a resource control.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "ResourceControlDelete"
|
operationId: "ResourceControlDelete"
|
||||||
parameters:
|
parameters:
|
||||||
- name: "id"
|
- name: "id"
|
||||||
|
@ -771,8 +829,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "settings"
|
- "settings"
|
||||||
summary: "Retrieve Portainer settings"
|
summary: "Retrieve Portainer settings"
|
||||||
description: "Retrieve Portainer settings. \n**Access policy**: administrator\
|
description: |
|
||||||
\ \n"
|
Retrieve Portainer settings.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "SettingsInspect"
|
operationId: "SettingsInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -790,8 +849,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "settings"
|
- "settings"
|
||||||
summary: "Update Portainer settings"
|
summary: "Update Portainer settings"
|
||||||
description: "Update Portainer settings. \n**Access policy**: administrator\
|
description: |
|
||||||
\ \n"
|
Update Portainer settings.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "SettingsUpdate"
|
operationId: "SettingsUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -823,9 +883,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "settings"
|
- "settings"
|
||||||
summary: "Retrieve Portainer public settings"
|
summary: "Retrieve Portainer public settings"
|
||||||
description: "Retrieve public settings. Returns a small set of settings that\
|
description: |
|
||||||
\ are not reserved to administrators only. \n**Access policy**: public \
|
Retrieve public settings. Returns a small set of settings that are not reserved to administrators only.
|
||||||
\ \n"
|
**Access policy**: public
|
||||||
operationId: "PublicSettingsInspect"
|
operationId: "PublicSettingsInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -844,8 +904,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "settings"
|
- "settings"
|
||||||
summary: "Test LDAP connectivity"
|
summary: "Test LDAP connectivity"
|
||||||
description: "Test LDAP connectivity using LDAP details. \n**Access policy**:\
|
description: |
|
||||||
\ administrator \n"
|
Test LDAP connectivity using LDAP details.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "SettingsLDAPCheck"
|
operationId: "SettingsLDAPCheck"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -877,8 +938,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "status"
|
- "status"
|
||||||
summary: "Check Portainer status"
|
summary: "Check Portainer status"
|
||||||
description: "Retrieve Portainer status. \n**Access policy**: public \
|
description: |
|
||||||
\ \n"
|
Retrieve Portainer status.
|
||||||
|
**Access policy**: public
|
||||||
operationId: "StatusInspect"
|
operationId: "StatusInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -897,9 +959,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "List users"
|
summary: "List users"
|
||||||
description: "List Portainer users. Non-administrator users will only be able\
|
description: |
|
||||||
\ to list other non-administrator user accounts. \n**Access policy**: restricted\
|
List Portainer users. Non-administrator users will only be able to list other non-administrator user accounts.
|
||||||
\ \n"
|
**Access policy**: restricted
|
||||||
operationId: "UserList"
|
operationId: "UserList"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -917,9 +979,10 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "Create a new user"
|
summary: "Create a new user"
|
||||||
description: "Create a new Portainer user. Only team leaders and administrators\
|
description: |
|
||||||
\ can create users. Only administrators can\ncreate an administrator user\
|
Create a new Portainer user. Only team leaders and administrators can create users. Only administrators can
|
||||||
\ account. \n**Access policy**: restricted \n"
|
create an administrator user account.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "UserCreate"
|
operationId: "UserCreate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -967,8 +1030,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "Inspect a user"
|
summary: "Inspect a user"
|
||||||
description: "Retrieve details about a user. \n**Access policy**: administrator\
|
description: |
|
||||||
\ \n"
|
Retrieve details about a user.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "UserInspect"
|
operationId: "UserInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1005,8 +1069,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "Update a user"
|
summary: "Update a user"
|
||||||
description: "Update user details. A regular user account can only update his\
|
description: |
|
||||||
\ details. \n**Access policy**: authenticated \n"
|
Update user details. A regular user account can only update his details.
|
||||||
|
**Access policy**: authenticated
|
||||||
operationId: "UserUpdate"
|
operationId: "UserUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1056,7 +1121,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "Remove a user"
|
summary: "Remove a user"
|
||||||
description: "Remove a user. \n**Access policy**: administrator \n"
|
description: |
|
||||||
|
Remove a user.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "UserDelete"
|
operationId: "UserDelete"
|
||||||
parameters:
|
parameters:
|
||||||
- name: "id"
|
- name: "id"
|
||||||
|
@ -1090,8 +1157,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "Inspect a user memberships"
|
summary: "Inspect a user memberships"
|
||||||
description: "Inspect a user memberships. \n**Access policy**: authenticated\
|
description: |
|
||||||
\ \n"
|
Inspect a user memberships.
|
||||||
|
**Access policy**: authenticated
|
||||||
operationId: "UserMembershipsInspect"
|
operationId: "UserMembershipsInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1124,13 +1192,15 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
|
||||||
/users/{id}/passwd:
|
/users/{id}/passwd:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "Check password validity for a user"
|
summary: "Check password validity for a user"
|
||||||
description: "Check if the submitted password is valid for the specified user.\
|
description: |
|
||||||
\ \n**Access policy**: authenticated \n"
|
Check if the submitted password is valid for the specified user.
|
||||||
|
**Access policy**: authenticated
|
||||||
operationId: "UserPasswordCheck"
|
operationId: "UserPasswordCheck"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1171,13 +1241,15 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
|
||||||
/users/admin/check:
|
/users/admin/check:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "Check administrator account existence"
|
summary: "Check administrator account existence"
|
||||||
description: "Check if an administrator account exists in the database.\n**Access\
|
description: |
|
||||||
\ policy**: public \n"
|
Check if an administrator account exists in the database.
|
||||||
|
**Access policy**: public
|
||||||
operationId: "UserAdminCheck"
|
operationId: "UserAdminCheck"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1198,13 +1270,15 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
|
||||||
/users/admin/init:
|
/users/admin/init:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- "users"
|
- "users"
|
||||||
summary: "Initialize administrator account"
|
summary: "Initialize administrator account"
|
||||||
description: "Initialize the 'admin' user account.\n**Access policy**: public\
|
description: |
|
||||||
\ \n"
|
Initialize the 'admin' user account.
|
||||||
|
**Access policy**: public
|
||||||
operationId: "UserAdminInit"
|
operationId: "UserAdminInit"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1238,34 +1312,35 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
|
||||||
/upload/tls/{certificate}:
|
/upload/tls/{certificate}:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- "upload"
|
- "upload"
|
||||||
summary: "Upload TLS files"
|
summary: "Upload TLS files"
|
||||||
description: "Use this endpoint to upload TLS files. \n**Access policy**: administrator\n"
|
description: |
|
||||||
|
Use this endpoint to upload TLS files.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "UploadTLS"
|
operationId: "UploadTLS"
|
||||||
consumes:
|
consumes:
|
||||||
- "multipart/form-data"
|
- multipart/form-data
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
parameters:
|
parameters:
|
||||||
- name: "certificate"
|
- in: "path"
|
||||||
in: "path"
|
name: "certificate"
|
||||||
description: "TLS file type. Valid values are 'ca', 'cert' or 'key'."
|
description: "TLS file type. Valid values are 'ca', 'cert' or 'key'."
|
||||||
required: true
|
required: true
|
||||||
type: "string"
|
type: "string"
|
||||||
- name: "folder"
|
- in: "query"
|
||||||
in: "query"
|
name: "folder"
|
||||||
description: "Folder where the TLS file will be stored. Will be created if\
|
description: "Folder where the TLS file will be stored. Will be created if not existing."
|
||||||
\ not existing."
|
|
||||||
required: true
|
required: true
|
||||||
type: "string"
|
type: "string"
|
||||||
- name: "file"
|
- in: "formData"
|
||||||
in: "formData"
|
name: "file"
|
||||||
description: "The file to upload."
|
|
||||||
required: false
|
|
||||||
type: "file"
|
type: "file"
|
||||||
|
description: "The file to upload."
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: "Success"
|
description: "Success"
|
||||||
|
@ -1280,13 +1355,15 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
|
||||||
/teams:
|
/teams:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- "teams"
|
- "teams"
|
||||||
summary: "List teams"
|
summary: "List teams"
|
||||||
description: "List teams. For non-administrator users, will only list the teams\
|
description: |
|
||||||
\ they are member of. \n**Access policy**: restricted \n"
|
List teams. For non-administrator users, will only list the teams they are member of.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "TeamList"
|
operationId: "TeamList"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1304,8 +1381,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "teams"
|
- "teams"
|
||||||
summary: "Create a new team"
|
summary: "Create a new team"
|
||||||
description: "Create a new team. \n**Access policy**: administrator \
|
description: |
|
||||||
\ \n"
|
Create a new team.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "TeamCreate"
|
operationId: "TeamCreate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1353,8 +1431,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "teams"
|
- "teams"
|
||||||
summary: "Inspect a team"
|
summary: "Inspect a team"
|
||||||
description: "Retrieve details about a team. Access is only available for administrator\
|
description: |
|
||||||
\ and leaders of that team. \n**Access policy**: restricted \n"
|
Retrieve details about a team. Access is only available for administrator and leaders of that team.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "TeamInspect"
|
operationId: "TeamInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1398,8 +1477,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "teams"
|
- "teams"
|
||||||
summary: "Update a team"
|
summary: "Update a team"
|
||||||
description: "Update a team. \n**Access policy**: administrator \
|
description: |
|
||||||
\ \n"
|
Update a team.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "TeamUpdate"
|
operationId: "TeamUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1442,7 +1522,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "teams"
|
- "teams"
|
||||||
summary: "Remove a team"
|
summary: "Remove a team"
|
||||||
description: "Remove a team. \n**Access policy**: administrator \n"
|
description: |
|
||||||
|
Remove a team.
|
||||||
|
**Access policy**: administrator
|
||||||
operationId: "TeamDelete"
|
operationId: "TeamDelete"
|
||||||
parameters:
|
parameters:
|
||||||
- name: "id"
|
- name: "id"
|
||||||
|
@ -1471,13 +1553,15 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
|
||||||
/teams/{id}/memberships:
|
/teams/{id}/memberships:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- "teams"
|
- "teams"
|
||||||
summary: "Inspect a team memberships"
|
summary: "Inspect a team memberships"
|
||||||
description: "Inspect a team memberships. Access is only available for administrator\
|
description: |
|
||||||
\ and leaders of that team. \n**Access policy**: restricted \n"
|
Inspect a team memberships. Access is only available for administrator and leaders of that team.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "TeamMembershipsInspect"
|
operationId: "TeamMembershipsInspect"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1510,13 +1594,15 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
|
||||||
/team_memberships:
|
/team_memberships:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- "team_memberships"
|
- "team_memberships"
|
||||||
summary: "List team memberships"
|
summary: "List team memberships"
|
||||||
description: "List team memberships. Access is only available to administrators\
|
description: |
|
||||||
\ and team leaders. \n**Access policy**: restricted \n"
|
List team memberships. Access is only available to administrators and team leaders.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "TeamMembershipList"
|
operationId: "TeamMembershipList"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1541,8 +1627,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "team_memberships"
|
- "team_memberships"
|
||||||
summary: "Create a new team membership"
|
summary: "Create a new team membership"
|
||||||
description: "Create a new team memberships. Access is only available to administrators\
|
description: |
|
||||||
\ leaders of the associated team. \n**Access policy**: restricted \n"
|
Create a new team memberships. Access is only available to administrators leaders of the associated team.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "TeamMembershipCreate"
|
operationId: "TeamMembershipCreate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1590,9 +1677,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "team_memberships"
|
- "team_memberships"
|
||||||
summary: "Update a team membership"
|
summary: "Update a team membership"
|
||||||
description: "Update a team membership. Access is only available to administrators\
|
description: |
|
||||||
\ leaders of the associated team. \n**Access policy**: restricted \
|
Update a team membership. Access is only available to administrators leaders of the associated team.
|
||||||
\ \n"
|
**Access policy**: restricted
|
||||||
operationId: "TeamMembershipUpdate"
|
operationId: "TeamMembershipUpdate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1642,8 +1729,9 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "team_memberships"
|
- "team_memberships"
|
||||||
summary: "Remove a team membership"
|
summary: "Remove a team membership"
|
||||||
description: "Remove a team membership. Access is only available to administrators\
|
description: |
|
||||||
\ leaders of the associated team. \n**Access policy**: restricted \n"
|
Remove a team membership. Access is only available to administrators leaders of the associated team.
|
||||||
|
**Access policy**: restricted
|
||||||
operationId: "TeamMembershipDelete"
|
operationId: "TeamMembershipDelete"
|
||||||
parameters:
|
parameters:
|
||||||
- name: "id"
|
- name: "id"
|
||||||
|
@ -1684,17 +1772,18 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- "templates"
|
- "templates"
|
||||||
summary: "Retrieve App templates"
|
summary: "Retrieve App templates"
|
||||||
description: "Retrieve App templates. \nYou can find more information about\
|
description: |
|
||||||
\ the format at http://portainer.readthedocs.io/en/stable/templates.html \
|
Retrieve App templates.
|
||||||
\ \n**Access policy**: authenticated \n"
|
You can find more information about the format at http://portainer.readthedocs.io/en/stable/templates.html
|
||||||
|
**Access policy**: authenticated
|
||||||
operationId: "TemplateList"
|
operationId: "TemplateList"
|
||||||
produces:
|
produces:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
parameters:
|
parameters:
|
||||||
- name: "key"
|
- name: "key"
|
||||||
in: "query"
|
in: "query"
|
||||||
description: "Templates key. Valid values are 'container' or 'linuxserver.io'."
|
|
||||||
required: true
|
required: true
|
||||||
|
description: "Templates key. Valid values are 'container' or 'linuxserver.io'."
|
||||||
type: "string"
|
type: "string"
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
|
@ -1780,7 +1869,7 @@ definitions:
|
||||||
description: "Is analytics enabled"
|
description: "Is analytics enabled"
|
||||||
Version:
|
Version:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "1.14.0"
|
example: "1.14.1"
|
||||||
description: "Portainer API version"
|
description: "Portainer API version"
|
||||||
PublicSettingsInspectResponse:
|
PublicSettingsInspectResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
@ -1799,8 +1888,8 @@ definitions:
|
||||||
AuthenticationMethod:
|
AuthenticationMethod:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
example: 1
|
example: 1
|
||||||
description: "Active authentication method for the Portainer instance. Valid\
|
description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
|
||||||
\ values are: 1 for managed or 2 for LDAP."
|
|
||||||
TLSConfiguration:
|
TLSConfiguration:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
@ -1824,14 +1913,14 @@ definitions:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "/data/tls/key.pem"
|
example: "/data/tls/key.pem"
|
||||||
description: "Path to the TLS client key file"
|
description: "Path to the TLS client key file"
|
||||||
|
|
||||||
LDAPSearchSettings:
|
LDAPSearchSettings:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
BaseDN:
|
BaseDN:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "dc=ldap,dc=domain,dc=tld"
|
example: "dc=ldap,dc=domain,dc=tld"
|
||||||
description: "The distinguished name of the element from which the LDAP server\
|
description: "The distinguished name of the element from which the LDAP server will search for users"
|
||||||
\ will search for users"
|
|
||||||
Filter:
|
Filter:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "(objectClass=account)"
|
example: "(objectClass=account)"
|
||||||
|
@ -1840,6 +1929,7 @@ definitions:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "uid"
|
example: "uid"
|
||||||
description: "LDAP attribute which denotes the username"
|
description: "LDAP attribute which denotes the username"
|
||||||
|
|
||||||
LDAPSettings:
|
LDAPSettings:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
@ -1865,6 +1955,7 @@ definitions:
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/LDAPSearchSettings"
|
$ref: "#/definitions/LDAPSearchSettings"
|
||||||
|
|
||||||
Settings:
|
Settings:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
@ -1893,8 +1984,7 @@ definitions:
|
||||||
AuthenticationMethod:
|
AuthenticationMethod:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
example: 1
|
example: 1
|
||||||
description: "Active authentication method for the Portainer instance. Valid\
|
description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
|
||||||
\ values are: 1 for managed or 2 for LDAP."
|
|
||||||
LDAPSettings:
|
LDAPSettings:
|
||||||
$ref: "#/definitions/LDAPSettings"
|
$ref: "#/definitions/LDAPSettings"
|
||||||
Settings_BlackListedLabels:
|
Settings_BlackListedLabels:
|
||||||
|
@ -2060,6 +2150,14 @@ definitions:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
example: true
|
example: true
|
||||||
description: "Require TLS to connect against this endpoint"
|
description: "Require TLS to connect against this endpoint"
|
||||||
|
TLSSkipVerify:
|
||||||
|
type: "boolean"
|
||||||
|
example: false
|
||||||
|
description: "Skip server verification when using TLS"
|
||||||
|
TLSSkipClientVerify:
|
||||||
|
type: "boolean"
|
||||||
|
example: false
|
||||||
|
description: "Skip client verification when using TLS"
|
||||||
EndpointCreateResponse:
|
EndpointCreateResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
@ -2091,6 +2189,14 @@ definitions:
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
example: true
|
example: true
|
||||||
description: "Require TLS to connect against this endpoint"
|
description: "Require TLS to connect against this endpoint"
|
||||||
|
TLSSkipVerify:
|
||||||
|
type: "boolean"
|
||||||
|
example: false
|
||||||
|
description: "Skip server verification when using TLS"
|
||||||
|
TLSSkipClientVerify:
|
||||||
|
type: "boolean"
|
||||||
|
example: false
|
||||||
|
description: "Skip client verification when using TLS"
|
||||||
EndpointAccessUpdateRequest:
|
EndpointAccessUpdateRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
@ -2257,8 +2363,8 @@ definitions:
|
||||||
SettingsUpdateRequest:
|
SettingsUpdateRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
required:
|
required:
|
||||||
- "AuthenticationMethod"
|
|
||||||
- "TemplatesURL"
|
- "TemplatesURL"
|
||||||
|
- "AuthenticationMethod"
|
||||||
properties:
|
properties:
|
||||||
TemplatesURL:
|
TemplatesURL:
|
||||||
type: "string"
|
type: "string"
|
||||||
|
@ -2285,8 +2391,7 @@ definitions:
|
||||||
AuthenticationMethod:
|
AuthenticationMethod:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
example: 1
|
example: 1
|
||||||
description: "Active authentication method for the Portainer instance. Valid\
|
description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP."
|
||||||
\ values are: 1 for managed or 2 for LDAP."
|
|
||||||
LDAPSettings:
|
LDAPSettings:
|
||||||
$ref: "#/definitions/LDAPSettings"
|
$ref: "#/definitions/LDAPSettings"
|
||||||
UserCreateRequest:
|
UserCreateRequest:
|
||||||
|
@ -2383,12 +2488,13 @@ definitions:
|
||||||
type: "array"
|
type: "array"
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/TeamMembership"
|
$ref: "#/definitions/TeamMembership"
|
||||||
|
|
||||||
TeamMembershipCreateRequest:
|
TeamMembershipCreateRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
required:
|
required:
|
||||||
- "Role"
|
|
||||||
- "TeamID"
|
|
||||||
- "UserID"
|
- "UserID"
|
||||||
|
- "TeamID"
|
||||||
|
- "Role"
|
||||||
properties:
|
properties:
|
||||||
UserID:
|
UserID:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
|
@ -2401,8 +2507,7 @@ definitions:
|
||||||
Role:
|
Role:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
example: 1
|
example: 1
|
||||||
description: "Role for the user inside the team (1 for leader and 2 for regular\
|
description: "Role for the user inside the team (1 for leader and 2 for regular member)"
|
||||||
\ member)"
|
|
||||||
TeamMembershipCreateResponse:
|
TeamMembershipCreateResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
@ -2417,9 +2522,9 @@ definitions:
|
||||||
TeamMembershipUpdateRequest:
|
TeamMembershipUpdateRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
required:
|
required:
|
||||||
- "Role"
|
|
||||||
- "TeamID"
|
|
||||||
- "UserID"
|
- "UserID"
|
||||||
|
- "TeamID"
|
||||||
|
- "Role"
|
||||||
properties:
|
properties:
|
||||||
UserID:
|
UserID:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
|
@ -2432,8 +2537,7 @@ definitions:
|
||||||
Role:
|
Role:
|
||||||
type: "integer"
|
type: "integer"
|
||||||
example: 1
|
example: 1
|
||||||
description: "Role for the user inside the team (1 for leader and 2 for regular\
|
description: "Role for the user inside the team (1 for leader and 2 for regular member)"
|
||||||
\ member)"
|
|
||||||
SettingsLDAPCheckRequest:
|
SettingsLDAPCheckRequest:
|
||||||
type: "object"
|
type: "object"
|
||||||
properties:
|
properties:
|
||||||
|
|
77
app/app.js
77
app/app.js
|
@ -23,6 +23,7 @@ angular.module('portainer', [
|
||||||
'container',
|
'container',
|
||||||
'containerConsole',
|
'containerConsole',
|
||||||
'containerLogs',
|
'containerLogs',
|
||||||
|
'containerStats',
|
||||||
'serviceLogs',
|
'serviceLogs',
|
||||||
'containers',
|
'containers',
|
||||||
'createContainer',
|
'createContainer',
|
||||||
|
@ -31,14 +32,15 @@ angular.module('portainer', [
|
||||||
'createSecret',
|
'createSecret',
|
||||||
'createService',
|
'createService',
|
||||||
'createVolume',
|
'createVolume',
|
||||||
'docker',
|
'engine',
|
||||||
'endpoint',
|
'endpoint',
|
||||||
'endpointAccess',
|
'endpointAccess',
|
||||||
'endpointInit',
|
|
||||||
'endpoints',
|
'endpoints',
|
||||||
'events',
|
'events',
|
||||||
'image',
|
'image',
|
||||||
'images',
|
'images',
|
||||||
|
'initAdmin',
|
||||||
|
'initEndpoint',
|
||||||
'main',
|
'main',
|
||||||
'network',
|
'network',
|
||||||
'networks',
|
'networks',
|
||||||
|
@ -53,8 +55,8 @@ angular.module('portainer', [
|
||||||
'settings',
|
'settings',
|
||||||
'settingsAuthentication',
|
'settingsAuthentication',
|
||||||
'sidebar',
|
'sidebar',
|
||||||
'stats',
|
|
||||||
'swarm',
|
'swarm',
|
||||||
|
'swarmVisualizer',
|
||||||
'task',
|
'task',
|
||||||
'team',
|
'team',
|
||||||
'teams',
|
'teams',
|
||||||
|
@ -63,7 +65,8 @@ angular.module('portainer', [
|
||||||
'users',
|
'users',
|
||||||
'userSettings',
|
'userSettings',
|
||||||
'volume',
|
'volume',
|
||||||
'volumes'])
|
'volumes',
|
||||||
|
'rzModule'])
|
||||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
|
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
@ -157,8 +160,8 @@ angular.module('portainer', [
|
||||||
url: '^/containers/:id/stats',
|
url: '^/containers/:id/stats',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: 'app/components/stats/stats.html',
|
templateUrl: 'app/components/containerStats/containerStats.html',
|
||||||
controller: 'StatsController'
|
controller: 'ContainerStatsController'
|
||||||
},
|
},
|
||||||
'sidebar@': {
|
'sidebar@': {
|
||||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||||
|
@ -321,12 +324,39 @@ angular.module('portainer', [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.state('docker', {
|
.state('init', {
|
||||||
url: '/docker/',
|
abstract: true,
|
||||||
|
url: '/init',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: 'app/components/docker/docker.html',
|
template: '<div ui-view="content@"></div>'
|
||||||
controller: 'DockerController'
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('init.endpoint', {
|
||||||
|
url: '/endpoint',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/components/initEndpoint/initEndpoint.html',
|
||||||
|
controller: 'InitEndpointController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('init.admin', {
|
||||||
|
url: '/admin',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/components/initAdmin/initAdmin.html',
|
||||||
|
controller: 'InitAdminController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.state('engine', {
|
||||||
|
url: '/engine/',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/components/engine/engine.html',
|
||||||
|
controller: 'EngineController'
|
||||||
},
|
},
|
||||||
'sidebar@': {
|
'sidebar@': {
|
||||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||||
|
@ -373,15 +403,6 @@ angular.module('portainer', [
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.state('endpointInit', {
|
|
||||||
url: '/init/endpoint',
|
|
||||||
views: {
|
|
||||||
'content@': {
|
|
||||||
templateUrl: 'app/components/endpointInit/endpointInit.html',
|
|
||||||
controller: 'EndpointInitController'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.state('events', {
|
.state('events', {
|
||||||
url: '/events/',
|
url: '/events/',
|
||||||
views: {
|
views: {
|
||||||
|
@ -716,7 +737,7 @@ angular.module('portainer', [
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.state('swarm', {
|
.state('swarm', {
|
||||||
url: '/swarm/',
|
url: '/swarm',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
templateUrl: 'app/components/swarm/swarm.html',
|
templateUrl: 'app/components/swarm/swarm.html',
|
||||||
|
@ -727,7 +748,21 @@ angular.module('portainer', [
|
||||||
controller: 'SidebarController'
|
controller: 'SidebarController'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.state('swarm.visualizer', {
|
||||||
|
url: '/visualizer',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/components/swarmVisualizer/swarmVisualizer.html',
|
||||||
|
controller: 'SwarmVisualizerController'
|
||||||
|
},
|
||||||
|
'sidebar@': {
|
||||||
|
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||||
|
controller: 'SidebarController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
;
|
||||||
}])
|
}])
|
||||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) {
|
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) {
|
||||||
EndpointProvider.initialize();
|
EndpointProvider.initialize();
|
||||||
|
|
|
@ -1,92 +1,38 @@
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- login box -->
|
<!-- login box -->
|
||||||
<div class="container simple-box">
|
<div class="container simple-box">
|
||||||
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
<div class="col-sm-6 col-sm-offset-3">
|
||||||
<!-- login box logo -->
|
<!-- login box logo -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
|
||||||
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||||
|
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||||
</div>
|
</div>
|
||||||
<!-- !login box logo -->
|
<!-- !login box logo -->
|
||||||
<!-- init password panel -->
|
|
||||||
<div class="panel panel-default" ng-if="initPassword">
|
|
||||||
<div class="panel-body">
|
|
||||||
<!-- init password form -->
|
|
||||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
|
||||||
<!-- comment -->
|
|
||||||
<div class="input-group">
|
|
||||||
<p style="margin: 5px;">
|
|
||||||
Please specify a password for the <b>admin</b> user account.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- !comment input -->
|
|
||||||
<!-- comment -->
|
|
||||||
<div class="input-group">
|
|
||||||
<p style="margin: 5px;">
|
|
||||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password.length >= 8]" aria-hidden="true"></i>
|
|
||||||
Your password must be at least 8 characters long
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- !comment input -->
|
|
||||||
<!-- password input -->
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
|
||||||
<input id="admin_password" type="password" class="form-control" name="password" ng-model="initPasswordData.password" autofocus>
|
|
||||||
</div>
|
|
||||||
<!-- !password input -->
|
|
||||||
<!-- comment -->
|
|
||||||
<div class="input-group">
|
|
||||||
<p style="margin: 5px;">
|
|
||||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password !== '' && initPasswordData.password === initPasswordData.password_confirmation]" aria-hidden="true"></i>
|
|
||||||
Confirm your password
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- !comment input -->
|
|
||||||
<!-- password confirmation input -->
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
|
||||||
<input id="password_confirmation" type="password" class="form-control" name="password" ng-model="initPasswordData.password_confirmation">
|
|
||||||
</div>
|
|
||||||
<!-- !password confirmation input -->
|
|
||||||
<!-- validate button -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12 controls">
|
|
||||||
<p class="pull-left text-danger" ng-if="initPasswordData.error" style="margin: 5px;">
|
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Unable to create default user
|
|
||||||
</p>
|
|
||||||
<button type="submit" class="btn btn-primary pull-right" ng-disabled="initPasswordData.password.length < 8 || initPasswordData.password !== initPasswordData.password_confirmation" ng-click="createAdminUser()"><i class="fa fa-key" aria-hidden="true"></i> Validate</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !validate button -->
|
|
||||||
</form>
|
|
||||||
<!-- !init password form -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !init password panel -->
|
|
||||||
<!-- login panel -->
|
<!-- login panel -->
|
||||||
<div class="panel panel-default" ng-if="!initPassword">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<!-- login form -->
|
<!-- login form -->
|
||||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
<form class="simple-box-form form-horizontal">
|
||||||
<!-- username input -->
|
<!-- username input -->
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
|
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
|
||||||
<input id="username" type="text" class="form-control" name="username" ng-model="authData.username" placeholder="Username">
|
<input id="username" type="text" class="form-control" name="username" ng-model="formValues.Username" auto-focus>
|
||||||
</div>
|
</div>
|
||||||
<!-- !username input -->
|
<!-- !username input -->
|
||||||
<!-- password input -->
|
<!-- password input -->
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||||
<input id="password" type="password" class="form-control" name="password" ng-model="authData.password" autofocus>
|
<input id="password" type="password" class="form-control" name="password" ng-model="formValues.Password">
|
||||||
</div>
|
</div>
|
||||||
<!-- !password input -->
|
<!-- !password input -->
|
||||||
<!-- login button -->
|
<!-- login button -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12 controls">
|
<div class="col-sm-12">
|
||||||
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;">
|
<button type="submit" class="btn btn-primary btn-sm pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }}
|
<span class="pull-left" style="margin: 5px;" ng-if="state.AuthenticationError">
|
||||||
</p>
|
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
<span class="small text-danger">{{ state.AuthenticationError }}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !login button -->
|
<!-- !login button -->
|
||||||
|
|
|
@ -1,113 +1,110 @@
|
||||||
angular.module('auth', [])
|
angular.module('auth', [])
|
||||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
|
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService',
|
||||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) {
|
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) {
|
||||||
|
|
||||||
$scope.authData = {
|
|
||||||
username: 'admin',
|
|
||||||
password: '',
|
|
||||||
error: ''
|
|
||||||
};
|
|
||||||
$scope.initPasswordData = {
|
|
||||||
password: '',
|
|
||||||
password_confirmation: '',
|
|
||||||
error: false
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
if (!$scope.applicationState.application.authentication) {
|
$scope.formValues = {
|
||||||
EndpointService.endpoints()
|
Username: '',
|
||||||
.then(function success(data) {
|
Password: ''
|
||||||
if (data.length > 0) {
|
|
||||||
endpointID = EndpointProvider.endpointID();
|
|
||||||
if (!endpointID) {
|
|
||||||
endpointID = data[0].Id;
|
|
||||||
EndpointProvider.setEndpointID(endpointID);
|
|
||||||
}
|
|
||||||
StateManager.updateEndpointState(true)
|
|
||||||
.then(function success() {
|
|
||||||
$state.go('dashboard');
|
|
||||||
}, function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$state.go('endpointInit');
|
|
||||||
}
|
|
||||||
}, function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Users.checkAdminUser({}, function () {},
|
|
||||||
function (e) {
|
|
||||||
if (e.status === 404) {
|
|
||||||
$scope.initPassword = true;
|
|
||||||
} else {
|
|
||||||
Notifications.error('Failure', e, 'Unable to verify administrator account existence');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($stateParams.logout) {
|
|
||||||
Authentication.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($stateParams.error) {
|
|
||||||
$scope.authData.error = $stateParams.error;
|
|
||||||
Authentication.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Authentication.isAuthenticated()) {
|
|
||||||
$state.go('dashboard');
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.createAdminUser = function() {
|
|
||||||
var password = $sanitize($scope.initPasswordData.password);
|
|
||||||
Users.initAdminUser({password: password}, function (d) {
|
|
||||||
$scope.initPassword = false;
|
|
||||||
$timeout(function() {
|
|
||||||
var element = $window.document.getElementById('password');
|
|
||||||
if(element) {
|
|
||||||
element.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, function (e) {
|
|
||||||
$scope.initPassword.error = true;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.authenticateUser = function() {
|
$scope.state = {
|
||||||
$scope.authenticationError = false;
|
AuthenticationError: ''
|
||||||
var username = $sanitize($scope.authData.username);
|
};
|
||||||
var password = $sanitize($scope.authData.password);
|
|
||||||
Authentication.login(username, password)
|
function setActiveEndpointAndRedirectToDashboard(endpoint) {
|
||||||
|
var endpointID = EndpointProvider.endpointID();
|
||||||
|
if (!endpointID) {
|
||||||
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
|
}
|
||||||
|
StateManager.updateEndpointState(true)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
return EndpointService.endpoints();
|
$state.go('dashboard');
|
||||||
})
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unauthenticatedFlow() {
|
||||||
|
EndpointService.endpoints()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var userDetails = Authentication.getUserDetails();
|
var endpoints = data;
|
||||||
if (data.length > 0) {
|
if (endpoints.length > 0) {
|
||||||
endpointID = EndpointProvider.endpointID();
|
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||||
if (!endpointID) {
|
} else {
|
||||||
endpointID = data[0].Id;
|
$state.go('init.endpoint');
|
||||||
EndpointProvider.setEndpointID(endpointID);
|
|
||||||
}
|
|
||||||
StateManager.updateEndpointState(true)
|
|
||||||
.then(function success() {
|
|
||||||
$state.go('dashboard');
|
|
||||||
}, function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (data.length === 0 && userDetails.role === 1) {
|
|
||||||
$state.go('endpointInit');
|
|
||||||
} else if (data.length === 0 && userDetails.role === 2) {
|
|
||||||
Authentication.logout();
|
|
||||||
$scope.authData.error = 'User not allowed. Please contact your administrator.';
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.authData.error = 'Authentication error';
|
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function authenticatedFlow() {
|
||||||
|
UserService.administratorExists()
|
||||||
|
.then(function success(exists) {
|
||||||
|
if (!exists) {
|
||||||
|
$state.go('init.admin');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to verify administrator account existence');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.authenticateUser = function() {
|
||||||
|
var username = $scope.formValues.Username;
|
||||||
|
var password = $scope.formValues.Password;
|
||||||
|
|
||||||
|
SettingsService.publicSettings()
|
||||||
|
.then(function success(data) {
|
||||||
|
var settings = data;
|
||||||
|
if (settings.AuthenticationMethod === 1) {
|
||||||
|
username = $sanitize(username);
|
||||||
|
password = $sanitize(password);
|
||||||
|
}
|
||||||
|
return Authentication.login(username, password);
|
||||||
|
})
|
||||||
|
.then(function success() {
|
||||||
|
return EndpointService.endpoints();
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
var endpoints = data;
|
||||||
|
var userDetails = Authentication.getUserDetails();
|
||||||
|
if (endpoints.length > 0) {
|
||||||
|
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||||
|
} else if (endpoints.length === 0 && userDetails.role === 1) {
|
||||||
|
$state.go('init.endpoint');
|
||||||
|
} else if (endpoints.length === 0 && userDetails.role === 2) {
|
||||||
|
Authentication.logout();
|
||||||
|
$scope.state.AuthenticationError = 'User not allowed. Please contact your administrator.';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function error() {
|
||||||
|
$scope.state.AuthenticationError = 'Invalid credentials';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
if ($stateParams.logout || $stateParams.error) {
|
||||||
|
Authentication.logout();
|
||||||
|
$scope.state.AuthenticationError = $stateParams.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Authentication.isAuthenticated()) {
|
||||||
|
$state.go('dashboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
|
if (!authenticationEnabled) {
|
||||||
|
unauthenticatedFlow();
|
||||||
|
} else {
|
||||||
|
authenticatedFlow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -20,8 +20,8 @@
|
||||||
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||||
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||||
<button class="btn btn-danger" ng-click="confirmRemove()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
<button class="btn btn-danger" ng-click="confirmRemove()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||||
<button class="btn btn-danger" ng-click="recreate()"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Recreate</button>
|
<button class="btn btn-danger" ng-click="recreate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Recreate</button>
|
||||||
<button class="btn btn-primary" ng-click="duplicate()"><i class="fa fa-files-o space-right" aria-hidden="true"></i>Duplicate/Edit</button>
|
<button class="btn btn-primary" ng-click="duplicate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-files-o space-right" aria-hidden="true"></i>Duplicate/Edit</button>
|
||||||
</div>
|
</div>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
|
|
|
@ -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-down"></span>
|
||||||
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Names' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
|
<a data-toggle="tooltip" title="More" ng-click="truncateMore();" ng-show="showMore">
|
||||||
|
<i class="fa fa-plus-square" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="containers" ng-click="order('Image')">
|
<a ui-sref="containers" ng-click="order('Image')">
|
||||||
|
@ -106,8 +109,8 @@
|
||||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) !== -1" class="label label-{{ container.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ container.Status }}</span>
|
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) !== -1" class="label label-{{ container.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ container.Status }}</span>
|
||||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) === -1" class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span>
|
<span ng-if="['starting','healthy','unhealthy'].indexOf(container.Status) === -1" class="label label-{{ container.Status|containerstatusbadge }}">{{ container.Status }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername|truncate: 40}}</a></td>
|
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|swarmcontainername|truncate: truncate_size}}</a></td>
|
||||||
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: 40}}</a></td>
|
<td ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM'"><a ui-sref="container({id: container.Id})">{{ container|containername|truncate: truncate_size}}</a></td>
|
||||||
<td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></td>
|
<td><a ui-sref="image({id: container.Image})">{{ container.Image | hideshasum }}</a></td>
|
||||||
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
<td ng-if="state.displayIP">{{ container.IP ? container.IP : '-' }}</td>
|
||||||
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
|
<td ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM'">{{ container.hostIP }}</td>
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
angular.module('containers', [])
|
angular.module('containers', [])
|
||||||
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'SystemService', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider',
|
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'SystemService', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider', 'LocalStorage',
|
||||||
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, SystemService, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
|
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, SystemService, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider, LocalStorage) {
|
||||||
$scope.state = {};
|
$scope.state = {};
|
||||||
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
|
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
|
||||||
$scope.state.displayAll = true;
|
$scope.state.displayAll = LocalStorage.getFilterContainerShowAll();
|
||||||
$scope.state.displayIP = false;
|
$scope.state.displayIP = false;
|
||||||
$scope.sortType = 'State';
|
$scope.sortType = 'State';
|
||||||
$scope.sortReverse = false;
|
$scope.sortReverse = false;
|
||||||
$scope.state.selectedItemCount = 0;
|
$scope.state.selectedItemCount = 0;
|
||||||
|
$scope.truncate_size = 40;
|
||||||
|
$scope.showMore = true;
|
||||||
|
|
||||||
$scope.order = function (sortType) {
|
$scope.order = function (sortType) {
|
||||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||||
$scope.sortType = sortType;
|
$scope.sortType = sortType;
|
||||||
|
@ -130,6 +133,7 @@ angular.module('containers', [])
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.toggleGetAll = function () {
|
$scope.toggleGetAll = function () {
|
||||||
|
LocalStorage.storeFilterContainerShowAll($scope.state.displayAll);
|
||||||
update({all: $scope.state.displayAll ? 1 : 0});
|
update({all: $scope.state.displayAll ? 1 : 0});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -161,6 +165,12 @@ angular.module('containers', [])
|
||||||
batch($scope.containers, Container.remove, 'Removed');
|
batch($scope.containers, Container.remove, 'Removed');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
$scope.truncateMore = function(size) {
|
||||||
|
$scope.truncate_size = 80;
|
||||||
|
$scope.showMore = false;
|
||||||
|
};
|
||||||
|
|
||||||
$scope.confirmRemoveAction = function () {
|
$scope.confirmRemoveAction = function () {
|
||||||
var isOneContainerRunning = false;
|
var isOneContainerRunning = false;
|
||||||
angular.forEach($scope.containers, function (c) {
|
angular.forEach($scope.containers, function (c) {
|
||||||
|
@ -205,7 +215,7 @@ angular.module('containers', [])
|
||||||
|
|
||||||
if(container.Status === 'paused') {
|
if(container.Status === 'paused') {
|
||||||
$scope.state.noPausedItemsSelected = false;
|
$scope.state.noPausedItemsSelected = false;
|
||||||
} else if(container.Status === 'stopped' ||
|
} else if(container.Status === 'stopped' ||
|
||||||
container.Status === 'created') {
|
container.Status === 'created') {
|
||||||
$scope.state.noStoppedItemsSelected = false;
|
$scope.state.noStoppedItemsSelected = false;
|
||||||
} else if(container.Status === 'running') {
|
} else if(container.Status === 'running') {
|
||||||
|
|
|
@ -403,7 +403,6 @@ function ($q, $scope, $state, $stateParams, $filter, Container, ContainerHelper,
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFromContainerImageConfig(d) {
|
function loadFromContainerImageConfig(d) {
|
||||||
// If no registry found, we let default DockerHub and let full image path
|
|
||||||
var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image);
|
var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image);
|
||||||
RegistryService.retrieveRegistryFromRepository($scope.config.Image)
|
RegistryService.retrieveRegistryFromRepository($scope.config.Image)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
|
|
|
@ -21,24 +21,30 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Image configuration
|
Image configuration
|
||||||
</div>
|
</div>
|
||||||
<!-- image-and-registry -->
|
<div ng-if="!formValues.Registry && fromContainer">
|
||||||
<div class="form-group">
|
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i>
|
||||||
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry>
|
<span class="small text-danger" style="margin-left: 5px;">The Docker registry for the <code>{{ config.Image }}</code> image is not registered inside Portainer, you will not be able to create a container. Please register that registry first.</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- !image-and-registry -->
|
<div ng-if="formValues.Registry || !fromContainer">
|
||||||
<!-- always-pull -->
|
<!-- image-and-registry -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<por-image-registry image="config.Image" registry="formValues.Registry" ng-if="formValues.Registry"></por-image-registry>
|
||||||
<label for="ownership" class="control-label text-left">
|
|
||||||
Always pull the image
|
|
||||||
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically try to pull the specified image before creating the container."></portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<label class="switch" style="margin-left: 20px;">
|
|
||||||
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- !image-and-registry -->
|
||||||
|
<!-- always-pull -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="ownership" class="control-label text-left">
|
||||||
|
Always pull the image
|
||||||
|
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically try to pull the specified image before creating the container."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" ng-model="formValues.alwaysPull"><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !always-pull -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !always-pull -->
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Ports configuration
|
Ports configuration
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,7 +112,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="create()">Start container</button>
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image || (!formValues.Registry && fromContainer)" ng-click="create()">Start container</button>
|
||||||
<a type="button" class="btn btn-default btn-sm" ui-sref="containers">Cancel</a>
|
<a type="button" class="btn btn-default btn-sm" ui-sref="containers">Cancel</a>
|
||||||
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
<i id="createContainerSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
angular.module('createNetwork', [])
|
angular.module('createNetwork', [])
|
||||||
.controller('CreateNetworkController', ['$scope', '$state', 'Notifications', 'Network', 'LabelHelper',
|
.controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
|
||||||
function ($scope, $state, Notifications, Network, LabelHelper) {
|
function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
DriverOptions: [],
|
DriverOptions: [],
|
||||||
Subnet: '',
|
Subnet: '',
|
||||||
Gateway: '',
|
Gateway: '',
|
||||||
Labels: []
|
Labels: [],
|
||||||
|
AccessControlData: new AccessControlFormData()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
formValidationError: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.availableNetworkDrivers = [];
|
||||||
|
|
||||||
$scope.config = {
|
$scope.config = {
|
||||||
Driver: 'bridge',
|
Driver: 'bridge',
|
||||||
CheckDuplicate: true,
|
CheckDuplicate: true,
|
||||||
|
@ -37,23 +45,6 @@ function ($scope, $state, Notifications, Network, LabelHelper) {
|
||||||
$scope.formValues.Labels.splice(index, 1);
|
$scope.formValues.Labels.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
function createNetwork(config) {
|
|
||||||
$('#createNetworkSpinner').show();
|
|
||||||
Network.create(config, function (d) {
|
|
||||||
if (d.message) {
|
|
||||||
$('#createNetworkSpinner').hide();
|
|
||||||
Notifications.error('Unable to create network', {}, d.message);
|
|
||||||
} else {
|
|
||||||
Notifications.success('Network created', d.Id);
|
|
||||||
$('#createNetworkSpinner').hide();
|
|
||||||
$state.go('networks', {}, {reload: true});
|
|
||||||
}
|
|
||||||
}, function (e) {
|
|
||||||
$('#createNetworkSpinner').hide();
|
|
||||||
Notifications.error('Failure', e, 'Unable to create network');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function prepareIPAMConfiguration(config) {
|
function prepareIPAMConfiguration(config) {
|
||||||
if ($scope.formValues.Subnet) {
|
if ($scope.formValues.Subnet) {
|
||||||
var ipamConfig = {};
|
var ipamConfig = {};
|
||||||
|
@ -85,8 +76,66 @@ function ($scope, $state, Notifications, Network, LabelHelper) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateForm(accessControlData, isAdmin) {
|
||||||
|
$scope.state.formValidationError = '';
|
||||||
|
var error = '';
|
||||||
|
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
$scope.state.formValidationError = error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.create = function () {
|
$scope.create = function () {
|
||||||
var config = prepareConfiguration();
|
$('#createResourceSpinner').show();
|
||||||
createNetwork(config);
|
|
||||||
|
var networkConfiguration = prepareConfiguration();
|
||||||
|
var accessControlData = $scope.formValues.AccessControlData;
|
||||||
|
var userDetails = Authentication.getUserDetails();
|
||||||
|
var isAdmin = userDetails.role === 1 ? true : false;
|
||||||
|
|
||||||
|
if (!validateForm(accessControlData, isAdmin)) {
|
||||||
|
$('#createResourceSpinner').hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NetworkService.create(networkConfiguration)
|
||||||
|
.then(function success(data) {
|
||||||
|
var networkIdentifier = data.Id;
|
||||||
|
var userId = userDetails.ID;
|
||||||
|
return ResourceControlService.applyResourceControl('network', networkIdentifier, userId, accessControlData, []);
|
||||||
|
})
|
||||||
|
.then(function success() {
|
||||||
|
Notifications.success('Network successfully created');
|
||||||
|
$state.go('networks', {}, {reload: true});
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'An error occured during network creation');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#createResourceSpinner').hide();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
$('#loadingViewSpinner').show();
|
||||||
|
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||||
|
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||||
|
if(endpointProvider !== 'DOCKER_SWARM') {
|
||||||
|
PluginService.networkPlugins(apiVersion < 1.25)
|
||||||
|
.then(function success(data){
|
||||||
|
$scope.availableNetworkDrivers = data;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve network drivers');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#loadingViewSpinner').hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title="Create network"></rd-header-title>
|
<rd-header-title title="Create network">
|
||||||
|
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||||
|
</rd-header-title>
|
||||||
<rd-header-content>
|
<rd-header-content>
|
||||||
<a ui-sref="networks">Networks</a> > Add network
|
<a ui-sref="networks">Networks</a> > Add network
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
|
@ -39,8 +41,11 @@
|
||||||
<!-- driver-input -->
|
<!-- driver-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="network_driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
<label for="network_driver" class="col-sm-2 col-lg-1 control-label text-left">Driver</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-11">
|
||||||
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName">
|
<select class="form-control" ng-options="driver for driver in availableNetworkDrivers" ng-model="config.Driver" ng-if="availableNetworkDrivers.length > 0">
|
||||||
|
<option disabled hidden value="">Select a driver</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" class="form-control" ng-model="config.Driver" id="network_driver" placeholder="e.g. driverName" ng-if="availableNetworkDrivers.length === 0">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !driver-input -->
|
<!-- !driver-input -->
|
||||||
|
@ -116,6 +121,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !internal -->
|
<!-- !internal -->
|
||||||
|
<!-- access-control -->
|
||||||
|
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||||
|
<!-- !access-control -->
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
|
@ -124,7 +132,8 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="create()">Create network</button>
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="create()">Create network</button>
|
||||||
<a type="button" class="btn btn-default btn-sm" ui-sref="networks">Cancel</a>
|
<a type="button" class="btn btn-default btn-sm" ui-sref="networks">Cancel</a>
|
||||||
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !actions -->
|
<!-- !actions -->
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
angular.module('createSecret', [])
|
angular.module('createSecret', [])
|
||||||
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper',
|
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
|
||||||
function ($scope, $state, Notifications, SecretService, LabelHelper) {
|
function ($scope, $state, Notifications, SecretService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
Data: '',
|
Data: '',
|
||||||
Labels: [],
|
Labels: [],
|
||||||
encodeSecret: true
|
encodeSecret: true,
|
||||||
|
AccessControlData: new AccessControlFormData()
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
formValidationError: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addLabel = function() {
|
$scope.addLabel = function() {
|
||||||
|
@ -36,10 +42,38 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSecret(config) {
|
function validateForm(accessControlData, isAdmin) {
|
||||||
$('#createSecretSpinner').show();
|
$scope.state.formValidationError = '';
|
||||||
SecretService.create(config)
|
var error = '';
|
||||||
|
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
$scope.state.formValidationError = error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.create = function () {
|
||||||
|
$('#createResourceSpinner').show();
|
||||||
|
|
||||||
|
var accessControlData = $scope.formValues.AccessControlData;
|
||||||
|
var userDetails = Authentication.getUserDetails();
|
||||||
|
var isAdmin = userDetails.role === 1 ? true : false;
|
||||||
|
|
||||||
|
if (!validateForm(accessControlData, isAdmin)) {
|
||||||
|
$('#createResourceSpinner').hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretConfiguration = prepareConfiguration();
|
||||||
|
SecretService.create(secretConfiguration)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
|
var secretIdentifier = data.ID;
|
||||||
|
var userId = userDetails.ID;
|
||||||
|
return ResourceControlService.applyResourceControl('secret', secretIdentifier, userId, accessControlData, []);
|
||||||
|
})
|
||||||
|
.then(function success() {
|
||||||
Notifications.success('Secret successfully created');
|
Notifications.success('Secret successfully created');
|
||||||
$state.go('secrets', {}, {reload: true});
|
$state.go('secrets', {}, {reload: true});
|
||||||
})
|
})
|
||||||
|
@ -47,12 +81,7 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
|
||||||
Notifications.error('Failure', err, 'Unable to create secret');
|
Notifications.error('Failure', err, 'Unable to create secret');
|
||||||
})
|
})
|
||||||
.finally(function final() {
|
.finally(function final() {
|
||||||
$('#createSecretSpinner').hide();
|
$('#createResourceSpinner').hide();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
$scope.create = function () {
|
|
||||||
var config = prepareConfiguration();
|
|
||||||
createSecret(config);
|
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -66,6 +66,9 @@
|
||||||
<!-- !labels-input-list -->
|
<!-- !labels-input-list -->
|
||||||
</div>
|
</div>
|
||||||
<!-- !labels-->
|
<!-- !labels-->
|
||||||
|
<!-- access-control -->
|
||||||
|
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
|
||||||
|
<!-- !access-control -->
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
|
@ -74,7 +77,8 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.Data" ng-click="create()">Create secret</button>
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.Data" ng-click="create()">Create secret</button>
|
||||||
<a type="button" class="btn btn-default btn-sm" ui-sref="secrets">Cancel</a>
|
<a type="button" class="btn btn-default btn-sm" ui-sref="secrets">Cancel</a>
|
||||||
<i id="createSecretSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
|
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !actions -->
|
<!-- !actions -->
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
|
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
|
||||||
// See app/components/templates/templatesController.js as a reference.
|
// See app/components/templates/templatesController.js as a reference.
|
||||||
angular.module('createService', [])
|
angular.module('createService', [])
|
||||||
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper',
|
.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'RegistryService', 'HttpRequestHelper', 'NodeService',
|
||||||
function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper) {
|
function ($q, $scope, $state, $timeout, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, RegistryService, HttpRequestHelper, NodeService) {
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
|
@ -28,13 +28,25 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
||||||
UpdateOrder: 'stop-first',
|
UpdateOrder: 'stop-first',
|
||||||
FailureAction: 'pause',
|
FailureAction: 'pause',
|
||||||
Secrets: [],
|
Secrets: [],
|
||||||
AccessControlData: new AccessControlFormData()
|
AccessControlData: new AccessControlFormData(),
|
||||||
|
CpuLimit: 0,
|
||||||
|
CpuReservation: 0,
|
||||||
|
MemoryLimit: 0,
|
||||||
|
MemoryReservation: 0,
|
||||||
|
MemoryLimitUnit: 'MB',
|
||||||
|
MemoryReservationUnit: 'MB'
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
formValidationError: ''
|
formValidationError: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.refreshSlider = function () {
|
||||||
|
$timeout(function () {
|
||||||
|
$scope.$broadcast('rzSliderForceRender');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.addPortBinding = function() {
|
$scope.addPortBinding = function() {
|
||||||
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
|
$scope.formValues.Ports.push({ PublishedPort: '', TargetPort: '', Protocol: 'tcp', PublishMode: 'ingress' });
|
||||||
};
|
};
|
||||||
|
@ -224,6 +236,38 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareResourcesCpuConfig(config, input) {
|
||||||
|
// CPU Limit
|
||||||
|
if (input.CpuLimit > 0) {
|
||||||
|
config.TaskTemplate.Resources.Limits.NanoCPUs = input.CpuLimit * 1000000000;
|
||||||
|
}
|
||||||
|
// CPU Reservation
|
||||||
|
if (input.CpuReservation > 0) {
|
||||||
|
config.TaskTemplate.Resources.Reservations.NanoCPUs = input.CpuReservation * 1000000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareResourcesMemoryConfig(config, input) {
|
||||||
|
// Memory Limit - Round to 0.125
|
||||||
|
var memoryLimit = (Math.round(input.MemoryLimit * 8) / 8).toFixed(3);
|
||||||
|
memoryLimit *= 1024 * 1024;
|
||||||
|
if (input.MemoryLimitUnit === 'GB') {
|
||||||
|
memoryLimit *= 1024;
|
||||||
|
}
|
||||||
|
if (memoryLimit > 0) {
|
||||||
|
config.TaskTemplate.Resources.Limits.MemoryBytes = memoryLimit;
|
||||||
|
}
|
||||||
|
// Memory Resevation - Round to 0.125
|
||||||
|
var memoryReservation = (Math.round(input.MemoryReservation * 8) / 8).toFixed(3);
|
||||||
|
memoryReservation *= 1024 * 1024;
|
||||||
|
if (input.MemoryReservationUnit === 'GB') {
|
||||||
|
memoryReservation *= 1024;
|
||||||
|
}
|
||||||
|
if (memoryReservation > 0) {
|
||||||
|
config.TaskTemplate.Resources.Reservations.MemoryBytes = memoryReservation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function prepareConfiguration() {
|
function prepareConfiguration() {
|
||||||
var input = $scope.formValues;
|
var input = $scope.formValues;
|
||||||
var config = {
|
var config = {
|
||||||
|
@ -232,7 +276,11 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
||||||
ContainerSpec: {
|
ContainerSpec: {
|
||||||
Mounts: []
|
Mounts: []
|
||||||
},
|
},
|
||||||
Placement: {}
|
Placement: {},
|
||||||
|
Resources: {
|
||||||
|
Limits: {},
|
||||||
|
Reservations: {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Mode: {},
|
Mode: {},
|
||||||
EndpointSpec: {}
|
EndpointSpec: {}
|
||||||
|
@ -248,6 +296,8 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
||||||
prepareUpdateConfig(config, input);
|
prepareUpdateConfig(config, input);
|
||||||
prepareSecretConfig(config, input);
|
prepareSecretConfig(config, input);
|
||||||
preparePlacementConfig(config, input);
|
preparePlacementConfig(config, input);
|
||||||
|
prepareResourcesCpuConfig(config, input);
|
||||||
|
prepareResourcesMemoryConfig(config, input);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,16 +355,30 @@ function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretServic
|
||||||
function initView() {
|
function initView() {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||||
|
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
volumes: VolumeService.volumes(),
|
volumes: VolumeService.volumes(),
|
||||||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||||
networks: NetworkService.networks(true, true, false, false)
|
networks: NetworkService.networks(true, true, false, false),
|
||||||
|
nodes: NodeService.nodes()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.availableVolumes = data.volumes;
|
$scope.availableVolumes = data.volumes;
|
||||||
$scope.availableNetworks = data.networks;
|
$scope.availableNetworks = data.networks;
|
||||||
$scope.availableSecrets = data.secrets;
|
$scope.availableSecrets = data.secrets;
|
||||||
|
// Set max cpu value
|
||||||
|
var maxCpus = 0;
|
||||||
|
for (var n in data.nodes) {
|
||||||
|
if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) {
|
||||||
|
maxCpus = data.nodes[n].CPUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxCpus > 0) {
|
||||||
|
$scope.state.sliderMaxCpu = maxCpus / 1000000000;
|
||||||
|
} else {
|
||||||
|
$scope.state.sliderMaxCpu = 32;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to initialize view');
|
Notifications.error('Failure', err, 'Unable to initialize view');
|
||||||
|
|
|
@ -133,7 +133,7 @@
|
||||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||||
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li>
|
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li>
|
||||||
<li class="interactive" ng-if="applicationState.endpoint.apiVersion >= 1.25"><a data-target="#secrets" data-toggle="tab">Secrets</a></li>
|
<li class="interactive" ng-if="applicationState.endpoint.apiVersion >= 1.25"><a data-target="#secrets" data-toggle="tab">Secrets</a></li>
|
||||||
<li class="interactive"><a data-target="#placement" data-toggle="tab">Placement</a></li>
|
<li class="interactive"><a data-target="#resources-placement" data-toggle="tab" ng-click="refreshSlider()">Resources & Placement</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- tab-content -->
|
<!-- tab-content -->
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
@ -442,9 +442,9 @@
|
||||||
<!-- tab-secrets -->
|
<!-- tab-secrets -->
|
||||||
<div class="tab-pane" id="secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" ng-include="'app/components/createService/includes/secret.html'"></div>
|
<div class="tab-pane" id="secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" ng-include="'app/components/createService/includes/secret.html'"></div>
|
||||||
<!-- !tab-secrets -->
|
<!-- !tab-secrets -->
|
||||||
<!-- tab-placement -->
|
<!-- tab-resources-placement -->
|
||||||
<div class="tab-pane" id="placement" ng-include="'app/components/createService/includes/placement.html'"></div>
|
<div class="tab-pane" id="resources-placement" ng-include="'app/components/createService/includes/resources-placement.html'"></div>
|
||||||
<!-- !tab-placement -->
|
<!-- !tab-resources-placement -->
|
||||||
</div>
|
</div>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
|
|
|
@ -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">
|
<div class="widget-icon blue pull-left">
|
||||||
<i class="fa fa-cubes"></i>
|
<i class="fa fa-cubes"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right" ng-if="infoData.Driver">
|
|
||||||
<div><i class="fa fa-hdd-o space-right"></i>{{ infoData.Driver }} driver</div>
|
|
||||||
</div>
|
|
||||||
<div class="title">{{ volumeData.total }}</div>
|
<div class="title">{{ volumeData.total }}</div>
|
||||||
<div class="comment">Volumes</div>
|
<div class="comment">Volumes</div>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Configuration
|
||||||
|
</div>
|
||||||
<!-- name-input -->
|
<!-- name-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||||
|
@ -42,73 +45,19 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-public-url-input -->
|
<!-- !endpoint-public-url-input -->
|
||||||
<!-- tls-checkbox -->
|
<!-- endpoint-security -->
|
||||||
<div class="form-group" ng-if="endpointType === 'remote'">
|
<div ng-if="endpointType === 'remote'">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12 form-section-title">
|
||||||
<label for="tls" class="control-label text-left">
|
Security
|
||||||
TLS
|
|
||||||
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<label class="switch" style="margin-left: 20px;">
|
|
||||||
<input type="checkbox" ng-model="endpoint.TLS"><i></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
<por-endpoint-security form-data="formValues.SecurityFormData" endpoint="endpoint"></por-endpoint-security>
|
||||||
</div>
|
</div>
|
||||||
<!-- !tls-checkbox -->
|
<!-- !endpoint-security -->
|
||||||
<!-- tls-certs -->
|
|
||||||
<div ng-if="endpoint.TLS">
|
|
||||||
<!-- ca-input -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
|
||||||
<span style="margin-left: 5px;">
|
|
||||||
<span ng-if="formValues.TLSCACert !== endpoint.TLSCACert">{{ formValues.TLSCACert.name }}</span>
|
|
||||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSCACert && formValues.TLSCACert === endpoint.TLSCACert" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !ca-input -->
|
|
||||||
<!-- cert-input -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
|
||||||
<span style="margin-left: 5px;">
|
|
||||||
<span ng-if="formValues.TLSCert !== endpoint.TLSCert">{{ formValues.TLSCert.name }}</span>
|
|
||||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSCert && formValues.TLSCert === endpoint.TLSCert" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !cert-input -->
|
|
||||||
<!-- key-input -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-2 control-label text-left">TLS key</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
|
||||||
<span style="margin-left: 5px;">
|
|
||||||
<span ng-if="formValues.TLSKey !== endpoint.TLSKey">{{ formValues.TLSKey.name }}</span>
|
|
||||||
<i class="fa fa-check green-icon" ng-if="formValues.TLSKey && formValues.TLSKey === endpoint.TLSKey" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !key-input -->
|
|
||||||
</div>
|
|
||||||
<!-- !tls-certs -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="updateEndpoint()">Update endpoint</button>
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && ((endpoint.TLSVerify && !formValues.TLSCACert) || (endpoint.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="updateEndpoint()">Update endpoint</button>
|
||||||
<a type="button" class="btn btn-default btn-sm" ui-sref="endpoints">Cancel</a>
|
<a type="button" class="btn btn-default btn-sm" ui-sref="endpoints">Cancel</a>
|
||||||
<i id="updateEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
<i id="updateResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -7,35 +7,41 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
error: '',
|
|
||||||
uploadInProgress: false
|
uploadInProgress: false
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
TLSCACert: null,
|
SecurityFormData: new EndpointSecurityFormData()
|
||||||
TLSCert: null,
|
|
||||||
TLSKey: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.updateEndpoint = function() {
|
$scope.updateEndpoint = function() {
|
||||||
var ID = $scope.endpoint.Id;
|
var endpoint = $scope.endpoint;
|
||||||
|
var securityData = $scope.formValues.SecurityFormData;
|
||||||
|
var TLS = securityData.TLS;
|
||||||
|
var TLSMode = securityData.TLSMode;
|
||||||
|
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||||
|
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||||
|
|
||||||
var endpointParams = {
|
var endpointParams = {
|
||||||
name: $scope.endpoint.Name,
|
name: endpoint.Name,
|
||||||
URL: $scope.endpoint.URL,
|
URL: endpoint.URL,
|
||||||
PublicURL: $scope.endpoint.PublicURL,
|
PublicURL: endpoint.PublicURL,
|
||||||
TLS: $scope.endpoint.TLS,
|
TLS: TLS,
|
||||||
TLSCACert: $scope.formValues.TLSCACert !== $scope.endpoint.TLSCACert ? $scope.formValues.TLSCACert : null,
|
TLSSkipVerify: TLSSkipVerify,
|
||||||
TLSCert: $scope.formValues.TLSCert !== $scope.endpoint.TLSCert ? $scope.formValues.TLSCert : null,
|
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||||
TLSKey: $scope.formValues.TLSKey !== $scope.endpoint.TLSKey ? $scope.formValues.TLSKey : null,
|
TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert,
|
||||||
|
TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert,
|
||||||
|
TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey,
|
||||||
type: $scope.endpointType
|
type: $scope.endpointType
|
||||||
};
|
};
|
||||||
|
|
||||||
EndpointService.updateEndpoint(ID, endpointParams)
|
$('updateResourceSpinner').show();
|
||||||
|
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||||
$state.go('endpoints');
|
$state.go('endpoints');
|
||||||
}, function error(err) {
|
}, function error(err) {
|
||||||
$scope.state.error = err.msg;
|
Notifications.error('Failure', err, 'Unable to update endpoint');
|
||||||
}, function update(evt) {
|
}, function update(evt) {
|
||||||
if (evt.upload) {
|
if (evt.upload) {
|
||||||
$scope.state.uploadInProgress = evt.upload;
|
$scope.state.uploadInProgress = evt.upload;
|
||||||
|
@ -43,25 +49,27 @@ function ($scope, $state, $stateParams, $filter, EndpointService, Notifications)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function getEndpoint(endpointID) {
|
function initView() {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
EndpointService.endpoint($stateParams.id).then(function success(data) {
|
EndpointService.endpoint($stateParams.id)
|
||||||
$('#loadingViewSpinner').hide();
|
.then(function success(data) {
|
||||||
$scope.endpoint = data;
|
var endpoint = data;
|
||||||
if (data.URL.indexOf('unix://') === 0) {
|
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||||
|
$scope.endpoint = endpoint;
|
||||||
|
|
||||||
|
if (endpoint.URL.indexOf('unix://') === 0) {
|
||||||
$scope.endpointType = 'local';
|
$scope.endpointType = 'local';
|
||||||
} else {
|
} else {
|
||||||
$scope.endpointType = 'remote';
|
$scope.endpointType = 'remote';
|
||||||
}
|
}
|
||||||
$scope.endpoint.URL = $filter('stripprotocol')(data.URL);
|
})
|
||||||
$scope.formValues.TLSCACert = data.TLSCACert;
|
.catch(function error(err) {
|
||||||
$scope.formValues.TLSCert = data.TLSCert;
|
|
||||||
$scope.formValues.TLSKey = data.TLSKey;
|
|
||||||
}, function error(err) {
|
|
||||||
$('#loadingViewSpinner').hide();
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#loadingViewSpinner').hide();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getEndpoint($stateParams.id);
|
initView();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-public-url-input -->
|
<!-- !endpoint-public-url-input -->
|
||||||
<!-- tls-checkbox -->
|
<!-- endpoint-security -->
|
||||||
|
<por-endpoint-security form-data="formValues.SecurityFormData"></por-endpoint-security>
|
||||||
|
<!-- !endpoint-security -->
|
||||||
|
<!-- actions -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label for="tls" class="control-label text-left">
|
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && ((formValues.TLSVerify && !formValues.TLSCACert) || (formValues.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="addEndpoint()"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</button>
|
||||||
TLS
|
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||||
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
|
|
||||||
</label>
|
|
||||||
<label class="switch" style="margin-left: 20px;">
|
|
||||||
<input type="checkbox" ng-model="formValues.TLS"><i></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- !tls-checkbox -->
|
</div>
|
||||||
<!-- tls-certs -->
|
<!-- !actions -->
|
||||||
<div ng-if="formValues.TLS">
|
</form>
|
||||||
<!-- ca-input -->
|
</rd-widget-body>
|
||||||
<div class="form-group">
|
</rd-widget>
|
||||||
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
|
</div>
|
||||||
<div class="col-sm-10">
|
|
||||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
|
|
||||||
<span style="margin-left: 5px;">
|
|
||||||
{{ formValues.TLSCACert.name }}
|
|
||||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !ca-input -->
|
|
||||||
<!-- cert-input -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
|
|
||||||
<span style="margin-left: 5px;">
|
|
||||||
{{ formValues.TLSCert.name }}
|
|
||||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !cert-input -->
|
|
||||||
<!-- key-input -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-2 control-label text-left">TLS key</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
|
|
||||||
<span style="margin-left: 5px;">
|
|
||||||
{{ formValues.TLSKey.name }}
|
|
||||||
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
|
|
||||||
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !key-input -->
|
|
||||||
</div>
|
|
||||||
<!-- !tls-certs -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="addEndpoint()">Add endpoint</button>
|
|
||||||
<i id="createEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
|
||||||
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
|
|
||||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -191,7 +137,7 @@
|
||||||
<span ng-if="applicationState.application.authentication">
|
<span ng-if="applicationState.application.authentication">
|
||||||
<a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
|
<a ui-sref="endpoint.access({id: endpoint.Id})"><i class="fa fa-users" aria-hidden="true" style="margin-left: 7px;"></i> Manage access</a>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!endpoints">
|
<tr ng-if="!endpoints">
|
||||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||||
|
|
|
@ -2,7 +2,6 @@ angular.module('endpoints', [])
|
||||||
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination',
|
.controller('EndpointsController', ['$scope', '$state', 'EndpointService', 'EndpointProvider', 'Notifications', 'Pagination',
|
||||||
function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagination) {
|
function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagination) {
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
error: '',
|
|
||||||
uploadInProgress: false,
|
uploadInProgress: false,
|
||||||
selectedItemCount: 0,
|
selectedItemCount: 0,
|
||||||
pagination_count: Pagination.getPaginationCount('endpoints')
|
pagination_count: Pagination.getPaginationCount('endpoints')
|
||||||
|
@ -14,10 +13,7 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
|
||||||
Name: '',
|
Name: '',
|
||||||
URL: '',
|
URL: '',
|
||||||
PublicURL: '',
|
PublicURL: '',
|
||||||
TLS: false,
|
SecurityFormData: new EndpointSecurityFormData()
|
||||||
TLSCACert: null,
|
|
||||||
TLSCert: null,
|
|
||||||
TLSKey: null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.order = function(sortType) {
|
$scope.order = function(sortType) {
|
||||||
|
@ -47,23 +43,28 @@ function ($scope, $state, EndpointService, EndpointProvider, Notifications, Pagi
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addEndpoint = function() {
|
$scope.addEndpoint = function() {
|
||||||
$scope.state.error = '';
|
|
||||||
var name = $scope.formValues.Name;
|
var name = $scope.formValues.Name;
|
||||||
var URL = $scope.formValues.URL;
|
var URL = $scope.formValues.URL;
|
||||||
var PublicURL = $scope.formValues.PublicURL;
|
var PublicURL = $scope.formValues.PublicURL;
|
||||||
if (PublicURL === '') {
|
if (PublicURL === '') {
|
||||||
PublicURL = URL.split(':')[0];
|
PublicURL = URL.split(':')[0];
|
||||||
}
|
}
|
||||||
var TLS = $scope.formValues.TLS;
|
|
||||||
var TLSCAFile = $scope.formValues.TLSCACert;
|
var securityData = $scope.formValues.SecurityFormData;
|
||||||
var TLSCertFile = $scope.formValues.TLSCert;
|
var TLS = securityData.TLS;
|
||||||
var TLSKeyFile = $scope.formValues.TLSKey;
|
var TLSMode = securityData.TLSMode;
|
||||||
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile, false).then(function success(data) {
|
var TLSSkipVerify = TLS && (TLSMode === 'tls_client_noca' || TLSMode === 'tls_only');
|
||||||
|
var TLSSkipClientVerify = TLS && (TLSMode === 'tls_ca' || TLSMode === 'tls_only');
|
||||||
|
var TLSCAFile = TLSSkipVerify ? null : securityData.TLSCACert;
|
||||||
|
var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert;
|
||||||
|
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||||
|
|
||||||
|
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile).then(function success(data) {
|
||||||
Notifications.success('Endpoint created', name);
|
Notifications.success('Endpoint created', name);
|
||||||
$state.reload();
|
$state.reload();
|
||||||
}, function error(err) {
|
}, function error(err) {
|
||||||
$scope.state.uploadInProgress = false;
|
$scope.state.uploadInProgress = false;
|
||||||
$scope.state.error = err.msg;
|
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||||
}, function update(evt) {
|
}, function update(evt) {
|
||||||
if (evt.upload) {
|
if (evt.upload) {
|
||||||
$scope.state.uploadInProgress = evt.upload;
|
$scope.state.uploadInProgress = evt.upload;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title="Engine overview">
|
<rd-header-title title="Engine overview">
|
||||||
<a data-toggle="tooltip" title="Refresh" ui-sref="docker" ui-sref-opts="{reload: true}">
|
<a data-toggle="tooltip" title="Refresh" ui-sref="engine" ui-sref-opts="{reload: true}">
|
||||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
|
@ -1,9 +1,7 @@
|
||||||
angular.module('docker', [])
|
angular.module('engine', [])
|
||||||
.controller('DockerController', ['$q', '$scope', 'SystemService', 'Notifications',
|
.controller('EngineController', ['$q', '$scope', 'SystemService', 'Notifications',
|
||||||
function ($q, $scope, SystemService, Notifications) {
|
function ($q, $scope, SystemService, Notifications) {
|
||||||
$scope.info = {};
|
|
||||||
$scope.version = {};
|
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
$q.all({
|
$q.all({
|
||||||
|
@ -15,6 +13,8 @@ function ($q, $scope, SystemService, Notifications) {
|
||||||
$scope.info = data.info;
|
$scope.info = data.info;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
$scope.info = {};
|
||||||
|
$scope.version = {};
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve engine details');
|
Notifications.error('Failure', err, 'Unable to retrieve engine details');
|
||||||
})
|
})
|
||||||
.finally(function final() {
|
.finally(function final() {
|
|
@ -70,7 +70,7 @@
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||||
</div>
|
</div>
|
||||||
<span class="btn-group btn-group-sm pull-right" style="margin-right: 20px;" ng-if="applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
|
<span class="btn-group btn-group-sm pull-right" style="margin-right: 20px;">
|
||||||
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="undefined">
|
<label class="btn btn-primary" ng-model="state.containersCountFilter" uib-btn-radio="undefined">
|
||||||
All
|
All
|
||||||
</label>
|
</label>
|
||||||
|
@ -121,12 +121,12 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="image in (state.filteredImages = (images | filter:{ Containers: state.containersCountFilter } | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
<tr dir-paginate="image in (state.filteredImages = (images | filter:{ ContainerCount: state.containersCountFilter } | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||||
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
|
<td><input type="checkbox" ng-model="image.Checked" ng-change="selectItem(image)" /></td>
|
||||||
<td>
|
<td>
|
||||||
<a class="monospaced" ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a>
|
<a class="monospaced" ui-sref="image({id: image.Id})">{{ image.Id|truncate:20}}</a>
|
||||||
<span style="margin-left: 10px;" class="label label-warning image-tag"
|
<span style="margin-left: 10px;" class="label label-warning image-tag"
|
||||||
ng-if="::image.Containers === 0 && applicationState.endpoint.mode.provider !== 'DOCKER_SWARM' && applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
|
ng-if="::image.ContainerCount === 0">
|
||||||
Unused
|
Unused
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -95,7 +95,7 @@ function ($scope, $state, ImageService, Notifications, Pagination, ModalService)
|
||||||
$('#loadImagesSpinner').show();
|
$('#loadImagesSpinner').show();
|
||||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||||
ImageService.images(apiVersion >= 1.25 && endpointProvider !== 'DOCKER_SWARM' && endpointProvider !== 'VMWARE_VIC')
|
ImageService.images(true)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.images = data;
|
$scope.images = data;
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- access-control-panel -->
|
||||||
|
<por-access-control-panel
|
||||||
|
ng-if="network && applicationState.application.authentication"
|
||||||
|
resource-id="network.Id"
|
||||||
|
resource-control="network.ResourceControl"
|
||||||
|
resource-type="'network'">
|
||||||
|
</por-access-control-panel>
|
||||||
|
<!-- !access-control-panel -->
|
||||||
|
|
||||||
<div class="row" ng-if="!(network.Options | emptyobject)">
|
<div class="row" ng-if="!(network.Options | emptyobject)">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('network', [])
|
angular.module('network', [])
|
||||||
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'Container', 'ContainerHelper', 'Notifications',
|
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'NetworkService', 'Container', 'ContainerHelper', 'Notifications',
|
||||||
function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHelper, Notifications) {
|
function ($scope, $state, $stateParams, $filter, Network, NetworkService, Container, ContainerHelper, Notifications) {
|
||||||
|
|
||||||
$scope.removeNetwork = function removeNetwork(networkId) {
|
$scope.removeNetwork = function removeNetwork(networkId) {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
|
@ -82,7 +82,7 @@ function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHe
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
Network.get({id: $stateParams.id}).$promise
|
NetworkService.network($stateParams.id)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.network = data;
|
$scope.network = data;
|
||||||
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
var endpointProvider = $scope.applicationState.endpoint.mode.provider;
|
||||||
|
|
|
@ -8,46 +8,6 @@
|
||||||
<rd-header-content>Networks</rd-header-content>
|
<rd-header-content>Networks</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
|
||||||
<rd-widget>
|
|
||||||
<rd-widget-header icon="fa-plus" title="Add a network">
|
|
||||||
</rd-widget-header>
|
|
||||||
<rd-widget-body>
|
|
||||||
<form class="form-horizontal">
|
|
||||||
<!-- name-input -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="network_name" class="col-sm-1 control-label text-left">Name</label>
|
|
||||||
<div class="col-sm-11">
|
|
||||||
<input type="text" class="form-control" ng-model="config.Name" id="network_name" placeholder="e.g. myNetwork">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !name-input -->
|
|
||||||
<!-- tag-note -->
|
|
||||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<span class="small text-muted">Note: The network will be created using the overlay driver and will allow containers to communicate across the hosts of your cluster.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<span class="small text-muted">Note: The network will be created using the bridge driver.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !tag-note -->
|
|
||||||
<div class="form-group">
|
|
||||||
<div class="col-sm-12">
|
|
||||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Name" ng-click="createNetwork()">Create</button>
|
|
||||||
<button type="button" class="btn btn-primary btn-sm" ui-sref="actions.create.network">Advanced settings...</button>
|
|
||||||
<i id="createNetworkSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
|
@ -66,6 +26,7 @@
|
||||||
<rd-widget-taskbar classes="col-lg-12">
|
<rd-widget-taskbar classes="col-lg-12">
|
||||||
<div class="pull-left">
|
<div class="pull-left">
|
||||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||||
|
<a class="btn btn-primary" type="button" ui-sref="actions.create.network"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add network</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||||
|
@ -80,54 +41,61 @@
|
||||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="networks" ng-click="order('Name')">
|
<a ng-click="order('Name')">
|
||||||
Name
|
Name
|
||||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="networks" ng-click="order('Id')">
|
<a ng-click="order('Id')">
|
||||||
Id
|
Id
|
||||||
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'Id' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Id' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="networks" ng-click="order('Scope')">
|
<a ng-click="order('Scope')">
|
||||||
Scope
|
Scope
|
||||||
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'Scope' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Scope' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="networks" ng-click="order('Driver')">
|
<a ng-click="order('Driver')">
|
||||||
Driver
|
Driver
|
||||||
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="networks" ng-click="order('IPAM.Driver')">
|
<a ng-click="order('IPAM.Driver')">
|
||||||
IPAM Driver
|
IPAM Driver
|
||||||
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'IPAM.Driver' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'IPAM.Driver' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Subnet')">
|
<a ng-click="order('IPAM.Config[0].Subnet')">
|
||||||
IPAM Subnet
|
IPAM Subnet
|
||||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'IPAM.Config[0].Subnet' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="networks" ng-click="order('IPAM.Config[0].Gateway')">
|
<a ng-click="order('IPAM.Config[0].Gateway')">
|
||||||
IPAM Gateway
|
IPAM Gateway
|
||||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'IPAM.Config[0].Gateway' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th ng-if="applicationState.application.authentication">
|
||||||
|
<a ng-click="order('ResourceControl.Ownership')">
|
||||||
|
Ownership
|
||||||
|
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -140,12 +108,18 @@
|
||||||
<td>{{ network.IPAM.Driver }}</td>
|
<td>{{ network.IPAM.Driver }}</td>
|
||||||
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
|
<td>{{ network.IPAM.Config[0].Subnet ? network.IPAM.Config[0].Subnet : '-' }}</td>
|
||||||
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
|
<td>{{ network.IPAM.Config[0].Gateway ? network.IPAM.Config[0].Gateway : '-' }}</td>
|
||||||
|
<td ng-if="applicationState.application.authentication">
|
||||||
|
<span>
|
||||||
|
<i ng-class="network.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
|
{{ network.ResourceControl.Ownership ? network.ResourceControl.Ownership : network.ResourceControl.Ownership = 'public' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!networks">
|
<tr ng-if="!networks">
|
||||||
<td colspan="8" class="text-center text-muted">Loading...</td>
|
<td colspan="9" class="text-center text-muted">Loading...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="networks.length == 0">
|
<tr ng-if="networks.length == 0">
|
||||||
<td colspan="8" class="text-center text-muted">No networks available.</td>
|
<td colspan="9" class="text-center text-muted">No networks available.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,51 +1,17 @@
|
||||||
angular.module('networks', [])
|
angular.module('networks', [])
|
||||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Notifications', 'Pagination',
|
.controller('NetworksController', ['$scope', '$state', 'Network', 'NetworkService', 'Notifications', 'Pagination',
|
||||||
function ($scope, $state, Network, Notifications, Pagination) {
|
function ($scope, $state, Network, NetworkService, Notifications, Pagination) {
|
||||||
$scope.state = {};
|
$scope.state = {};
|
||||||
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
|
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
|
||||||
$scope.state.selectedItemCount = 0;
|
$scope.state.selectedItemCount = 0;
|
||||||
$scope.state.advancedSettings = false;
|
$scope.state.advancedSettings = false;
|
||||||
$scope.sortType = 'Name';
|
$scope.sortType = 'Name';
|
||||||
$scope.sortReverse = false;
|
$scope.sortReverse = false;
|
||||||
$scope.config = {
|
|
||||||
Name: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.changePaginationCount = function() {
|
$scope.changePaginationCount = function() {
|
||||||
Pagination.setPaginationCount('networks', $scope.state.pagination_count);
|
Pagination.setPaginationCount('networks', $scope.state.pagination_count);
|
||||||
};
|
};
|
||||||
|
|
||||||
function prepareNetworkConfiguration() {
|
|
||||||
var config = angular.copy($scope.config);
|
|
||||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
|
|
||||||
config.Driver = 'overlay';
|
|
||||||
// Force IPAM Driver to 'default', should not be required.
|
|
||||||
// See: https://github.com/docker/docker/issues/25735
|
|
||||||
config.IPAM = {
|
|
||||||
Driver: 'default'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.createNetwork = function() {
|
|
||||||
$('#createNetworkSpinner').show();
|
|
||||||
var config = prepareNetworkConfiguration();
|
|
||||||
Network.create(config, function (d) {
|
|
||||||
if (d.message) {
|
|
||||||
$('#createNetworkSpinner').hide();
|
|
||||||
Notifications.error('Unable to create network', {}, d.message);
|
|
||||||
} else {
|
|
||||||
Notifications.success('Network created', d.Id);
|
|
||||||
$('#createNetworkSpinner').hide();
|
|
||||||
$state.reload();
|
|
||||||
}
|
|
||||||
}, function (e) {
|
|
||||||
$('#createNetworkSpinner').hide();
|
|
||||||
Notifications.error('Failure', e, 'Unable to create network');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.order = function(sortType) {
|
$scope.order = function(sortType) {
|
||||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||||
$scope.sortType = sortType;
|
$scope.sortType = sortType;
|
||||||
|
@ -99,13 +65,17 @@ function ($scope, $state, Network, Notifications, Pagination) {
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$('#loadNetworksSpinner').show();
|
$('#loadNetworksSpinner').show();
|
||||||
Network.query({}, function (d) {
|
|
||||||
$scope.networks = d;
|
NetworkService.networks(true, true, true, true)
|
||||||
$('#loadNetworksSpinner').hide();
|
.then(function success(data) {
|
||||||
}, function (e) {
|
$scope.networks = data;
|
||||||
$('#loadNetworksSpinner').hide();
|
})
|
||||||
Notifications.error('Failure', e, 'Unable to retrieve networks');
|
.catch(function error(err) {
|
||||||
$scope.networks = [];
|
$scope.networks = [];
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve networks');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$('#loadNetworksSpinner').hide();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,3 +53,12 @@
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- access-control-panel -->
|
||||||
|
<por-access-control-panel
|
||||||
|
ng-if="secret && applicationState.application.authentication"
|
||||||
|
resource-id="secret.Id"
|
||||||
|
resource-control="secret.ResourceControl"
|
||||||
|
resource-type="'secret'">
|
||||||
|
</por-access-control-panel>
|
||||||
|
<!-- !access-control-panel -->
|
||||||
|
|
|
@ -30,31 +30,44 @@
|
||||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="secrets" ng-click="order('Name')">
|
<a ng-click="order('Name')">
|
||||||
Name
|
Name
|
||||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<a ui-sref="secrets" ng-click="order('CreatedAt')">
|
<a ng-click="order('CreatedAt')">
|
||||||
Created at
|
Created at
|
||||||
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
</a>
|
</a>
|
||||||
</th>
|
</th>
|
||||||
|
<th ng-if="applicationState.application.authentication">
|
||||||
|
<a ng-click="order('ResourceControl.Ownership')">
|
||||||
|
Ownership
|
||||||
|
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||||
|
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="secret in (state.filteredSecrets = ( secrets | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
<tr dir-paginate="secret in (state.filteredSecrets = ( secrets | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||||
<td><input type="checkbox" ng-model="secret.Checked" ng-change="selectItem(secret)"/></td>
|
<td><input type="checkbox" ng-model="secret.Checked" ng-change="selectItem(secret)"/></td>
|
||||||
<td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
|
<td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
|
||||||
<td>{{ secret.CreatedAt | getisodate }}</td>
|
<td>{{ secret.CreatedAt | getisodate }}</td>
|
||||||
|
<td ng-if="applicationState.application.authentication">
|
||||||
|
<span>
|
||||||
|
<i ng-class="secret.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
|
{{ secret.ResourceControl.Ownership ? secret.ResourceControl.Ownership : secret.ResourceControl.Ownership = 'public' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!secrets">
|
<tr ng-if="!secrets">
|
||||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
<td colspan="4" class="text-center text-muted">Loading...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="secrets.length == 0">
|
<tr ng-if="secrets.length == 0">
|
||||||
<td colspan="3" class="text-center text-muted">No secrets available.</td>
|
<td colspan="4" class="text-center text-muted">No secrets available.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -6,31 +6,77 @@
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>CPU limits</td>
|
<td style="vertical-align : middle;">
|
||||||
<td ng-if="service.LimitNanoCPUs">
|
Memory reservation (MB)
|
||||||
{{ service.LimitNanoCPUs / 1000000000 }}
|
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="!service.LimitNanoCPUs">None</td>
|
<td>
|
||||||
</tr>
|
<input class="input-sm" type="number" step="0.125" min="0" ng-model="service.ReservationMemoryBytes" ng-change="updateServiceAttribute(service, 'ReservationMemoryBytes')" ng-disabled="isUpdating"/>
|
||||||
<tr>
|
</td>
|
||||||
<td>Memory limits</td>
|
<td style="vertical-align : middle;">
|
||||||
<td ng-if="service.LimitMemoryBytes">{{service.LimitMemoryBytes|humansize}}</td>
|
<p class="small text-muted">
|
||||||
<td ng-if="!service.LimitMemoryBytes">None</td>
|
Minimum memory available on a node to run a task (set to 0 for unlimited)
|
||||||
</tr>
|
</p>
|
||||||
<tr>
|
|
||||||
<td>CPU reservation</td>
|
|
||||||
<td ng-if="service.ReservationNanoCPUs">
|
|
||||||
{{service.ReservationNanoCPUs / 1000000000}}
|
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="!service.ReservationNanoCPUs">None</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Memory reservation</td>
|
<td style="vertical-align : middle;">
|
||||||
<td ng-if="service.ReservationMemoryBytes">{{service.ReservationMemoryBytes|humansize}}</td>
|
Memory limit (MB)
|
||||||
<td ng-if="!service.ReservationMemoryBytes">None</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<input class="input-sm" type="number" step="0.125" min="0" ng-model="service.LimitMemoryBytes" ng-change="updateServiceAttribute(service, 'LimitMemoryBytes')" ng-disabled="isUpdating"/>
|
||||||
|
</td>
|
||||||
|
<td style="vertical-align : middle;">
|
||||||
|
<p class="small text-muted">
|
||||||
|
Maximum memory usage per task (set to 0 for unlimited)
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align : middle;">
|
||||||
|
<div>
|
||||||
|
CPU reservation
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<por-slider model="service.ReservationNanoCPUs" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="service && state.sliderMaxCpu" on-change="updateServiceAttribute(service, 'ReservationNanoCPUs')"></por-slider>
|
||||||
|
</td>
|
||||||
|
<td style="vertical-align : middle;">
|
||||||
|
<p class="small text-muted">
|
||||||
|
Minimum CPU available on a node to run a task
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align : middle;">
|
||||||
|
<div>
|
||||||
|
CPU limit
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<por-slider model="service.LimitNanoCPUs" floor="0" ceil="state.sliderMaxCpu" step="0.25" precision="2" ng-if="service && state.sliderMaxCpu" on-change="updateServiceAttribute(service, 'LimitNanoCPUs')"></por-slider>
|
||||||
|
</td>
|
||||||
|
<td style="vertical-align : middle;">
|
||||||
|
<p class="small text-muted">
|
||||||
|
Maximum CPU usage per task
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
<rd-widget-footer>
|
||||||
|
<div class="btn-toolbar" role="toolbar">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['LimitNanoCPUs', 'LimitMemoryBytes', 'ReservationNanoCPUs', 'ReservationMemoryBytes'])" ng-click="updateService(service)">Apply changes</button>
|
||||||
|
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a ng-click="cancelChanges(service, ['LimitNanoCPUs', 'LimitMemoryBytes', 'ReservationNanoCPUs', 'ReservationMemoryBytes'])">Reset changes</a></li>
|
||||||
|
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rd-widget-footer>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -204,14 +204,19 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
||||||
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints);
|
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(service.ServiceConstraints);
|
||||||
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences);
|
config.TaskTemplate.Placement.Preferences = ServiceHelper.translateKeyValueToPlacementPreferences(service.ServicePreferences);
|
||||||
|
|
||||||
|
// Round memory values to 0.125 and convert MB to B
|
||||||
|
var memoryLimit = (Math.round(service.LimitMemoryBytes * 8) / 8).toFixed(3);
|
||||||
|
memoryLimit *= 1024 * 1024;
|
||||||
|
var memoryReservation = (Math.round(service.ReservationMemoryBytes * 8) / 8).toFixed(3);
|
||||||
|
memoryReservation *= 1024 * 1024;
|
||||||
config.TaskTemplate.Resources = {
|
config.TaskTemplate.Resources = {
|
||||||
Limits: {
|
Limits: {
|
||||||
NanoCPUs: service.LimitNanoCPUs,
|
NanoCPUs: service.LimitNanoCPUs * 1000000000,
|
||||||
MemoryBytes: service.LimitMemoryBytes
|
MemoryBytes: memoryLimit
|
||||||
},
|
},
|
||||||
Reservations: {
|
Reservations: {
|
||||||
NanoCPUs: service.ReservationNanoCPUs,
|
NanoCPUs: service.ReservationNanoCPUs * 1000000000,
|
||||||
MemoryBytes: service.ReservationMemoryBytes
|
MemoryBytes: memoryReservation
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -244,7 +249,11 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
||||||
|
|
||||||
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
Service.update({ id: service.Id, version: service.Version }, config, function (data) {
|
||||||
$('#loadingViewSpinner').hide();
|
$('#loadingViewSpinner').hide();
|
||||||
Notifications.success('Service successfully updated', 'Service updated');
|
if (data.message && data.message.match(/^rpc error:/)) {
|
||||||
|
Notifications.error(data.message, 'Error');
|
||||||
|
} else {
|
||||||
|
Notifications.success('Service successfully updated', 'Service updated');
|
||||||
|
}
|
||||||
$scope.cancelChanges({});
|
$scope.cancelChanges({});
|
||||||
initView();
|
initView();
|
||||||
}, function (e) {
|
}, function (e) {
|
||||||
|
@ -288,6 +297,13 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
||||||
service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences);
|
service.ServicePreferences = ServiceHelper.translatePreferencesToKeyValue(service.Preferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transformResources(service) {
|
||||||
|
service.LimitNanoCPUs = service.LimitNanoCPUs / 1000000000 || 0;
|
||||||
|
service.ReservationNanoCPUs = service.ReservationNanoCPUs / 1000000000 || 0;
|
||||||
|
service.LimitMemoryBytes = service.LimitMemoryBytes / 1024 / 1024 || 0;
|
||||||
|
service.ReservationMemoryBytes = service.ReservationMemoryBytes / 1024 / 1024 || 0;
|
||||||
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||||
|
@ -299,6 +315,7 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
||||||
$scope.lastVersion = service.Version;
|
$scope.lastVersion = service.Version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transformResources(service);
|
||||||
translateServiceArrays(service);
|
translateServiceArrays(service);
|
||||||
$scope.service = service;
|
$scope.service = service;
|
||||||
originalService = angular.copy(service);
|
originalService = angular.copy(service);
|
||||||
|
@ -314,6 +331,19 @@ function ($q, $scope, $stateParams, $state, $location, $timeout, $anchorScroll,
|
||||||
$scope.nodes = data.nodes;
|
$scope.nodes = data.nodes;
|
||||||
$scope.secrets = data.secrets;
|
$scope.secrets = data.secrets;
|
||||||
|
|
||||||
|
// Set max cpu value
|
||||||
|
var maxCpus = 0;
|
||||||
|
for (var n in data.nodes) {
|
||||||
|
if (data.nodes[n].CPUs && data.nodes[n].CPUs > maxCpus) {
|
||||||
|
maxCpus = data.nodes[n].CPUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxCpus > 0) {
|
||||||
|
$scope.state.sliderMaxCpu = maxCpus / 1000000000;
|
||||||
|
} else {
|
||||||
|
$scope.state.sliderMaxCpu = 32;
|
||||||
|
}
|
||||||
|
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
$anchorScroll();
|
$anchorScroll();
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,14 +43,14 @@
|
||||||
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
||||||
<a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a>
|
<a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
|
<li class="sidebar-list" ng-if="(applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC') && isAdmin">
|
||||||
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a>
|
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')">
|
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || (applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER')">
|
||||||
<a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
<a ui-sref="swarm" ui-sref-active="active">Swarm <span class="menu-icon fa fa-object-group"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
|
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'">
|
||||||
<a ui-sref="docker" ui-sref-active="active">Docker <span class="menu-icon fa fa-th"></span></a>
|
<a ui-sref="engine" ui-sref-active="active">Engine <span class="menu-icon fa fa-th"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
||||||
<span>Portainer settings</span>
|
<span>Portainer settings</span>
|
||||||
|
|
|
@ -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>Go version</td>
|
||||||
<td>{{ docker.GoVersion }}</td>
|
<td>{{ docker.GoVersion }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr ng-if="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="btn-group" role="group" aria-label="...">
|
||||||
|
<a class="btn btn-outline-secondary" type="button" ui-sref="swarm.visualizer"><i class="fa fa-object-group space-right" aria-hidden="true"></i>Go to cluster visualizer</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
@ -216,7 +223,10 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
<tr dir-paginate="node in (state.filteredNodes = (nodes | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count))">
|
||||||
<td><a ui-sref="node({id: node.Id})">{{ node.Hostname }}</a></td>
|
<td>
|
||||||
|
<a ui-sref="node({id: node.Id})" ng-if="isAdmin">{{ node.Hostname }}</a>
|
||||||
|
<span ng-if="!isAdmin">{{ node.Hostname }}</span>
|
||||||
|
</td>
|
||||||
<td>{{ node.Role }}</td>
|
<td>{{ node.Role }}</td>
|
||||||
<td>{{ node.CPUs / 1000000000 }}</td>
|
<td>{{ node.CPUs / 1000000000 }}</td>
|
||||||
<td>{{ node.Memory|humansize }}</td>
|
<td>{{ node.Memory|humansize }}</td>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('swarm', [])
|
angular.module('swarm', [])
|
||||||
.controller('SwarmController', ['$q', '$scope', 'SystemService', 'NodeService', 'Pagination', 'Notifications',
|
.controller('SwarmController', ['$q', '$scope', 'SystemService', 'NodeService', 'Pagination', 'Notifications', 'StateManager', 'Authentication',
|
||||||
function ($q, $scope, SystemService, NodeService, Pagination, Notifications) {
|
function ($q, $scope, SystemService, NodeService, Pagination, Notifications, StateManager, Authentication) {
|
||||||
$scope.state = {};
|
$scope.state = {};
|
||||||
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
|
$scope.state.pagination_count = Pagination.getPaginationCount('swarm_nodes');
|
||||||
$scope.sortType = 'Spec.Role';
|
$scope.sortType = 'Spec.Role';
|
||||||
|
@ -73,6 +73,13 @@ function ($q, $scope, SystemService, NodeService, Pagination, Notifications) {
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$('#loadingViewSpinner').show();
|
$('#loadingViewSpinner').show();
|
||||||
|
|
||||||
|
if (StateManager.getState().application.authentication) {
|
||||||
|
var userDetails = Authentication.getUserDetails();
|
||||||
|
var isAdmin = userDetails.role === 1 ? true: false;
|
||||||
|
$scope.isAdmin = isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||||
$q.all({
|
$q.all({
|
||||||
version: SystemService.version(),
|
version: SystemService.version(),
|
||||||
|
|
|
@ -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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<span class="template-note" ng-bind-html="state.selectedTemplate.Note"></span>
|
<div class="template-note" ng-if="state.selectedTemplate.Note" ng-bind-html="state.selectedTemplate.Note"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -151,7 +151,7 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerSer
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
templates: TemplateService.getTemplates(templatesKey),
|
templates: TemplateService.getTemplates(templatesKey),
|
||||||
containers: ContainerService.getContainers(0),
|
containers: ContainerService.containers(0),
|
||||||
volumes: VolumeService.getVolumes(),
|
volumes: VolumeService.getVolumes(),
|
||||||
networks: NetworkService.networks(
|
networks: NetworkService.networks(
|
||||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||||
|
|
|
@ -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 () {
|
.filter('taskstatusbadge', function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
return function (text) {
|
return function (text) {
|
||||||
|
|
|
@ -119,6 +119,5 @@ angular.module('portainer.helpers').factory('ServiceHelper', [function ServiceHe
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -16,11 +16,20 @@ function TemplateViewModel(data) {
|
||||||
this.Volumes = [];
|
this.Volumes = [];
|
||||||
if (data.volumes) {
|
if (data.volumes) {
|
||||||
this.Volumes = data.volumes.map(function (v) {
|
this.Volumes = data.volumes.map(function (v) {
|
||||||
return {
|
// @DEPRECATED: New volume definition introduced
|
||||||
readOnly: false,
|
// via https://github.com/portainer/portainer/pull/1154
|
||||||
containerPath: v,
|
var volume = {
|
||||||
|
readOnly: v.readonly || false,
|
||||||
|
containerPath: v.container || v,
|
||||||
type: 'auto'
|
type: 'auto'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (v.bind) {
|
||||||
|
volume.name = v.bind;
|
||||||
|
volume.type = 'bind';
|
||||||
|
}
|
||||||
|
|
||||||
|
return volume;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.Ports = [];
|
this.Ports = [];
|
||||||
|
|
|
@ -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.Tag = data.Tag;
|
||||||
this.Repository = data.Repository;
|
this.Repository = data.Repository;
|
||||||
this.Created = data.Created;
|
this.Created = data.Created;
|
||||||
this.Containers = data.dataUsage ? data.dataUsage.Containers : 0;
|
|
||||||
this.Checked = false;
|
this.Checked = false;
|
||||||
this.RepoTags = data.RepoTags;
|
this.RepoTags = data.RepoTags;
|
||||||
this.VirtualSize = data.VirtualSize;
|
this.VirtualSize = data.VirtualSize;
|
||||||
|
this.ContainerCount = data.ContainerCount;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Version = data.Version.Index;
|
||||||
this.Name = data.Spec.Name;
|
this.Name = data.Spec.Name;
|
||||||
this.Labels = data.Spec.Labels;
|
this.Labels = data.Spec.Labels;
|
||||||
|
|
||||||
|
if (data.Portainer) {
|
||||||
|
if (data.Portainer.ResourceControl) {
|
||||||
|
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,14 @@ angular.module('portainer.rest')
|
||||||
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
kill: {method: 'POST', params: {id: '@id', action: 'kill'}},
|
||||||
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
pause: {method: 'POST', params: {id: '@id', action: 'pause'}},
|
||||||
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
unpause: {method: 'POST', params: {id: '@id', action: 'unpause'}},
|
||||||
stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 5000},
|
stats: {
|
||||||
|
method: 'GET', params: { id: '@id', stream: false, action: 'stats' },
|
||||||
|
timeout: 4500
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
method: 'GET', params: { id: '@id', action: 'top' },
|
||||||
|
timeout: 4500
|
||||||
|
},
|
||||||
start: {
|
start: {
|
||||||
method: 'POST', params: {id: '@id', action: 'start'},
|
method: 'POST', params: {id: '@id', action: 'start'},
|
||||||
transformResponse: genericHandler
|
transformResponse: genericHandler
|
||||||
|
|
|
@ -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,
|
name: endpointParams.name,
|
||||||
PublicURL: endpointParams.PublicURL,
|
PublicURL: endpointParams.PublicURL,
|
||||||
TLS: endpointParams.TLS,
|
TLS: endpointParams.TLS,
|
||||||
|
TLSSkipVerify: endpointParams.TLSSkipVerify,
|
||||||
|
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
|
||||||
authorizedUsers: endpointParams.authorizedUsers
|
authorizedUsers: endpointParams.authorizedUsers
|
||||||
};
|
};
|
||||||
if (endpointParams.type && endpointParams.URL) {
|
if (endpointParams.type && endpointParams.URL) {
|
||||||
|
@ -55,18 +57,20 @@ angular.module('portainer.services')
|
||||||
return Endpoints.create({}, endpoint).$promise;
|
return Endpoints.create({}, endpoint).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
var endpoint = {
|
var endpoint = {
|
||||||
Name: name,
|
Name: name,
|
||||||
URL: 'tcp://' + URL,
|
URL: 'tcp://' + URL,
|
||||||
PublicURL: PublicURL,
|
PublicURL: PublicURL,
|
||||||
TLS: TLS
|
TLS: TLS,
|
||||||
|
TLSSkipVerify: TLSSkipVerify,
|
||||||
|
TLSSkipClientVerify: TLSSkipClientVerify
|
||||||
};
|
};
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
Endpoints.create({}, endpoint).$promise
|
Endpoints.create({}, endpoint).$promise
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpointID = data.Id;
|
var endpointID = data.Id;
|
||||||
if (TLS) {
|
if (!TLSSkipVerify || !TLSSkipClientVerify) {
|
||||||
deferred.notify({upload: true});
|
deferred.notify({upload: true});
|
||||||
FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile)
|
FileUploadService.uploadTLSFilesForEndpoint(endpointID, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
|
|
|
@ -134,5 +134,26 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.initAdministrator = function(username, password) {
|
||||||
|
return Users.initAdminUser({ Username: username, Password: password }).$promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.administratorExists = function() {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Users.checkAdminUser({}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
deferred.resolve(true);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
if (err.status === 404) {
|
||||||
|
deferred.resolve(false);
|
||||||
|
}
|
||||||
|
deferred.reject({ msg: 'Unable to verify administrator account existence', err: err });
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.getContainers = function (all) {
|
service.container = function(id) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Container.get({ id: id }).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var container = new ContainerDetailsViewModel(data);
|
||||||
|
deferred.resolve(container);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject({ msg: 'Unable to retrieve container information', err: err });
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.containers = function(all) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
Container.query({ all: all }).$promise
|
Container.query({ all: all }).$promise
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
|
@ -11,7 +26,7 @@ angular.module('portainer.services')
|
||||||
deferred.resolve(containers);
|
deferred.resolve(containers);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to retriever containers', err: err });
|
deferred.reject({ msg: 'Unable to retrieve containers', err: err });
|
||||||
});
|
});
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
@ -105,5 +120,35 @@ angular.module('portainer.services')
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.containerStats = function(id) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Container.stats({id: id}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var containerStats = new ContainerStatsViewModel(data);
|
||||||
|
deferred.resolve(containerStats);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.containerTop = function(id) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Container.top({id: id}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var containerTop = data;
|
||||||
|
deferred.resolve(containerTop);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
return service;
|
return service;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
angular.module('portainer.services')
|
angular.module('portainer.services')
|
||||||
.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', 'SystemService', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, SystemService) {
|
.factory('ImageService', ['$q', 'Image', 'ImageHelper', 'RegistryService', 'HttpRequestHelper', 'ContainerService', function ImageServiceFactory($q, Image, ImageHelper, RegistryService, HttpRequestHelper, ContainerService) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
@ -24,17 +24,23 @@ angular.module('portainer.services')
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
dataUsage: withUsage ? SystemService.dataUsage() : { Images: [] },
|
containers: withUsage ? ContainerService.containers(1) : [],
|
||||||
images: Image.query({}).$promise
|
images: Image.query({}).$promise
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var images = data.images.map(function(item) {
|
var containers = data.containers;
|
||||||
item.dataUsage = data.dataUsage.Images.find(function(usage) {
|
|
||||||
return item.Id === usage.Id;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
var images = data.images.map(function(item) {
|
||||||
|
item.ContainerCount = 0;
|
||||||
|
for (var i = 0; i < containers.length; i++) {
|
||||||
|
var container = containers[i];
|
||||||
|
if (container.ImageID === item.Id) {
|
||||||
|
item.ContainerCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
return new ImageViewModel(item);
|
return new ImageViewModel(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
deferred.resolve(images);
|
deferred.resolve(images);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -3,6 +3,35 @@ angular.module('portainer.services')
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
service.create = function(networkConfiguration) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Network.create(networkConfiguration).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
deferred.resolve(data);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject({ msg: 'Unable to create network', err: err });
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
service.network = function(id) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
Network.get({id: id}).$promise
|
||||||
|
.then(function success(data) {
|
||||||
|
var network = new NetworkViewModel(data);
|
||||||
|
deferred.resolve(network);
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
deferred.reject({msg: 'Unable to retrieve network details', err: err});
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
service.networks = function(localNetworks, swarmNetworks, swarmAttachableNetworks, globalNetworks) {
|
service.networks = function(localNetworks, swarmNetworks, swarmAttachableNetworks, globalNetworks) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
@ -23,6 +52,8 @@ angular.module('portainer.services')
|
||||||
if (globalNetworks && network.Scope === 'global') {
|
if (globalNetworks && network.Scope === 'global') {
|
||||||
return network;
|
return network;
|
||||||
}
|
}
|
||||||
|
}).map(function (item) {
|
||||||
|
return new NetworkViewModel(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
deferred.resolve(filteredNetworks);
|
deferred.resolve(filteredNetworks);
|
||||||
|
|
|
@ -3,7 +3,7 @@ angular.module('portainer.services')
|
||||||
'use strict';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
service.nodes = function(id) {
|
service.nodes = function() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
Node.query({}).$promise
|
Node.query({}).$promise
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue