Merge branch 'release/1.14.1'

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,9 @@
<rd-widget>
<rd-widget-body>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
Configuration
</div>
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
@ -42,73 +45,19 @@
</div>
</div>
<!-- !endpoint-public-url-input -->
<!-- tls-checkbox -->
<div class="form-group" ng-if="endpointType === 'remote'">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="endpoint.TLS"><i></i>
</label>
<!-- endpoint-security -->
<div ng-if="endpointType === 'remote'">
<div class="col-sm-12 form-section-title">
Security
</div>
<por-endpoint-security form-data="formValues.SecurityFormData" endpoint="endpoint"></por-endpoint-security>
</div>
<!-- !tls-checkbox -->
<!-- tls-certs -->
<div ng-if="endpoint.TLS">
<!-- ca-input -->
<div class="form-group">
<label class="col-sm-2 control-label text-left">TLS CA certificate</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
<span style="margin-left: 5px;">
<span ng-if="formValues.TLSCACert !== endpoint.TLSCACert">{{ formValues.TLSCACert.name }}</span>
<i class="fa fa-check green-icon" ng-if="formValues.TLSCACert && formValues.TLSCACert === endpoint.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !ca-input -->
<!-- cert-input -->
<div class="form-group">
<label for="tls_cert" class="col-sm-2 control-label text-left">TLS certificate</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
<span style="margin-left: 5px;">
<span ng-if="formValues.TLSCert !== endpoint.TLSCert">{{ formValues.TLSCert.name }}</span>
<i class="fa fa-check green-icon" ng-if="formValues.TLSCert && formValues.TLSCert === endpoint.TLSCert" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !cert-input -->
<!-- key-input -->
<div class="form-group">
<label class="col-sm-2 control-label text-left">TLS key</label>
<div class="col-sm-10">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
<span style="margin-left: 5px;">
<span ng-if="formValues.TLSKey !== endpoint.TLSKey">{{ formValues.TLSKey.name }}</span>
<i class="fa fa-check green-icon" ng-if="formValues.TLSKey && formValues.TLSKey === endpoint.TLSKey" aria-hidden="true"></i>
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !key-input -->
</div>
<!-- !tls-certs -->
<!-- !endpoint-security -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="updateEndpoint()">Update endpoint</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!endpoint.Name || !endpoint.URL || (endpoint.TLS && ((endpoint.TLSVerify && !formValues.TLSCACert) || (endpoint.TLSClientCert && (!formValues.TLSCert || !formValues.TLSKey))))" ng-click="updateEndpoint()">Update endpoint</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="endpoints">Cancel</a>
<i id="updateEndpointSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</span>
<i id="updateResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
</div>
</div>
</form>

View File

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

View File

