diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go
index 96b179b46..3c56ede26 100644
--- a/api/bolt/datastore.go
+++ b/api/bolt/datastore.go
@@ -22,6 +22,7 @@ type Store struct {
EndpointService *EndpointService
ResourceControlService *ResourceControlService
VersionService *VersionService
+ SettingsService *SettingsService
db *bolt.DB
checkForDataMigration bool
@@ -35,6 +36,7 @@ const (
teamMembershipBucketName = "team_membership"
endpointBucketName = "endpoints"
resourceControlBucketName = "resource_control"
+ settingsBucketName = "settings"
)
// NewStore initializes a new Store and the associated services
@@ -47,6 +49,7 @@ func NewStore(storePath string) (*Store, error) {
EndpointService: &EndpointService{},
ResourceControlService: &ResourceControlService{},
VersionService: &VersionService{},
+ SettingsService: &SettingsService{},
}
store.UserService.store = store
store.TeamService.store = store
@@ -54,6 +57,7 @@ func NewStore(storePath string) (*Store, error) {
store.EndpointService.store = store
store.ResourceControlService.store = store
store.VersionService.store = store
+ store.SettingsService.store = store
_, err := os.Stat(storePath + "/" + databaseFileName)
if err != nil && os.IsNotExist(err) {
@@ -100,6 +104,10 @@ func (store *Store) Open() error {
if err != nil {
return err
}
+ _, err = tx.CreateBucketIfNotExists([]byte(settingsBucketName))
+ if err != nil {
+ return err
+ }
return nil
})
}
diff --git a/api/bolt/internal/internal.go b/api/bolt/internal/internal.go
index e02c26ba4..f55f72118 100644
--- a/api/bolt/internal/internal.go
+++ b/api/bolt/internal/internal.go
@@ -57,6 +57,16 @@ func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error
return json.Unmarshal(data, rc)
}
+// MarshalSettings encodes a settings object to binary format.
+func MarshalSettings(settings *portainer.Settings) ([]byte, error) {
+ return json.Marshal(settings)
+}
+
+// UnmarshalSettings decodes a settings object from a binary data.
+func UnmarshalSettings(data []byte, settings *portainer.Settings) error {
+ return json.Unmarshal(data, settings)
+}
+
// Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys.
diff --git a/api/bolt/settings_service.go b/api/bolt/settings_service.go
new file mode 100644
index 000000000..9ea7cc2ab
--- /dev/null
+++ b/api/bolt/settings_service.go
@@ -0,0 +1,61 @@
+package bolt
+
+import (
+ "github.com/portainer/portainer"
+ "github.com/portainer/portainer/bolt/internal"
+
+ "github.com/boltdb/bolt"
+)
+
+// SettingsService represents a service to manage application settings.
+type SettingsService struct {
+ store *Store
+}
+
+const (
+ dbSettingsKey = "SETTINGS"
+)
+
+// Settings retrieve the settings object.
+func (service *SettingsService) Settings() (*portainer.Settings, error) {
+ var data []byte
+ err := service.store.db.View(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(settingsBucketName))
+ value := bucket.Get([]byte(dbSettingsKey))
+ if value == nil {
+ return portainer.ErrSettingsNotFound
+ }
+
+ data = make([]byte, len(value))
+ copy(data, value)
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ var settings portainer.Settings
+ err = internal.UnmarshalSettings(data, &settings)
+ if err != nil {
+ return nil, err
+ }
+ return &settings, nil
+}
+
+// StoreSettings persists a Settings object.
+func (service *SettingsService) StoreSettings(settings *portainer.Settings) error {
+ return service.store.db.Update(func(tx *bolt.Tx) error {
+ bucket := tx.Bucket([]byte(settingsBucketName))
+
+ data, err := internal.MarshalSettings(settings)
+ if err != nil {
+ return err
+ }
+
+ err = bucket.Put([]byte(dbSettingsKey), data)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+}
diff --git a/api/cli/cli.go b/api/cli/cli.go
index 68a017b1d..80308d41a 100644
--- a/api/cli/cli.go
+++ b/api/cli/cli.go
@@ -1,6 +1,7 @@
package cli
import (
+ "log"
"time"
"github.com/portainer/portainer"
@@ -29,14 +30,11 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
flags := &portainer.CLIFlags{
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
- Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
- Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
- Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
@@ -47,6 +45,10 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
+ // Deprecated flags
+ Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
+ Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
+ Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
}
kingpin.Parse()
@@ -79,6 +81,8 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return errNoAuthExcludeAdminPassword
}
+ displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels)
+
return nil
}
@@ -122,3 +126,15 @@ func validateSyncInterval(syncInterval string) error {
}
return nil
}
+
+func displayDeprecationWarnings(templates, logo string, labels []portainer.Pair) {
+ if templates != "" {
+ log.Println("Warning: the --templates / -t flag is deprecated and will be removed in future versions.")
+ }
+ if logo != "" {
+ log.Println("Warning: the --logo flag is deprecated and will be removed in future versions.")
+ }
+ if labels != nil {
+ log.Println("Warning: the --hide-label / -l flag is deprecated and will be removed in future versions.")
+ }
+}
diff --git a/api/cli/defaults.go b/api/cli/defaults.go
index 5de413911..80ba58c51 100644
--- a/api/cli/defaults.go
+++ b/api/cli/defaults.go
@@ -6,7 +6,6 @@ const (
defaultBindAddress = ":9000"
defaultDataDirectory = "/data"
defaultAssetsDirectory = "."
- defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go
index da5f0ce45..5e80489e4 100644
--- a/api/cli/defaults_windows.go
+++ b/api/cli/defaults_windows.go
@@ -4,7 +4,6 @@ const (
defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "."
- defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false"
defaultNoAnalytics = "false"
defaultTLSVerify = "false"
diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go
index 68c6183fb..9876de6aa 100644
--- a/api/cmd/portainer/main.go
+++ b/api/cmd/portainer/main.go
@@ -82,16 +82,43 @@ func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpo
return authorizeEndpointMgmt
}
-func initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Settings {
- return &portainer.Settings{
- HiddenLabels: *flags.Labels,
- Logo: *flags.Logo,
+func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
+ return &portainer.Status{
Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
+ Version: portainer.APIVersion,
}
}
+func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
+ _, err := settingsService.Settings()
+ if err == portainer.ErrSettingsNotFound {
+ settings := &portainer.Settings{
+ LogoURL: *flags.Logo,
+ DisplayExternalContributors: true,
+ }
+
+ if *flags.Templates != "" {
+ settings.TemplatesURL = *flags.Templates
+ } else {
+ settings.TemplatesURL = portainer.DefaultTemplatesURL
+ }
+
+ if *flags.Labels != nil {
+ settings.BlackListedLabels = *flags.Labels
+ } else {
+ settings.BlackListedLabels = make([]portainer.Pair, 0)
+ }
+
+ return settingsService.StoreSettings(settings)
+ } else if err != nil {
+ return err
+ }
+
+ return nil
+}
+
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
endpoints, err := endpointService.Endpoints()
if err != nil {
@@ -114,7 +141,12 @@ func main() {
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
- settings := initSettings(authorizeEndpointMgmt, flags)
+ err := initSettings(store.SettingsService, flags)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ applicationStatus := initStatus(authorizeEndpointMgmt, flags)
if *flags.Endpoint != "" {
var endpoints []portainer.Endpoint
@@ -156,10 +188,9 @@ func main() {
}
var server portainer.Server = &http.Server{
+ Status: applicationStatus,
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
- Settings: settings,
- TemplatesURL: *flags.Templates,
AuthDisabled: *flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt,
UserService: store.UserService,
@@ -167,6 +198,7 @@ func main() {
TeamMembershipService: store.TeamMembershipService,
EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService,
+ SettingsService: store.SettingsService,
CryptoService: cryptoService,
JWTService: jwtService,
FileService: fileService,
@@ -176,7 +208,7 @@ func main() {
}
log.Printf("Starting Portainer on %s", *flags.Addr)
- err := server.Start()
+ err = server.Start()
if err != nil {
log.Fatal(err)
}
diff --git a/api/errors.go b/api/errors.go
index e22dbc01d..0be2338d8 100644
--- a/api/errors.go
+++ b/api/errors.go
@@ -47,6 +47,11 @@ const (
ErrDBVersionNotFound = Error("DB version not found")
)
+// Settings errors.
+const (
+ ErrSettingsNotFound = Error("Settings not found")
+)
+
// Crypto errors.
const (
ErrCryptoHashFailure = Error("Unable to hash data")
diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go
index 89a963c33..0692dc6de 100644
--- a/api/http/handler/handler.go
+++ b/api/http/handler/handler.go
@@ -18,6 +18,7 @@ type Handler struct {
TeamMembershipHandler *TeamMembershipHandler
EndpointHandler *EndpointHandler
ResourceHandler *ResourceHandler
+ StatusHandler *StatusHandler
SettingsHandler *SettingsHandler
TemplatesHandler *TemplatesHandler
DockerHandler *DockerHandler
@@ -53,6 +54,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
+ } else if strings.HasPrefix(r.URL.Path, "/api/status") {
+ http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/upload") {
diff --git a/api/http/handler/settings.go b/api/http/handler/settings.go
index cfae1dc30..26e1cfe92 100644
--- a/api/http/handler/settings.go
+++ b/api/http/handler/settings.go
@@ -1,6 +1,9 @@
package handler
import (
+ "encoding/json"
+
+ "github.com/asaskevich/govalidator"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
@@ -12,32 +15,69 @@ import (
"github.com/gorilla/mux"
)
-// SettingsHandler represents an HTTP API handler for managing settings.
+// SettingsHandler represents an HTTP API handler for managing Settings.
type SettingsHandler struct {
*mux.Router
- Logger *log.Logger
- settings *portainer.Settings
+ Logger *log.Logger
+ SettingsService portainer.SettingsService
}
-// NewSettingsHandler returns a new instance of SettingsHandler.
-func NewSettingsHandler(bouncer *security.RequestBouncer, settings *portainer.Settings) *SettingsHandler {
+// NewSettingsHandler returns a new instance of OldSettingsHandler.
+func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
h := &SettingsHandler{
- Router: mux.NewRouter(),
- Logger: log.New(os.Stderr, "", log.LstdFlags),
- settings: settings,
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/settings",
- bouncer.PublicAccess(http.HandlerFunc(h.handleGetSettings)))
+ bouncer.PublicAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet)
+ h.Handle("/settings",
+ bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettings))).Methods(http.MethodPut)
return h
}
// handleGetSettings handles GET requests on /settings
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet})
+ settings, err := handler.SettingsService.Settings()
+ if err != nil {
+ httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return
}
- encodeJSON(w, handler.settings, handler.Logger)
+ encodeJSON(w, settings, handler.Logger)
+ return
+}
+
+// handlePutSettings handles PUT requests on /settings
+func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) {
+ var req putSettingsRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ _, err := govalidator.ValidateStruct(req)
+ if err != nil {
+ httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
+ return
+ }
+
+ settings := &portainer.Settings{
+ TemplatesURL: req.TemplatesURL,
+ LogoURL: req.LogoURL,
+ BlackListedLabels: req.BlackListedLabels,
+ DisplayExternalContributors: req.DisplayExternalContributors,
+ }
+
+ err = handler.SettingsService.StoreSettings(settings)
+ if err != nil {
+ httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
+ }
+}
+
+type putSettingsRequest struct {
+ TemplatesURL string `valid:"required"`
+ LogoURL string `valid:""`
+ BlackListedLabels []portainer.Pair `valid:""`
+ DisplayExternalContributors bool `valid:""`
}
diff --git a/api/http/handler/status.go b/api/http/handler/status.go
new file mode 100644
index 000000000..6bae3c8a7
--- /dev/null
+++ b/api/http/handler/status.go
@@ -0,0 +1,38 @@
+package handler
+
+import (
+ "github.com/portainer/portainer"
+ "github.com/portainer/portainer/http/security"
+
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/gorilla/mux"
+)
+
+// StatusHandler represents an HTTP API handler for managing Status.
+type StatusHandler struct {
+ *mux.Router
+ Logger *log.Logger
+ Status *portainer.Status
+}
+
+// NewStatusHandler returns a new instance of StatusHandler.
+func NewStatusHandler(bouncer *security.RequestBouncer, status *portainer.Status) *StatusHandler {
+ h := &StatusHandler{
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
+ Status: status,
+ }
+ h.Handle("/status",
+ bouncer.PublicAccess(http.HandlerFunc(h.handleGetStatus))).Methods(http.MethodGet)
+
+ return h
+}
+
+// handleGetStatus handles GET requests on /status
+func (handler *StatusHandler) handleGetStatus(w http.ResponseWriter, r *http.Request) {
+ encodeJSON(w, handler.Status, handler.Logger)
+ return
+}
diff --git a/api/http/handler/templates.go b/api/http/handler/templates.go
index 9383e407e..516fc892e 100644
--- a/api/http/handler/templates.go
+++ b/api/http/handler/templates.go
@@ -7,6 +7,7 @@ import (
"os"
"github.com/gorilla/mux"
+ "github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security"
)
@@ -14,8 +15,8 @@ import (
// TemplatesHandler represents an HTTP API handler for managing templates.
type TemplatesHandler struct {
*mux.Router
- Logger *log.Logger
- containerTemplatesURL string
+ Logger *log.Logger
+ SettingsService portainer.SettingsService
}
const (
@@ -23,11 +24,10 @@ const (
)
// NewTemplatesHandler returns a new instance of TemplatesHandler.
-func NewTemplatesHandler(bouncer *security.RequestBouncer, containerTemplatesURL string) *TemplatesHandler {
+func NewTemplatesHandler(bouncer *security.RequestBouncer) *TemplatesHandler {
h := &TemplatesHandler{
- Router: mux.NewRouter(),
- Logger: log.New(os.Stderr, "", log.LstdFlags),
- containerTemplatesURL: containerTemplatesURL,
+ Router: mux.NewRouter(),
+ Logger: log.New(os.Stderr, "", log.LstdFlags),
}
h.Handle("/templates",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates)))
@@ -49,7 +49,12 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht
var templatesURL string
if key == "containers" {
- templatesURL = handler.containerTemplatesURL
+ settings, err := handler.SettingsService.Settings()
+ if err != nil {
+ httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
+ return
+ }
+ templatesURL = settings.TemplatesURL
} else if key == "linuxserver.io" {
templatesURL = containerTemplatesURLLinuxServerIo
} else {
diff --git a/api/http/proxy/containers.go b/api/http/proxy/containers.go
index 909a7dc0d..964dab1f6 100644
--- a/api/http/proxy/containers.go
+++ b/api/http/proxy/containers.go
@@ -15,7 +15,7 @@ const (
// containerListOperation extracts the response as a JSON object, loop through the containers array
// decorate and/or filter the containers based on resource controls before rewriting the response
-func containerListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
+func containerListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
var err error
// ContainerList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
@@ -24,22 +24,30 @@ func containerListOperation(request *http.Request, response *http.Response, oper
return err
}
- if operationContext.isAdmin {
- responseArray, err = decorateContainerList(responseArray, operationContext.resourceControls)
+ if executor.operationContext.isAdmin {
+ responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
} else {
- responseArray, err = filterContainerList(responseArray, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
+ responseArray, err = filterContainerList(responseArray, executor.operationContext.resourceControls,
+ executor.operationContext.userID, executor.operationContext.userTeamIDs)
}
if err != nil {
return err
}
+ if executor.labelBlackList != nil {
+ responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList)
+ if err != nil {
+ return err
+ }
+ }
+
return rewriteResponse(response, responseArray, http.StatusOK)
}
// containerInspectOperation extracts the response as a JSON object, verify that the user
// has access to the container based on resource control (check are done based on the containerID and optional Swarm service ID)
// and either rewrite an access denied response or a decorated container.
-func containerInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
+func containerInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
// ContainerInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
responseObject, err := getResponseAsJSONOBject(response)
@@ -52,9 +60,10 @@ func containerInspectOperation(request *http.Request, response *http.Response, o
}
containerID := responseObject[containerIdentifier].(string)
- resourceControl := getResourceControlByResourceID(containerID, operationContext.resourceControls)
+ resourceControl := getResourceControlByResourceID(containerID, executor.operationContext.resourceControls)
if resourceControl != nil {
- if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
+ if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
+ executor.operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
@@ -64,9 +73,10 @@ func containerInspectOperation(request *http.Request, response *http.Response, o
containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject)
if containerLabels != nil && containerLabels[containerLabelForServiceIdentifier] != nil {
serviceID := containerLabels[containerLabelForServiceIdentifier].(string)
- resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls)
+ resourceControl := getResourceControlByResourceID(serviceID, executor.operationContext.resourceControls)
if resourceControl != nil {
- if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
+ if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
+ executor.operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go
index 96d2239d2..3e0d71445 100644
--- a/api/http/proxy/factory.go
+++ b/api/http/proxy/factory.go
@@ -13,6 +13,7 @@ import (
type proxyFactory struct {
ResourceControlService portainer.ResourceControlService
TeamMembershipService portainer.TeamMembershipService
+ SettingsService portainer.SettingsService
}
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
@@ -37,6 +38,7 @@ func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
TeamMembershipService: factory.TeamMembershipService,
+ SettingsService: factory.SettingsService,
dockerTransport: newSocketTransport(path),
}
proxy.Transport = transport
@@ -48,6 +50,7 @@ func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReversePro
transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService,
TeamMembershipService: factory.TeamMembershipService,
+ SettingsService: factory.SettingsService,
dockerTransport: newHTTPTransport(),
}
proxy.Transport = transport
diff --git a/api/http/proxy/filter.go b/api/http/proxy/filter.go
index 02de684b3..0e66ab4fd 100644
--- a/api/http/proxy/filter.go
+++ b/api/http/proxy/filter.go
@@ -65,6 +65,27 @@ func filterContainerList(containerData []interface{}, resourceControls []portain
return filteredContainerData, nil
}
+// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains
+// any labels in the labels black list.
+func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) {
+ filteredContainerData := make([]interface{}, 0)
+
+ for _, container := range containerData {
+ containerObject := container.(map[string]interface{})
+
+ containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
+ if containerLabels != nil {
+ if !containerHasBlackListedLabel(containerLabels, labelBlackList) {
+ filteredContainerData = append(filteredContainerData, containerObject)
+ }
+ } else {
+ filteredContainerData = append(filteredContainerData, containerObject)
+ }
+ }
+
+ return filteredContainerData, nil
+}
+
// filterServiceList loops through all services, filters services without any resource control (public resources) or with
// any resource control giving access to the user (these services will be decorated).
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go
index b90596e10..8710c7a44 100644
--- a/api/http/proxy/manager.go
+++ b/api/http/proxy/manager.go
@@ -15,12 +15,13 @@ type Manager struct {
}
// NewManager initializes a new proxy Service
-func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService) *Manager {
+func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager {
return &Manager{
proxies: cmap.New(),
proxyFactory: &proxyFactory{
ResourceControlService: resourceControlService,
TeamMembershipService: teamMembershipService,
+ SettingsService: settingsService,
},
}
}
diff --git a/api/http/proxy/service.go b/api/http/proxy/service.go
index fcf604a84..317da9ebc 100644
--- a/api/http/proxy/service.go
+++ b/api/http/proxy/service.go
@@ -14,7 +14,7 @@ const (
// serviceListOperation extracts the response as a JSON array, loop through the service array
// decorate and/or filter the services based on resource controls before rewriting the response
-func serviceListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
+func serviceListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
var err error
// ServiceList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
@@ -23,10 +23,10 @@ func serviceListOperation(request *http.Request, response *http.Response, operat
return err
}
- if operationContext.isAdmin {
- responseArray, err = decorateServiceList(responseArray, operationContext.resourceControls)
+ if executor.operationContext.isAdmin {
+ responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
} else {
- responseArray, err = filterServiceList(responseArray, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
+ responseArray, err = filterServiceList(responseArray, executor.operationContext.resourceControls, executor.operationContext.userID, executor.operationContext.userTeamIDs)
}
if err != nil {
return err
@@ -38,7 +38,7 @@ func serviceListOperation(request *http.Request, response *http.Response, operat
// serviceInspectOperation extracts the response as a JSON object, verify that the user
// has access to the service based on resource control and either rewrite an access denied response
// or a decorated service.
-func serviceInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
+func serviceInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
// ServiceInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
responseObject, err := getResponseAsJSONOBject(response)
@@ -51,9 +51,9 @@ func serviceInspectOperation(request *http.Request, response *http.Response, ope
}
serviceID := responseObject[serviceIdentifier].(string)
- resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls)
+ resourceControl := getResourceControlByResourceID(serviceID, executor.operationContext.resourceControls)
if resourceControl != nil {
- if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
+ if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
diff --git a/api/http/proxy/transport.go b/api/http/proxy/transport.go
index ca18ebcb5..29c1d6c72 100644
--- a/api/http/proxy/transport.go
+++ b/api/http/proxy/transport.go
@@ -15,6 +15,7 @@ type (
dockerTransport *http.Transport
ResourceControlService portainer.ResourceControlService
TeamMembershipService portainer.TeamMembershipService
+ SettingsService portainer.SettingsService
}
restrictedOperationContext struct {
isAdmin bool
@@ -22,7 +23,11 @@ type (
userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl
}
- restrictedOperationRequest func(*http.Request, *http.Response, *restrictedOperationContext) error
+ operationExecutor struct {
+ operationContext *restrictedOperationContext
+ labelBlackList []portainer.Pair
+ }
+ restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
)
func newSocketTransport(socketPath string) *http.Transport {
@@ -60,7 +65,6 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
}
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
- // return p.executeDockerRequest(request)
switch requestPath := request.URL.Path; requestPath {
case "/containers/create":
return p.executeDockerRequest(request)
@@ -69,7 +73,7 @@ func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Res
return p.administratorOperation(request)
case "/containers/json":
- return p.rewriteOperation(request, containerListOperation)
+ return p.rewriteOperationWithLabelFiltering(request, containerListOperation)
default:
// This section assumes /containers/**
@@ -96,9 +100,6 @@ func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Respo
case "/services/create":
return p.executeDockerRequest(request)
- case "/volumes/prune":
- return p.administratorOperation(request)
-
case "/services":
return p.rewriteOperation(request, serviceListOperation)
@@ -177,9 +178,69 @@ func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID s
return p.executeDockerRequest(request)
}
+// rewriteOperation will create a new operation context with data that will be used
+// to decorate the original request's response as well as retrieve all the black listed labels
+// to filter the resources.
+func (p *proxyTransport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
+ operationContext, err := p.createOperationContext(request)
+ if err != nil {
+ return nil, err
+ }
+
+ settings, err := p.SettingsService.Settings()
+ if err != nil {
+ return nil, err
+ }
+
+ executor := &operationExecutor{
+ operationContext: operationContext,
+ labelBlackList: settings.BlackListedLabels,
+ }
+
+ return p.executeRequestAndRewriteResponse(request, operation, executor)
+}
+
// rewriteOperation will create a new operation context with data that will be used
// to decorate the original request's response.
func (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
+ operationContext, err := p.createOperationContext(request)
+ if err != nil {
+ return nil, err
+ }
+
+ executor := &operationExecutor{
+ operationContext: operationContext,
+ }
+
+ return p.executeRequestAndRewriteResponse(request, operation, executor)
+}
+
+func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
+ response, err := p.executeDockerRequest(request)
+ if err != nil {
+ return response, err
+ }
+
+ err = operation(request, response, executor)
+ return response, err
+}
+
+// administratorOperation ensures that the user has administrator privileges
+// before executing the original request.
+func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
+ tokenData, err := security.RetrieveTokenData(request)
+ if err != nil {
+ return nil, err
+ }
+
+ if tokenData.Role != portainer.AdministratorRole {
+ return writeAccessDeniedResponse()
+ }
+
+ return p.executeDockerRequest(request)
+}
+
+func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) {
var err error
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
@@ -212,26 +273,5 @@ func (p *proxyTransport) rewriteOperation(request *http.Request, operation restr
operationContext.userTeamIDs = userTeamIDs
}
- response, err := p.executeDockerRequest(request)
- if err != nil {
- return response, err
- }
-
- err = operation(request, response, operationContext)
- return response, err
-}
-
-// administratorOperation ensures that the user has administrator privileges
-// before executing the original request.
-func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
- tokenData, err := security.RetrieveTokenData(request)
- if err != nil {
- return nil, err
- }
-
- if tokenData.Role != portainer.AdministratorRole {
- return writeAccessDeniedResponse()
- }
-
- return p.executeDockerRequest(request)
+ return operationContext, nil
}
diff --git a/api/http/proxy/utils.go b/api/http/proxy/utils.go
index 36afce97d..7f85a624a 100644
--- a/api/http/proxy/utils.go
+++ b/api/http/proxy/utils.go
@@ -15,3 +15,18 @@ func getResourceControlByResourceID(resourceID string, resourceControls []portai
}
return nil
}
+
+func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool {
+ for key, value := range containerLabels {
+ labelName := key
+ labelValue := value.(string)
+
+ for _, blackListedLabel := range labelBlackList {
+ if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue {
+ return true
+ }
+ }
+ }
+
+ return false
+}
diff --git a/api/http/proxy/volumes.go b/api/http/proxy/volumes.go
index c39805de8..ce451c0e2 100644
--- a/api/http/proxy/volumes.go
+++ b/api/http/proxy/volumes.go
@@ -14,7 +14,7 @@ const (
// volumeListOperation extracts the response as a JSON object, loop through the volume array
// decorate and/or filter the volumes based on resource controls before rewriting the response
-func volumeListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
+func volumeListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
var err error
// VolumeList response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
@@ -28,10 +28,10 @@ func volumeListOperation(request *http.Request, response *http.Response, operati
if responseObject["Volumes"] != nil {
volumeData := responseObject["Volumes"].([]interface{})
- if operationContext.isAdmin {
- volumeData, err = decorateVolumeList(volumeData, operationContext.resourceControls)
+ if executor.operationContext.isAdmin {
+ volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
} else {
- volumeData, err = filterVolumeList(volumeData, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
+ volumeData, err = filterVolumeList(volumeData, executor.operationContext.resourceControls, executor.operationContext.userID, executor.operationContext.userTeamIDs)
}
if err != nil {
return err
@@ -47,7 +47,7 @@ func volumeListOperation(request *http.Request, response *http.Response, operati
// volumeInspectOperation extracts the response as a JSON object, verify that the user
// has access to the volume based on resource control and either rewrite an access denied response
// or a decorated volume.
-func volumeInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
+func volumeInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
// VolumeInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
responseObject, err := getResponseAsJSONOBject(response)
@@ -60,9 +60,9 @@ func volumeInspectOperation(request *http.Request, response *http.Response, oper
}
volumeID := responseObject[volumeIdentifier].(string)
- resourceControl := getResourceControlByResourceID(volumeID, operationContext.resourceControls)
+ resourceControl := getResourceControlByResourceID(volumeID, executor.operationContext.resourceControls)
if resourceControl != nil {
- if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
+ if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
diff --git a/api/http/server.go b/api/http/server.go
index 11d126d33..c183e81a5 100644
--- a/api/http/server.go
+++ b/api/http/server.go
@@ -15,16 +15,16 @@ type Server struct {
AssetsPath string
AuthDisabled bool
EndpointManagement bool
+ Status *portainer.Status
UserService portainer.UserService
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
EndpointService portainer.EndpointService
ResourceControlService portainer.ResourceControlService
+ SettingsService portainer.SettingsService
CryptoService portainer.CryptoService
JWTService portainer.JWTService
FileService portainer.FileService
- Settings *portainer.Settings
- TemplatesURL string
Handler *handler.Handler
SSL bool
SSLCert string
@@ -34,7 +34,7 @@ type Server struct {
// Start starts the HTTP server
func (server *Server) Start() error {
requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled)
- proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService)
+ proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService)
var authHandler = handler.NewAuthHandler(requestBouncer, server.AuthDisabled)
authHandler.UserService = server.UserService
@@ -51,8 +51,11 @@ func (server *Server) Start() error {
teamHandler.TeamMembershipService = server.TeamMembershipService
var teamMembershipHandler = handler.NewTeamMembershipHandler(requestBouncer)
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
- var settingsHandler = handler.NewSettingsHandler(requestBouncer, server.Settings)
- var templatesHandler = handler.NewTemplatesHandler(requestBouncer, server.TemplatesURL)
+ var statusHandler = handler.NewStatusHandler(requestBouncer, server.Status)
+ var settingsHandler = handler.NewSettingsHandler(requestBouncer)
+ settingsHandler.SettingsService = server.SettingsService
+ var templatesHandler = handler.NewTemplatesHandler(requestBouncer)
+ templatesHandler.SettingsService = server.SettingsService
var dockerHandler = handler.NewDockerHandler(requestBouncer)
dockerHandler.EndpointService = server.EndpointService
dockerHandler.TeamMembershipService = server.TeamMembershipService
@@ -77,6 +80,7 @@ func (server *Server) Start() error {
EndpointHandler: endpointHandler,
ResourceHandler: resourceHandler,
SettingsHandler: settingsHandler,
+ StatusHandler: statusHandler,
TemplatesHandler: templatesHandler,
DockerHandler: dockerHandler,
WebSocketHandler: websocketHandler,
diff --git a/api/portainer.go b/api/portainer.go
index c0315ecaf..7051588dd 100644
--- a/api/portainer.go
+++ b/api/portainer.go
@@ -17,9 +17,6 @@ type (
ExternalEndpoints *string
SyncInterval *string
Endpoint *string
- Labels *[]Pair
- Logo *string
- Templates *string
NoAuth *bool
NoAnalytics *bool
TLSVerify *bool
@@ -30,15 +27,26 @@ type (
SSLCert *string
SSLKey *string
AdminPassword *string
+ // Deprecated fields
+ Logo *string
+ Templates *string
+ Labels *[]Pair
}
- // Settings represents Portainer settings.
+ // Status represents the application status.
+ Status struct {
+ Authentication bool `json:"Authentication"`
+ EndpointManagement bool `json:"EndpointManagement"`
+ Analytics bool `json:"Analytics"`
+ Version string `json:"Version"`
+ }
+
+ // Settings represents the application settings.
Settings struct {
- HiddenLabels []Pair `json:"hiddenLabels"`
- Logo string `json:"logo"`
- Authentication bool `json:"authentication"`
- Analytics bool `json:"analytics"`
- EndpointManagement bool `json:"endpointManagement"`
+ TemplatesURL string `json:"TemplatesURL"`
+ LogoURL string `json:"LogoURL"`
+ BlackListedLabels []Pair `json:"BlackListedLabels"`
+ DisplayExternalContributors bool `json:"DisplayExternalContributors"`
}
// User represents a user account.
@@ -209,6 +217,12 @@ type (
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
}
+ // SettingsService represents a service for managing application settings.
+ SettingsService interface {
+ Settings() (*Settings, error)
+ StoreSettings(settings *Settings) error
+ }
+
// VersionService represents a service for managing version data.
VersionService interface {
DBVersion() (int, error)
@@ -255,6 +269,8 @@ const (
APIVersion = "1.13.1"
// DBVersion is the version number of the Portainer database.
DBVersion = 2
+ // DefaultTemplatesURL represents the default URL for the templates definitions.
+ DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
)
const (
diff --git a/app/app.js b/app/app.js
index 513743c75..ddb5bd587 100644
--- a/app/app.js
+++ b/app/app.js
@@ -57,6 +57,7 @@ angular.module('portainer', [
'templates',
'user',
'users',
+ 'userSettings',
'volume',
'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
@@ -594,6 +595,19 @@ angular.module('portainer', [
}
}
})
+ .state('userSettings', {
+ url: '/userSettings/',
+ views: {
+ 'content@': {
+ templateUrl: 'app/components/userSettings/userSettings.html',
+ controller: 'UserSettingsController'
+ },
+ 'sidebar@': {
+ templateUrl: 'app/components/sidebar/sidebar.html',
+ controller: 'SidebarController'
+ }
+ }
+ })
.state('teams', {
url: '/teams/',
views: {
@@ -662,9 +676,11 @@ angular.module('portainer', [
}])
// This is your docker url that the api will use to make requests
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
- .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
+ // .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
.constant('DOCKER_ENDPOINT', 'api/docker')
- .constant('CONFIG_ENDPOINT', 'api/settings')
+ .constant('CONFIG_ENDPOINT', 'api/old_settings')
+ .constant('SETTINGS_ENDPOINT', 'api/settings')
+ .constant('STATUS_ENDPOINT', 'api/status')
.constant('AUTH_ENDPOINT', 'api/auth')
.constant('USERS_ENDPOINT', 'api/users')
.constant('TEAMS_ENDPOINT', 'api/teams')
@@ -672,5 +688,6 @@ angular.module('portainer', [
.constant('RESOURCE_CONTROL_ENDPOINT', 'api/resource_controls')
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
.constant('TEMPLATES_ENDPOINT', 'api/templates')
- .constant('PAGINATION_MAX_ITEMS', 10)
- .constant('UI_VERSION', 'v1.13.1');
+ .constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
+ .constant('PAGINATION_MAX_ITEMS', 10);
+ // .constant('UI_VERSION', 'v1.13.1');
diff --git a/app/components/auth/authController.js b/app/components/auth/authController.js
index d6edeff95..14ef479d7 100644
--- a/app/components/auth/authController.js
+++ b/app/components/auth/authController.js
@@ -1,6 +1,6 @@
angular.module('auth', [])
-.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
-function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) {
+.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',
@@ -13,6 +13,8 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
error: false
};
+ $scope.logo = StateManager.getState().application.logo;
+
if (!$scope.applicationState.application.authentication) {
EndpointService.endpoints()
.then(function success(data) {
@@ -59,10 +61,6 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
$state.go('dashboard');
}
- Config.$promise.then(function (c) {
- $scope.logo = c.logo;
- });
-
$scope.createAdminUser = function() {
var password = $sanitize($scope.initPasswordData.password);
Users.initAdminUser({password: password}, function (d) {
diff --git a/app/components/containerConsole/containerConsoleController.js b/app/components/containerConsole/containerConsoleController.js
index d00881c6f..b9da6c20f 100644
--- a/app/components/containerConsole/containerConsoleController.js
+++ b/app/components/containerConsole/containerConsoleController.js
@@ -1,6 +1,6 @@
angular.module('containerConsole', [])
-.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications',
-function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, EndpointProvider, Notifications) {
+.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications',
+function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvider, Notifications) {
$scope.state = {};
$scope.state.loaded = false;
$scope.state.connected = false;
diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js
index 01c50acb9..b3cf31d85 100644
--- a/app/components/containers/containersController.js
+++ b/app/components/containers/containersController.js
@@ -1,9 +1,9 @@
angular.module('containers', [])
- .controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'Info', 'Settings', 'Notifications', 'Config', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider',
- function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, Info, Settings, Notifications, Config, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
+ .controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'Info', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider',
+ function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, Info, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
- $scope.state.displayAll = Settings.displayAll;
+ $scope.state.displayAll = true;
$scope.state.displayIP = false;
$scope.sortType = 'State';
$scope.sortReverse = false;
@@ -25,9 +25,6 @@ angular.module('containers', [])
$scope.state.selectedItemCount = 0;
Container.query(data, function (d) {
var containers = d;
- if ($scope.containersToHideLabels) {
- containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
- }
$scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container);
model.Status = $filter('containerstatus')(model.Status);
@@ -59,7 +56,7 @@ angular.module('containers', [])
counter = counter - 1;
if (counter === 0) {
$('#loadContainersSpinner').hide();
- update({all: Settings.displayAll ? 1 : 0});
+ update({all: $scope.state.displayAll ? 1 : 0});
}
};
angular.forEach(items, function (c) {
@@ -134,8 +131,7 @@ angular.module('containers', [])
};
$scope.toggleGetAll = function () {
- Settings.displayAll = $scope.state.displayAll;
- update({all: Settings.displayAll ? 1 : 0});
+ update({all: $scope.state.displayAll ? 1 : 0});
};
$scope.startAction = function () {
@@ -206,15 +202,16 @@ angular.module('containers', [])
return swarm_hosts;
}
- Config.$promise.then(function (c) {
- $scope.containersToHideLabels = c.hiddenLabels;
+ function initView(){
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
Info.get({}, function (d) {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
- update({all: Settings.displayAll ? 1 : 0});
+ update({all: $scope.state.displayAll ? 1 : 0});
});
} else {
- update({all: Settings.displayAll ? 1 : 0});
+ update({all: $scope.state.displayAll ? 1 : 0});
}
- });
+ }
+
+ initView();
}]);
diff --git a/app/components/createContainer/createContainerController.js b/app/components/createContainer/createContainerController.js
index 3f8bfe73a..f5375bf1e 100644
--- a/app/components/createContainer/createContainerController.js
+++ b/app/components/createContainer/createContainerController.js
@@ -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('createContainer', [])
-.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator',
-function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) {
+.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator',
+function ($q, $scope, $state, $stateParams, $filter, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) {
$scope.formValues = {
alwaysPull: true,
@@ -233,47 +233,41 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
}
function initView() {
- Config.$promise.then(function (c) {
- var containersToHideLabels = c.hiddenLabels;
-
- Volume.query({}, function (d) {
- $scope.availableVolumes = d.Volumes;
- }, function (e) {
- Notifications.error('Failure', e, 'Unable to retrieve volumes');
- });
-
- Network.query({}, function (d) {
- var networks = d;
- if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
- networks = d.filter(function (network) {
- if (network.Scope === 'global') {
- return network;
- }
- });
- $scope.globalNetworkCount = networks.length;
- networks.push({Name: 'bridge'});
- networks.push({Name: 'host'});
- networks.push({Name: 'none'});
- }
- networks.push({Name: 'container'});
- $scope.availableNetworks = networks;
- if (!_.find(networks, {'Name': 'bridge'})) {
- $scope.config.HostConfig.NetworkMode = 'nat';
- }
- }, function (e) {
- Notifications.error('Failure', e, 'Unable to retrieve networks');
- });
-
- Container.query({}, function (d) {
- var containers = d;
- if (containersToHideLabels) {
- containers = ContainerHelper.hideContainers(d, containersToHideLabels);
- }
- $scope.runningContainers = containers;
- }, function(e) {
- Notifications.error('Failure', e, 'Unable to retrieve running containers');
- });
+ Volume.query({}, function (d) {
+ $scope.availableVolumes = d.Volumes;
+ }, function (e) {
+ Notifications.error('Failure', e, 'Unable to retrieve volumes');
});
+
+ Network.query({}, function (d) {
+ var networks = d;
+ if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM' || $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') {
+ networks = d.filter(function (network) {
+ if (network.Scope === 'global') {
+ return network;
+ }
+ });
+ $scope.globalNetworkCount = networks.length;
+ networks.push({Name: 'bridge'});
+ networks.push({Name: 'host'});
+ networks.push({Name: 'none'});
+ }
+ networks.push({Name: 'container'});
+ $scope.availableNetworks = networks;
+ if (!_.find(networks, {'Name': 'bridge'})) {
+ $scope.config.HostConfig.NetworkMode = 'nat';
+ }
+ }, function (e) {
+ Notifications.error('Failure', e, 'Unable to retrieve networks');
+ });
+
+ Container.query({}, function (d) {
+ var containers = d;
+ $scope.runningContainers = containers;
+ }, function(e) {
+ Notifications.error('Failure', e, 'Unable to retrieve running containers');
+ });
+
}
function validateForm(accessControlData, isAdmin) {
@@ -327,5 +321,4 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
}
initView();
-
}]);
diff --git a/app/components/dashboard/dashboardController.js b/app/components/dashboard/dashboardController.js
index 89f38ce00..9b01ceb56 100644
--- a/app/components/dashboard/dashboardController.js
+++ b/app/components/dashboard/dashboardController.js
@@ -1,6 +1,6 @@
angular.module('dashboard', [])
-.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications',
-function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) {
+.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications',
+function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) {
$scope.containerData = {
total: 0
@@ -15,14 +15,10 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
total: 0
};
- function prepareContainerData(d, containersToHideLabels) {
+ function prepareContainerData(d) {
var running = 0;
var stopped = 0;
-
var containers = d;
- if (containersToHideLabels) {
- containers = ContainerHelper.hideContainers(d, containersToHideLabels);
- }
for (var i = 0; i < containers.length; i++) {
var item = containers[i];
@@ -65,7 +61,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
$scope.infoData = info;
}
- function fetchDashboardData(containersToHideLabels) {
+ function initView() {
$('#loadingViewSpinner').show();
$q.all([
Container.query({all: 1}).$promise,
@@ -74,7 +70,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
Network.query({}).$promise,
Info.get({}).$promise
]).then(function (d) {
- prepareContainerData(d[0], containersToHideLabels);
+ prepareContainerData(d[0]);
prepareImageData(d[1]);
prepareVolumeData(d[2]);
prepareNetworkData(d[3]);
@@ -86,7 +82,5 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
});
}
- Config.$promise.then(function (c) {
- fetchDashboardData(c.hiddenLabels);
- });
+ initView();
}]);
diff --git a/app/components/images/imagesController.js b/app/components/images/imagesController.js
index 70918b82d..f5ecbc618 100644
--- a/app/components/images/imagesController.js
+++ b/app/components/images/imagesController.js
@@ -1,6 +1,6 @@
angular.module('images', [])
-.controller('ImagesController', ['$scope', '$state', 'Config', 'ImageService', 'Notifications', 'Pagination', 'ModalService',
-function ($scope, $state, Config, ImageService, Notifications, Pagination, ModalService) {
+.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'Pagination', 'ModalService',
+function ($scope, $state, ImageService, Notifications, Pagination, ModalService) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('images');
$scope.sortType = 'RepoTags';
diff --git a/app/components/network/networkController.js b/app/components/network/networkController.js
index f6b44df84..291b6ca9e 100644
--- a/app/components/network/networkController.js
+++ b/app/components/network/networkController.js
@@ -1,6 +1,6 @@
angular.module('network', [])
-.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Notifications',
-function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Notifications) {
+.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'Container', 'ContainerHelper', 'Notifications',
+function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHelper, Notifications) {
$scope.removeNetwork = function removeNetwork(networkId) {
$('#loadingViewSpinner').show();
@@ -36,21 +36,7 @@ function ($scope, $state, $stateParams, $filter, Config, Network, Container, Con
});
};
- function getNetwork() {
- $('#loadingViewSpinner').show();
- Network.get({id: $stateParams.id}, function success(data) {
- $scope.network = data;
- getContainersInNetwork(data);
- }, function error(err) {
- $('#loadingViewSpinner').hide();
- Notifications.error('Failure', err, 'Unable to retrieve network info');
- });
- }
-
function filterContainersInNetwork(network, containers) {
- if ($scope.containersToHideLabels) {
- containers = ContainerHelper.hideContainers(containers, $scope.containersToHideLabels);
- }
var containersInNetwork = [];
containers.forEach(function(container) {
var containerInNetwork = network.Containers[container.Id];
@@ -93,8 +79,16 @@ function ($scope, $state, $stateParams, $filter, Config, Network, Container, Con
}
}
- Config.$promise.then(function (c) {
- $scope.containersToHideLabels = c.hiddenLabels;
- getNetwork();
- });
+ function initView() {
+ $('#loadingViewSpinner').show();
+ Network.get({id: $stateParams.id}, function success(data) {
+ $scope.network = data;
+ getContainersInNetwork(data);
+ }, function error(err) {
+ $('#loadingViewSpinner').hide();
+ Notifications.error('Failure', err, 'Unable to retrieve network info');
+ });
+ }
+
+ initView();
}]);
diff --git a/app/components/networks/networksController.js b/app/components/networks/networksController.js
index 2e8e6e9ed..8468a1283 100644
--- a/app/components/networks/networksController.js
+++ b/app/components/networks/networksController.js
@@ -1,6 +1,6 @@
angular.module('networks', [])
-.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Notifications', 'Pagination',
-function ($scope, $state, Network, Config, Notifications, Pagination) {
+.controller('NetworksController', ['$scope', '$state', 'Network', 'Notifications', 'Pagination',
+function ($scope, $state, Network, Notifications, Pagination) {
$scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
$scope.state.selectedItemCount = 0;
@@ -97,7 +97,7 @@ function ($scope, $state, Network, Config, Notifications, Pagination) {
});
};
- function fetchNetworks() {
+ function initView() {
$('#loadNetworksSpinner').show();
Network.query({}, function (d) {
$scope.networks = d;
@@ -109,7 +109,5 @@ function ($scope, $state, Network, Config, Notifications, Pagination) {
});
}
- Config.$promise.then(function (c) {
- fetchNetworks();
- });
+ initView();
}]);
diff --git a/app/components/settings/settings.html b/app/components/settings/settings.html
index f0b235b46..d3e9dcede 100644
--- a/app/components/settings/settings.html
+++ b/app/components/settings/settings.html
@@ -1,66 +1,153 @@