@ -1,153 +0,0 @@
<div class="page-wrapper">
<!-- simple box -->
<div class="container simple-box">
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
<!-- simple box logo -->
<div class="row">
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
</div>
<!-- !simple box logo -->
<!-- init-endpoint panel -->
<div class="panel panel-default">
<div class="panel-body">
<!-- init-endpoint form -->
<form class="form-horizontal" style="margin: 20px;" enctype="multipart/form-data" method="POST">
<!-- comment -->
<div class="form-group" style="text-align: center;">
<h4>Connect Portainer to a Docker engine or Swarm cluster endpoint</h4>
</div>
<!-- !comment input -->
<!-- endpoin-type radio -->
<div class="form-group">
<div class="radio">
<label><input type="radio" name="endpointType" value="local" ng-model="formValues.endpointType" ng-click="resetErrorMessage()">Manage the Docker instance where Portainer is running</label>
</div>
<div class="radio">
<label><input type="radio" name="endpointType" value="remote" ng-model="formValues.endpointType" ng-click="resetErrorMessage()">Manage a remote Docker instance</label>
</div>
</div>
<!-- endpoint-type radio -->
<!-- local-endpoint -->
<div ng-if="formValues.endpointType === 'local'" style="margin-top: 25px;">
<div class="form-group">
<i class="fa fa-exclamation-triangle" aria-hidden="true" style="margin-right: 5px;"></i>
<span class="small text-primary">This feature is not yet available for native Docker Windows containers.</span>
<div class="small text-primary">On Linux and when using Docker for Mac or Docker for Windows or Docker Toolbox, ensure that you have started Portainer container with the following Docker flag <code>-v "/var/run/docker.sock:/var/run/docker.sock"</code></div>
</div>
<!-- connect button -->
<div class="form-group" style="margin-top: 10px;">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</p>
<span class="pull-right">
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
<button type="submit" class="btn btn-primary" ng-click="createLocalEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
</span>
</div>
</div>
<!-- !connect button -->
</div>
<!-- !local-endpoint -->
<!-- remote-endpoint -->
<div ng-if="formValues.endpointType === 'remote'" style="margin-top: 25px;">
<!-- name-input -->
<div class="form-group">
<label for="container_name" class="col-sm-4 col-lg-3 control-label text-left">Name</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="container_name" ng-model="formValues.Name" placeholder="e.g. docker-prod01">
</div>
</div>
<!-- !name-input -->
<!-- endpoint-url-input -->
<div class="form-group">
<label for="endpoint_url" class="col-sm-4 col-lg-3 control-label text-left">
Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
</label>
<div class="col-sm-8 col-lg-9">
<input type="text" class="form-control" id="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:2375 or mydocker.mydomain.com:2375">
</div>
</div>
<!-- !endpoint-url-input -->
<!-- tls-checkbox -->
<div class="form-group">
<div class="col-sm-12">
<label for="tls" class="control-label text-left">
TLS
<portainer-tooltip position="bottom" message="Enable this option if you need to specify TLS certificates to connect to the Docker endpoint."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="formValues.TLS"><i></i>
</label>
</div>
</div>
<!-- !tls-checkbox -->
<!-- tls-certs -->
<div ng-if="formValues.TLS">
<!-- ca-input -->
<div class="form-group">
<label class="col-sm-3 control-label text-left">TLS CA certificate</label>
<div class="col-sm-9">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCACert">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSCACert.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCACert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !ca-input -->
<!-- cert-input -->
<div class="form-group">
<label for="tls_cert" class="col-sm-3 control-label text-left">TLS certificate</label>
<div class="col-sm-9">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSCert">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSCert.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSCert" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !cert-input -->
<!-- key-input -->
<div class="form-group">
<label class="col-sm-3 control-label text-left">TLS key</label>
<div class="col-sm-9">
<button class="btn btn-sm btn-primary" ngf-select ng-model="formValues.TLSKey">Select file</button>
<span style="margin-left: 5px;">
{{ formValues.TLSKey.name }}
<i class="fa fa-times red-icon" ng-if="!formValues.TLSKey" aria-hidden="true"></i>
<i class="fa fa-circle-o-notch fa-spin" ng-if="state.uploadInProgress"></i>
</span>
</div>
</div>
<!-- !key-input -->
</div>
<!-- !tls-certs -->
<!-- connect button -->
<div class="form-group" style="margin-top: 10px;">
<div class="col-sm-12 controls">
<p class="pull-left text-danger" ng-if="state.error" style="margin: 5px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ state.error }}
</p>
<span class="pull-right">
<i id="initEndpointSpinner" class="fa fa-cog fa-spin" style="margin-right: 5px; display: none;"></i>
<button type="submit" class="btn btn-primary" ng-disabled="!formValues.Name || !formValues.URL || (formValues.TLS && (!formValues.TLSCACert || !formValues.TLSCert || !formValues.TLSKey))" ng-click="createRemoteEndpoint()"><i class="fa fa-plug" aria-hidden="true"></i> Connect</button>
</span>
</div>
</div>
<!-- !connect button -->
</div>
<!-- !remote-endpoint -->
</form>
<!-- !init-endpoint form -->
</div>
</div>
<!-- !init-endpoint panel -->
</div>
</div>
<!-- !simple box -->
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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