feat(settings): add settings management (#906)

pull/908/head
Anthony Lapenna 2017-06-01 10:14:55 +02:00 committed by GitHub
parent 5e74a3993b
commit c7e306841a
93 changed files with 1086 additions and 457 deletions

View File

@ -22,6 +22,7 @@ type Store struct {
EndpointService *EndpointService EndpointService *EndpointService
ResourceControlService *ResourceControlService ResourceControlService *ResourceControlService
VersionService *VersionService VersionService *VersionService
SettingsService *SettingsService
db *bolt.DB db *bolt.DB
checkForDataMigration bool checkForDataMigration bool
@ -35,6 +36,7 @@ const (
teamMembershipBucketName = "team_membership" teamMembershipBucketName = "team_membership"
endpointBucketName = "endpoints" endpointBucketName = "endpoints"
resourceControlBucketName = "resource_control" resourceControlBucketName = "resource_control"
settingsBucketName = "settings"
) )
// NewStore initializes a new Store and the associated services // NewStore initializes a new Store and the associated services
@ -47,6 +49,7 @@ func NewStore(storePath string) (*Store, error) {
EndpointService: &EndpointService{}, EndpointService: &EndpointService{},
ResourceControlService: &ResourceControlService{}, ResourceControlService: &ResourceControlService{},
VersionService: &VersionService{}, VersionService: &VersionService{},
SettingsService: &SettingsService{},
} }
store.UserService.store = store store.UserService.store = store
store.TeamService.store = store store.TeamService.store = store
@ -54,6 +57,7 @@ func NewStore(storePath string) (*Store, error) {
store.EndpointService.store = store store.EndpointService.store = store
store.ResourceControlService.store = store store.ResourceControlService.store = store
store.VersionService.store = store store.VersionService.store = store
store.SettingsService.store = store
_, err := os.Stat(storePath + "/" + databaseFileName) _, err := os.Stat(storePath + "/" + databaseFileName)
if err != nil && os.IsNotExist(err) { if err != nil && os.IsNotExist(err) {
@ -100,6 +104,10 @@ func (store *Store) Open() error {
if err != nil { if err != nil {
return err return err
} }
_, err = tx.CreateBucketIfNotExists([]byte(settingsBucketName))
if err != nil {
return err
}
return nil return nil
}) })
} }

View File

@ -57,6 +57,16 @@ func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error
return json.Unmarshal(data, rc) 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. // Itob returns an 8-byte big endian representation of v.
// This function is typically used for encoding integer IDs to byte slices // This function is typically used for encoding integer IDs to byte slices
// so that they can be used as BoltDB keys. // so that they can be used as BoltDB keys.

View File

@ -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
})
}

View File

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"log"
"time" "time"
"github.com/portainer/portainer" "github.com/portainer/portainer"
@ -29,14 +30,11 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
flags := &portainer.CLIFlags{ flags := &portainer.CLIFlags{
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(), 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(), 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(), 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(), 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(), Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
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(), 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(defaultNoAuth).Bool(),
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).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(), 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(), 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(), 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() kingpin.Parse()
@ -79,6 +81,8 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return errNoAuthExcludeAdminPassword return errNoAuthExcludeAdminPassword
} }
displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels)
return nil return nil
} }
@ -122,3 +126,15 @@ func validateSyncInterval(syncInterval string) error {
} }
return nil 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.")
}
}

View File

@ -6,7 +6,6 @@ const (
defaultBindAddress = ":9000" defaultBindAddress = ":9000"
defaultDataDirectory = "/data" defaultDataDirectory = "/data"
defaultAssetsDirectory = "." defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false" defaultNoAuth = "false"
defaultNoAnalytics = "false" defaultNoAnalytics = "false"
defaultTLSVerify = "false" defaultTLSVerify = "false"

View File

@ -4,7 +4,6 @@ const (
defaultBindAddress = ":9000" defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data" defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "." defaultAssetsDirectory = "."
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
defaultNoAuth = "false" defaultNoAuth = "false"
defaultNoAnalytics = "false" defaultNoAnalytics = "false"
defaultTLSVerify = "false" defaultTLSVerify = "false"

View File

@ -82,16 +82,43 @@ func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpo
return authorizeEndpointMgmt return authorizeEndpointMgmt
} }
func initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Settings { func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Settings{ return &portainer.Status{
HiddenLabels: *flags.Labels,
Logo: *flags.Logo,
Analytics: !*flags.NoAnalytics, Analytics: !*flags.NoAnalytics,
Authentication: !*flags.NoAuth, Authentication: !*flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt, 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 { func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
endpoints, err := endpointService.Endpoints() endpoints, err := endpointService.Endpoints()
if err != nil { if err != nil {
@ -114,7 +141,12 @@ func main() {
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval) 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 != "" { if *flags.Endpoint != "" {
var endpoints []portainer.Endpoint var endpoints []portainer.Endpoint
@ -156,10 +188,9 @@ func main() {
} }
var server portainer.Server = &http.Server{ var server portainer.Server = &http.Server{
Status: applicationStatus,
BindAddress: *flags.Addr, BindAddress: *flags.Addr,
AssetsPath: *flags.Assets, AssetsPath: *flags.Assets,
Settings: settings,
TemplatesURL: *flags.Templates,
AuthDisabled: *flags.NoAuth, AuthDisabled: *flags.NoAuth,
EndpointManagement: authorizeEndpointMgmt, EndpointManagement: authorizeEndpointMgmt,
UserService: store.UserService, UserService: store.UserService,
@ -167,6 +198,7 @@ func main() {
TeamMembershipService: store.TeamMembershipService, TeamMembershipService: store.TeamMembershipService,
EndpointService: store.EndpointService, EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService, ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
CryptoService: cryptoService, CryptoService: cryptoService,
JWTService: jwtService, JWTService: jwtService,
FileService: fileService, FileService: fileService,
@ -176,7 +208,7 @@ func main() {
} }
log.Printf("Starting Portainer on %s", *flags.Addr) log.Printf("Starting Portainer on %s", *flags.Addr)
err := server.Start() err = server.Start()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -47,6 +47,11 @@ const (
ErrDBVersionNotFound = Error("DB version not found") ErrDBVersionNotFound = Error("DB version not found")
) )
// Settings errors.
const (
ErrSettingsNotFound = Error("Settings not found")
)
// Crypto errors. // Crypto errors.
const ( const (
ErrCryptoHashFailure = Error("Unable to hash data") ErrCryptoHashFailure = Error("Unable to hash data")

View File

@ -18,6 +18,7 @@ type Handler struct {
TeamMembershipHandler *TeamMembershipHandler TeamMembershipHandler *TeamMembershipHandler
EndpointHandler *EndpointHandler EndpointHandler *EndpointHandler
ResourceHandler *ResourceHandler ResourceHandler *ResourceHandler
StatusHandler *StatusHandler
SettingsHandler *SettingsHandler SettingsHandler *SettingsHandler
TemplatesHandler *TemplatesHandler TemplatesHandler *TemplatesHandler
DockerHandler *DockerHandler DockerHandler *DockerHandler
@ -53,6 +54,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/settings") { } else if strings.HasPrefix(r.URL.Path, "/api/settings") {
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r) 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") { } else if strings.HasPrefix(r.URL.Path, "/api/templates") {
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/upload") { } else if strings.HasPrefix(r.URL.Path, "/api/upload") {

View File

@ -1,6 +1,9 @@
package handler package handler
import ( import (
"encoding/json"
"github.com/asaskevich/govalidator"
"github.com/portainer/portainer" "github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error" httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security" "github.com/portainer/portainer/http/security"
@ -12,32 +15,69 @@ import (
"github.com/gorilla/mux" "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 { type SettingsHandler struct {
*mux.Router *mux.Router
Logger *log.Logger Logger *log.Logger
settings *portainer.Settings SettingsService portainer.SettingsService
} }
// NewSettingsHandler returns a new instance of SettingsHandler. // NewSettingsHandler returns a new instance of OldSettingsHandler.
func NewSettingsHandler(bouncer *security.RequestBouncer, settings *portainer.Settings) *SettingsHandler { func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
h := &SettingsHandler{ h := &SettingsHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
settings: settings,
} }
h.Handle("/settings", 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 return h
} }
// handleGetSettings handles GET requests on /settings // handleGetSettings handles GET requests on /settings
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) { func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { settings, err := handler.SettingsService.Settings()
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet}) if err != nil {
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
return 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:""`
} }

View File

@ -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
}

View File

@ -7,6 +7,7 @@ import (
"os" "os"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/portainer/portainer"
httperror "github.com/portainer/portainer/http/error" httperror "github.com/portainer/portainer/http/error"
"github.com/portainer/portainer/http/security" "github.com/portainer/portainer/http/security"
) )
@ -14,8 +15,8 @@ import (
// TemplatesHandler represents an HTTP API handler for managing templates. // TemplatesHandler represents an HTTP API handler for managing templates.
type TemplatesHandler struct { type TemplatesHandler struct {
*mux.Router *mux.Router
Logger *log.Logger Logger *log.Logger
containerTemplatesURL string SettingsService portainer.SettingsService
} }
const ( const (
@ -23,11 +24,10 @@ const (
) )
// NewTemplatesHandler returns a new instance of TemplatesHandler. // NewTemplatesHandler returns a new instance of TemplatesHandler.
func NewTemplatesHandler(bouncer *security.RequestBouncer, containerTemplatesURL string) *TemplatesHandler { func NewTemplatesHandler(bouncer *security.RequestBouncer) *TemplatesHandler {
h := &TemplatesHandler{ h := &TemplatesHandler{
Router: mux.NewRouter(), Router: mux.NewRouter(),
Logger: log.New(os.Stderr, "", log.LstdFlags), Logger: log.New(os.Stderr, "", log.LstdFlags),
containerTemplatesURL: containerTemplatesURL,
} }
h.Handle("/templates", h.Handle("/templates",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates))) bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates)))
@ -49,7 +49,12 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht
var templatesURL string var templatesURL string
if key == "containers" { 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" { } else if key == "linuxserver.io" {
templatesURL = containerTemplatesURLLinuxServerIo templatesURL = containerTemplatesURLLinuxServerIo
} else { } else {

View File

@ -15,7 +15,7 @@ const (
// containerListOperation extracts the response as a JSON object, loop through the containers array // 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 // 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 var err error
// ContainerList response is a JSON array // ContainerList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList // 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 return err
} }
if operationContext.isAdmin { if executor.operationContext.isAdmin {
responseArray, err = decorateContainerList(responseArray, operationContext.resourceControls) responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
} else { } 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 { if err != nil {
return err return err
} }
if executor.labelBlackList != nil {
responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList)
if err != nil {
return err
}
}
return rewriteResponse(response, responseArray, http.StatusOK) return rewriteResponse(response, responseArray, http.StatusOK)
} }
// containerInspectOperation extracts the response as a JSON object, verify that the user // 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) // 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. // 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 // ContainerInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect // https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
responseObject, err := getResponseAsJSONOBject(response) responseObject, err := getResponseAsJSONOBject(response)
@ -52,9 +60,10 @@ func containerInspectOperation(request *http.Request, response *http.Response, o
} }
containerID := responseObject[containerIdentifier].(string) containerID := responseObject[containerIdentifier].(string)
resourceControl := getResourceControlByResourceID(containerID, operationContext.resourceControls) resourceControl := getResourceControlByResourceID(containerID, executor.operationContext.resourceControls)
if resourceControl != nil { 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) responseObject = decorateObject(responseObject, resourceControl)
} else { } else {
return rewriteAccessDeniedResponse(response) return rewriteAccessDeniedResponse(response)
@ -64,9 +73,10 @@ func containerInspectOperation(request *http.Request, response *http.Response, o
containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject) containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject)
if containerLabels != nil && containerLabels[containerLabelForServiceIdentifier] != nil { if containerLabels != nil && containerLabels[containerLabelForServiceIdentifier] != nil {
serviceID := containerLabels[containerLabelForServiceIdentifier].(string) serviceID := containerLabels[containerLabelForServiceIdentifier].(string)
resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls) resourceControl := getResourceControlByResourceID(serviceID, executor.operationContext.resourceControls)
if resourceControl != nil { 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) responseObject = decorateObject(responseObject, resourceControl)
} else { } else {
return rewriteAccessDeniedResponse(response) return rewriteAccessDeniedResponse(response)

View File

@ -13,6 +13,7 @@ import (
type proxyFactory struct { type proxyFactory struct {
ResourceControlService portainer.ResourceControlService ResourceControlService portainer.ResourceControlService
TeamMembershipService portainer.TeamMembershipService TeamMembershipService portainer.TeamMembershipService
SettingsService portainer.SettingsService
} }
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
@ -37,6 +38,7 @@ func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
transport := &proxyTransport{ transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService, ResourceControlService: factory.ResourceControlService,
TeamMembershipService: factory.TeamMembershipService, TeamMembershipService: factory.TeamMembershipService,
SettingsService: factory.SettingsService,
dockerTransport: newSocketTransport(path), dockerTransport: newSocketTransport(path),
} }
proxy.Transport = transport proxy.Transport = transport
@ -48,6 +50,7 @@ func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReversePro
transport := &proxyTransport{ transport := &proxyTransport{
ResourceControlService: factory.ResourceControlService, ResourceControlService: factory.ResourceControlService,
TeamMembershipService: factory.TeamMembershipService, TeamMembershipService: factory.TeamMembershipService,
SettingsService: factory.SettingsService,
dockerTransport: newHTTPTransport(), dockerTransport: newHTTPTransport(),
} }
proxy.Transport = transport proxy.Transport = transport

View File

@ -65,6 +65,27 @@ func filterContainerList(containerData []interface{}, resourceControls []portain
return filteredContainerData, nil 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 // 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). // 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 // Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList

View File

@ -15,12 +15,13 @@ type Manager struct {
} }
// NewManager initializes a new proxy Service // 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{ return &Manager{
proxies: cmap.New(), proxies: cmap.New(),
proxyFactory: &proxyFactory{ proxyFactory: &proxyFactory{
ResourceControlService: resourceControlService, ResourceControlService: resourceControlService,
TeamMembershipService: teamMembershipService, TeamMembershipService: teamMembershipService,
SettingsService: settingsService,
}, },
} }
} }

View File

@ -14,7 +14,7 @@ const (
// serviceListOperation extracts the response as a JSON array, loop through the service array // 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 // 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 var err error
// ServiceList response is a JSON array // ServiceList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList // 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 return err
} }
if operationContext.isAdmin { if executor.operationContext.isAdmin {
responseArray, err = decorateServiceList(responseArray, operationContext.resourceControls) responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
} else { } 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 { if err != nil {
return err 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 // 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 // has access to the service based on resource control and either rewrite an access denied response
// or a decorated service. // 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 // ServiceInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect // https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
responseObject, err := getResponseAsJSONOBject(response) responseObject, err := getResponseAsJSONOBject(response)
@ -51,9 +51,9 @@ func serviceInspectOperation(request *http.Request, response *http.Response, ope
} }
serviceID := responseObject[serviceIdentifier].(string) serviceID := responseObject[serviceIdentifier].(string)
resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls) resourceControl := getResourceControlByResourceID(serviceID, executor.operationContext.resourceControls)
if resourceControl != nil { 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) responseObject = decorateObject(responseObject, resourceControl)
} else { } else {
return rewriteAccessDeniedResponse(response) return rewriteAccessDeniedResponse(response)

View File

@ -15,6 +15,7 @@ type (
dockerTransport *http.Transport dockerTransport *http.Transport
ResourceControlService portainer.ResourceControlService ResourceControlService portainer.ResourceControlService
TeamMembershipService portainer.TeamMembershipService TeamMembershipService portainer.TeamMembershipService
SettingsService portainer.SettingsService
} }
restrictedOperationContext struct { restrictedOperationContext struct {
isAdmin bool isAdmin bool
@ -22,7 +23,11 @@ type (
userTeamIDs []portainer.TeamID userTeamIDs []portainer.TeamID
resourceControls []portainer.ResourceControl 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 { 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) { func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
// return p.executeDockerRequest(request)
switch requestPath := request.URL.Path; requestPath { switch requestPath := request.URL.Path; requestPath {
case "/containers/create": case "/containers/create":
return p.executeDockerRequest(request) return p.executeDockerRequest(request)
@ -69,7 +73,7 @@ func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Res
return p.administratorOperation(request) return p.administratorOperation(request)
case "/containers/json": case "/containers/json":
return p.rewriteOperation(request, containerListOperation) return p.rewriteOperationWithLabelFiltering(request, containerListOperation)
default: default:
// This section assumes /containers/** // This section assumes /containers/**
@ -96,9 +100,6 @@ func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Respo
case "/services/create": case "/services/create":
return p.executeDockerRequest(request) return p.executeDockerRequest(request)
case "/volumes/prune":
return p.administratorOperation(request)
case "/services": case "/services":
return p.rewriteOperation(request, serviceListOperation) return p.rewriteOperation(request, serviceListOperation)
@ -177,9 +178,69 @@ func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID s
return p.executeDockerRequest(request) 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 // rewriteOperation will create a new operation context with data that will be used
// to decorate the original request's response. // to decorate the original request's response.
func (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) { 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 var err error
tokenData, err := security.RetrieveTokenData(request) tokenData, err := security.RetrieveTokenData(request)
if err != nil { if err != nil {
@ -212,26 +273,5 @@ func (p *proxyTransport) rewriteOperation(request *http.Request, operation restr
operationContext.userTeamIDs = userTeamIDs operationContext.userTeamIDs = userTeamIDs
} }
response, err := p.executeDockerRequest(request) return operationContext, nil
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)
} }

View File

@ -15,3 +15,18 @@ func getResourceControlByResourceID(resourceID string, resourceControls []portai
} }
return nil 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
}

View File

@ -14,7 +14,7 @@ const (
// volumeListOperation extracts the response as a JSON object, loop through the volume array // 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 // 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 var err error
// VolumeList response is a JSON object // VolumeList response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList // 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 { if responseObject["Volumes"] != nil {
volumeData := responseObject["Volumes"].([]interface{}) volumeData := responseObject["Volumes"].([]interface{})
if operationContext.isAdmin { if executor.operationContext.isAdmin {
volumeData, err = decorateVolumeList(volumeData, operationContext.resourceControls) volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
} else { } 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 { if err != nil {
return err 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 // 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 // has access to the volume based on resource control and either rewrite an access denied response
// or a decorated volume. // 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 // VolumeInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect // https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
responseObject, err := getResponseAsJSONOBject(response) responseObject, err := getResponseAsJSONOBject(response)
@ -60,9 +60,9 @@ func volumeInspectOperation(request *http.Request, response *http.Response, oper
} }
volumeID := responseObject[volumeIdentifier].(string) volumeID := responseObject[volumeIdentifier].(string)
resourceControl := getResourceControlByResourceID(volumeID, operationContext.resourceControls) resourceControl := getResourceControlByResourceID(volumeID, executor.operationContext.resourceControls)
if resourceControl != nil { 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) responseObject = decorateObject(responseObject, resourceControl)
} else { } else {
return rewriteAccessDeniedResponse(response) return rewriteAccessDeniedResponse(response)

View File

@ -15,16 +15,16 @@ type Server struct {
AssetsPath string AssetsPath string
AuthDisabled bool AuthDisabled bool
EndpointManagement bool EndpointManagement bool
Status *portainer.Status
UserService portainer.UserService UserService portainer.UserService
TeamService portainer.TeamService TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService TeamMembershipService portainer.TeamMembershipService
EndpointService portainer.EndpointService EndpointService portainer.EndpointService
ResourceControlService portainer.ResourceControlService ResourceControlService portainer.ResourceControlService
SettingsService portainer.SettingsService
CryptoService portainer.CryptoService CryptoService portainer.CryptoService
JWTService portainer.JWTService JWTService portainer.JWTService
FileService portainer.FileService FileService portainer.FileService
Settings *portainer.Settings
TemplatesURL string
Handler *handler.Handler Handler *handler.Handler
SSL bool SSL bool
SSLCert string SSLCert string
@ -34,7 +34,7 @@ type Server struct {
// Start starts the HTTP server // Start starts the HTTP server
func (server *Server) Start() error { func (server *Server) Start() error {
requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled) 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) var authHandler = handler.NewAuthHandler(requestBouncer, server.AuthDisabled)
authHandler.UserService = server.UserService authHandler.UserService = server.UserService
@ -51,8 +51,11 @@ func (server *Server) Start() error {
teamHandler.TeamMembershipService = server.TeamMembershipService teamHandler.TeamMembershipService = server.TeamMembershipService
var teamMembershipHandler = handler.NewTeamMembershipHandler(requestBouncer) var teamMembershipHandler = handler.NewTeamMembershipHandler(requestBouncer)
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
var settingsHandler = handler.NewSettingsHandler(requestBouncer, server.Settings) var statusHandler = handler.NewStatusHandler(requestBouncer, server.Status)
var templatesHandler = handler.NewTemplatesHandler(requestBouncer, server.TemplatesURL) var settingsHandler = handler.NewSettingsHandler(requestBouncer)
settingsHandler.SettingsService = server.SettingsService
var templatesHandler = handler.NewTemplatesHandler(requestBouncer)
templatesHandler.SettingsService = server.SettingsService
var dockerHandler = handler.NewDockerHandler(requestBouncer) var dockerHandler = handler.NewDockerHandler(requestBouncer)
dockerHandler.EndpointService = server.EndpointService dockerHandler.EndpointService = server.EndpointService
dockerHandler.TeamMembershipService = server.TeamMembershipService dockerHandler.TeamMembershipService = server.TeamMembershipService
@ -77,6 +80,7 @@ func (server *Server) Start() error {
EndpointHandler: endpointHandler, EndpointHandler: endpointHandler,
ResourceHandler: resourceHandler, ResourceHandler: resourceHandler,
SettingsHandler: settingsHandler, SettingsHandler: settingsHandler,
StatusHandler: statusHandler,
TemplatesHandler: templatesHandler, TemplatesHandler: templatesHandler,
DockerHandler: dockerHandler, DockerHandler: dockerHandler,
WebSocketHandler: websocketHandler, WebSocketHandler: websocketHandler,

View File

@ -17,9 +17,6 @@ type (
ExternalEndpoints *string ExternalEndpoints *string
SyncInterval *string SyncInterval *string
Endpoint *string Endpoint *string
Labels *[]Pair
Logo *string
Templates *string
NoAuth *bool NoAuth *bool
NoAnalytics *bool NoAnalytics *bool
TLSVerify *bool TLSVerify *bool
@ -30,15 +27,26 @@ type (
SSLCert *string SSLCert *string
SSLKey *string SSLKey *string
AdminPassword *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 { Settings struct {
HiddenLabels []Pair `json:"hiddenLabels"` TemplatesURL string `json:"TemplatesURL"`
Logo string `json:"logo"` LogoURL string `json:"LogoURL"`
Authentication bool `json:"authentication"` BlackListedLabels []Pair `json:"BlackListedLabels"`
Analytics bool `json:"analytics"` DisplayExternalContributors bool `json:"DisplayExternalContributors"`
EndpointManagement bool `json:"endpointManagement"`
} }
// User represents a user account. // User represents a user account.
@ -209,6 +217,12 @@ type (
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error 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 represents a service for managing version data.
VersionService interface { VersionService interface {
DBVersion() (int, error) DBVersion() (int, error)
@ -255,6 +269,8 @@ const (
APIVersion = "1.13.1" APIVersion = "1.13.1"
// DBVersion is the version number of the Portainer database. // DBVersion is the version number of the Portainer database.
DBVersion = 2 DBVersion = 2
// DefaultTemplatesURL represents the default URL for the templates definitions.
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
) )
const ( const (

View File

@ -57,6 +57,7 @@ angular.module('portainer', [
'templates', 'templates',
'user', 'user',
'users', 'users',
'userSettings',
'volume', 'volume',
'volumes']) 'volumes'])
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) { .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
@ -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', { .state('teams', {
url: '/teams/', url: '/teams/',
views: { views: {
@ -662,9 +676,11 @@ angular.module('portainer', [
}]) }])
// This is your docker url that the api will use to make requests // 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 // 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('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('AUTH_ENDPOINT', 'api/auth')
.constant('USERS_ENDPOINT', 'api/users') .constant('USERS_ENDPOINT', 'api/users')
.constant('TEAMS_ENDPOINT', 'api/teams') .constant('TEAMS_ENDPOINT', 'api/teams')
@ -672,5 +688,6 @@ angular.module('portainer', [
.constant('RESOURCE_CONTROL_ENDPOINT', 'api/resource_controls') .constant('RESOURCE_CONTROL_ENDPOINT', 'api/resource_controls')
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints') .constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
.constant('TEMPLATES_ENDPOINT', 'api/templates') .constant('TEMPLATES_ENDPOINT', 'api/templates')
.constant('PAGINATION_MAX_ITEMS', 10) .constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
.constant('UI_VERSION', 'v1.13.1'); .constant('PAGINATION_MAX_ITEMS', 10);
// .constant('UI_VERSION', 'v1.13.1');

View File

@ -1,6 +1,6 @@
angular.module('auth', []) angular.module('auth', [])
.controller('AuthenticationController', ['$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, Config, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) { function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) {
$scope.authData = { $scope.authData = {
username: 'admin', username: 'admin',
@ -13,6 +13,8 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
error: false error: false
}; };
$scope.logo = StateManager.getState().application.logo;
if (!$scope.applicationState.application.authentication) { if (!$scope.applicationState.application.authentication) {
EndpointService.endpoints() EndpointService.endpoints()
.then(function success(data) { .then(function success(data) {
@ -59,10 +61,6 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
$state.go('dashboard'); $state.go('dashboard');
} }
Config.$promise.then(function (c) {
$scope.logo = c.logo;
});
$scope.createAdminUser = function() { $scope.createAdminUser = function() {
var password = $sanitize($scope.initPasswordData.password); var password = $sanitize($scope.initPasswordData.password);
Users.initAdminUser({password: password}, function (d) { Users.initAdminUser({password: password}, function (d) {

View File

@ -1,6 +1,6 @@
angular.module('containerConsole', []) angular.module('containerConsole', [])
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications', .controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications',
function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, EndpointProvider, Notifications) { function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvider, Notifications) {
$scope.state = {}; $scope.state = {};
$scope.state.loaded = false; $scope.state.loaded = false;
$scope.state.connected = false; $scope.state.connected = false;

View File

@ -1,9 +1,9 @@
angular.module('containers', []) angular.module('containers', [])
.controller('ContainersController', ['$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, Settings, Notifications, Config, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) { function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, Info, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('containers'); $scope.state.pagination_count = Pagination.getPaginationCount('containers');
$scope.state.displayAll = Settings.displayAll; $scope.state.displayAll = true;
$scope.state.displayIP = false; $scope.state.displayIP = false;
$scope.sortType = 'State'; $scope.sortType = 'State';
$scope.sortReverse = false; $scope.sortReverse = false;
@ -25,9 +25,6 @@ angular.module('containers', [])
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
Container.query(data, function (d) { Container.query(data, function (d) {
var containers = d; var containers = d;
if ($scope.containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
}
$scope.containers = containers.map(function (container) { $scope.containers = containers.map(function (container) {
var model = new ContainerViewModel(container); var model = new ContainerViewModel(container);
model.Status = $filter('containerstatus')(model.Status); model.Status = $filter('containerstatus')(model.Status);
@ -59,7 +56,7 @@ angular.module('containers', [])
counter = counter - 1; counter = counter - 1;
if (counter === 0) { if (counter === 0) {
$('#loadContainersSpinner').hide(); $('#loadContainersSpinner').hide();
update({all: Settings.displayAll ? 1 : 0}); update({all: $scope.state.displayAll ? 1 : 0});
} }
}; };
angular.forEach(items, function (c) { angular.forEach(items, function (c) {
@ -134,8 +131,7 @@ angular.module('containers', [])
}; };
$scope.toggleGetAll = function () { $scope.toggleGetAll = function () {
Settings.displayAll = $scope.state.displayAll; update({all: $scope.state.displayAll ? 1 : 0});
update({all: Settings.displayAll ? 1 : 0});
}; };
$scope.startAction = function () { $scope.startAction = function () {
@ -206,15 +202,16 @@ angular.module('containers', [])
return swarm_hosts; return swarm_hosts;
} }
Config.$promise.then(function (c) { function initView(){
$scope.containersToHideLabels = c.hiddenLabels;
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') { if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
Info.get({}, function (d) { Info.get({}, function (d) {
$scope.swarm_hosts = retrieveSwarmHostsInfo(d); $scope.swarm_hosts = retrieveSwarmHostsInfo(d);
update({all: Settings.displayAll ? 1 : 0}); update({all: $scope.state.displayAll ? 1 : 0});
}); });
} else { } else {
update({all: Settings.displayAll ? 1 : 0}); update({all: $scope.state.displayAll ? 1 : 0});
} }
}); }
initView();
}]); }]);

View File

@ -1,8 +1,8 @@
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services. // @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
// See app/components/templates/templatesController.js as a reference. // See app/components/templates/templatesController.js as a reference.
angular.module('createContainer', []) 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', .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, Config, 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 = { $scope.formValues = {
alwaysPull: true, alwaysPull: true,
@ -233,47 +233,41 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
} }
function initView() { function initView() {
Config.$promise.then(function (c) { Volume.query({}, function (d) {
var containersToHideLabels = c.hiddenLabels; $scope.availableVolumes = d.Volumes;
}, function (e) {
Volume.query({}, function (d) { Notifications.error('Failure', e, 'Unable to retrieve volumes');
$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');
});
}); });
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) { function validateForm(accessControlData, isAdmin) {
@ -327,5 +321,4 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
} }
initView(); initView();
}]); }]);

View File

@ -1,6 +1,6 @@
angular.module('dashboard', []) angular.module('dashboard', [])
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications', .controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications',
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) { function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) {
$scope.containerData = { $scope.containerData = {
total: 0 total: 0
@ -15,14 +15,10 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
total: 0 total: 0
}; };
function prepareContainerData(d, containersToHideLabels) { function prepareContainerData(d) {
var running = 0; var running = 0;
var stopped = 0; var stopped = 0;
var containers = d; var containers = d;
if (containersToHideLabels) {
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
}
for (var i = 0; i < containers.length; i++) { for (var i = 0; i < containers.length; i++) {
var item = containers[i]; var item = containers[i];
@ -65,7 +61,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
$scope.infoData = info; $scope.infoData = info;
} }
function fetchDashboardData(containersToHideLabels) { function initView() {
$('#loadingViewSpinner').show(); $('#loadingViewSpinner').show();
$q.all([ $q.all([
Container.query({all: 1}).$promise, Container.query({all: 1}).$promise,
@ -74,7 +70,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
Network.query({}).$promise, Network.query({}).$promise,
Info.get({}).$promise Info.get({}).$promise
]).then(function (d) { ]).then(function (d) {
prepareContainerData(d[0], containersToHideLabels); prepareContainerData(d[0]);
prepareImageData(d[1]); prepareImageData(d[1]);
prepareVolumeData(d[2]); prepareVolumeData(d[2]);
prepareNetworkData(d[3]); prepareNetworkData(d[3]);
@ -86,7 +82,5 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
}); });
} }
Config.$promise.then(function (c) { initView();
fetchDashboardData(c.hiddenLabels);
});
}]); }]);

View File

@ -1,6 +1,6 @@
angular.module('images', []) angular.module('images', [])
.controller('ImagesController', ['$scope', '$state', 'Config', 'ImageService', 'Notifications', 'Pagination', 'ModalService', .controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'Pagination', 'ModalService',
function ($scope, $state, Config, ImageService, Notifications, Pagination, ModalService) { function ($scope, $state, ImageService, Notifications, Pagination, ModalService) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('images'); $scope.state.pagination_count = Pagination.getPaginationCount('images');
$scope.sortType = 'RepoTags'; $scope.sortType = 'RepoTags';

View File

@ -1,6 +1,6 @@
angular.module('network', []) angular.module('network', [])
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Notifications', .controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'Container', 'ContainerHelper', 'Notifications',
function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Notifications) { function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHelper, Notifications) {
$scope.removeNetwork = function removeNetwork(networkId) { $scope.removeNetwork = function removeNetwork(networkId) {
$('#loadingViewSpinner').show(); $('#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) { function filterContainersInNetwork(network, containers) {
if ($scope.containersToHideLabels) {
containers = ContainerHelper.hideContainers(containers, $scope.containersToHideLabels);
}
var containersInNetwork = []; var containersInNetwork = [];
containers.forEach(function(container) { containers.forEach(function(container) {
var containerInNetwork = network.Containers[container.Id]; var containerInNetwork = network.Containers[container.Id];
@ -93,8 +79,16 @@ function ($scope, $state, $stateParams, $filter, Config, Network, Container, Con
} }
} }
Config.$promise.then(function (c) { function initView() {
$scope.containersToHideLabels = c.hiddenLabels; $('#loadingViewSpinner').show();
getNetwork(); 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();
}]); }]);

View File

@ -1,6 +1,6 @@
angular.module('networks', []) angular.module('networks', [])
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Notifications', 'Pagination', .controller('NetworksController', ['$scope', '$state', 'Network', 'Notifications', 'Pagination',
function ($scope, $state, Network, Config, Notifications, Pagination) { function ($scope, $state, Network, Notifications, Pagination) {
$scope.state = {}; $scope.state = {};
$scope.state.pagination_count = Pagination.getPaginationCount('networks'); $scope.state.pagination_count = Pagination.getPaginationCount('networks');
$scope.state.selectedItemCount = 0; $scope.state.selectedItemCount = 0;
@ -97,7 +97,7 @@ function ($scope, $state, Network, Config, Notifications, Pagination) {
}); });
}; };
function fetchNetworks() { function initView() {
$('#loadNetworksSpinner').show(); $('#loadNetworksSpinner').show();
Network.query({}, function (d) { Network.query({}, function (d) {
$scope.networks = d; $scope.networks = d;
@ -109,7 +109,5 @@ function ($scope, $state, Network, Config, Notifications, Pagination) {
}); });
} }
Config.$promise.then(function (c) { initView();
fetchNetworks();
});
}]); }]);

View File

@ -1,66 +1,153 @@
<rd-header> <rd-header>
<rd-header-title title="Settings"> <rd-header-title title="Settings">
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
</rd-header-title> </rd-header-title>
<rd-header-content>Settings</rd-header-content> <rd-header-content>Settings</rd-header-content>
</rd-header> </rd-header>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-xs-12"> <div class="col-sm-12">
<rd-widget> <rd-widget>
<rd-widget-header icon="fa-lock" title="Change user password"></rd-widget-header> <rd-widget-header icon="fa-cogs" title="Application settings"></rd-widget-header>
<rd-widget-body> <rd-widget-body>
<form class="form-horizontal" style="margin-top: 15px;"> <form class="form-horizontal">
<!-- current-password-input --> <!-- logo -->
<div class="col-sm-12 form-section-title">
Logo
</div>
<div class="form-group"> <div class="form-group">
<label for="current_password" class="col-sm-2 control-label text-left">Current password</label> <div class="col-sm-12">
<div class="col-sm-8"> <label for="toggle_logo" class="control-label text-left">
<div class="input-group"> Use custom logo
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span> </label>
<input type="password" class="form-control" ng-model="formValues.currentPassword" id="current_password"> <label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo"><i></i>
</label>
</div>
</div>
<div ng-if="formValues.customLogo">
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can specify the URL to your logo here. For an optimal display, logo dimensions should be 155px by 55px.
</span>
</div>
<div class="form-group">
<label for="logo_url" class="col-sm-1 control-label text-left">
URL
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="settings.LogoURL" id="logo_url" placeholder="https://mycompany.com/logo.png">
</div> </div>
</div> </div>
</div> </div>
<!-- !current-password-input --> <!-- !logo -->
<div class="form-group" ng-if="invalidPassword"> <!-- app-templates -->
<div class="col-sm-12 form-section-title">
App Templates
</div>
<div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<i class="fa fa-times red-icon" aria-hidden="true"></i> <label for="toggle_templates" class="control-label text-left">
<span class="small text-muted">Current password is not valid</span> Use custom templates
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_templates" ng-model="formValues.customTemplates"><i></i>
</label>
</div> </div>
</div> </div>
<!-- new-password-input --> <div ng-if="formValues.customTemplates">
<div class="form-group"> <div class="form-group">
<label for="new_password" class="col-sm-2 control-label text-left">New password</label> <span class="col-sm-12 text-muted small">
<div class="col-sm-8"> You can specify the URL to your own template definitions file here. See <a href="https://portainer.readthedocs.io/en/stable/templates.html" target="_blank">Portainer documentation</a> for more details.
<div class="input-group"> </span>
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span> </div>
<input type="password" class="form-control" ng-model="formValues.newPassword" id="new_password"> <div class="form-group" >
<label for="templates_url" class="col-sm-1 control-label text-left">
URL
</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="settings.TemplatesURL" id="templates_url" placeholder="https://myserver.mydomain/templates.json">
</div> </div>
</div> </div>
</div> </div>
<!-- !new-password-input -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword.length >= 8]" aria-hidden="true"></i> <label for="toggle_external_contrib" class="control-label text-left">
<span class="small text-muted">Your new password must be at least 8 characters long</span> Hide external contributions
<portainer-tooltip position="bottom" message="When enabled, external contributions such as LinuxServer.io will not be displayed in the sidebar."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_external_contrib" ng-model="formValues.externalContributions"><i></i>
</label>
</div> </div>
</div> </div>
<!-- confirm-password-input --> <!-- !app-templates -->
<div class="form-group"> <!-- actions -->
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<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.newPassword !== '' && formValues.newPassword === formValues.confirmPassword]" aria-hidden="true"></i></span>
</div>
</div>
</div>
<!-- !confirm-password-input -->
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.currentPassword || formValues.newPassword.length < 8 || formValues.newPassword !== formValues.confirmPassword" ng-click="updatePassword()">Update password</button> <button type="button" class="btn btn-primary btn-sm" ng-click="saveApplicationSettings()">Save</button>
<i id="updateSettingsSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<!-- <span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span> -->
</div> </div>
</div> </div>
<!-- !actions -->
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-tags" title="Filtered containers"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can hide containers with specific labels from Portainer UI. You need to specify the label name and value.
</span>
</div>
<div class="form-group">
<label for="header_name" class="col-sm-1 control-label text-left">Name</label>
<div class="col-sm-11 col-md-4">
<input type="text" class="form-control" id="header_name" ng-model="formValues.labelName" placeholder="e.g. com.example.foo">
</div>
<label for="header_value" class="col-sm-1 margin-sm-top control-label text-left">Value</label>
<div class="col-sm-11 col-md-4 margin-sm-top">
<input type="text" class="form-control" id="header_value" ng-model="formValues.labelValue" placeholder="e.g. bar">
</div>
<div class="col-sm-12 col-md-2 margin-sm-top">
<button type="button" class="btn btn-primary btn-sm" ng-click="addFilteredContainerLabel()"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add label</button>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="label in settings.BlackListedLabels">
<td>{{ label.name }}</td>
<td>{{ label.value }}</td>
<td><button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button></td>
</tr>
<tr ng-if="settings.BlackListedLabels.length === 0">
<td colspan="2" class="text-center text-muted">No filtered containers labels.</td>
</tr>
<tr ng-if="!settings.BlackListedLabels">
<td colspan="2" class="text-center text-muted">Loading...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- !filtered-labels -->
</form> </form>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>

View File

@ -1,29 +1,94 @@
angular.module('settings', []) angular.module('settings', [])
.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications', .controller('SettingsController', ['$scope', '$state', 'Notifications', 'SettingsService', 'StateManager', 'DEFAULT_TEMPLATES_URL',
function ($scope, $state, $sanitize, Authentication, UserService, Notifications) { function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_TEMPLATES_URL) {
$scope.formValues = { $scope.formValues = {
currentPassword: '', customLogo: false,
newPassword: '', customTemplates: false,
confirmPassword: '' externalContributions: false,
labelName: '',
labelValue: ''
}; };
$scope.updatePassword = function() { $scope.removeFilteredContainerLabel = function(index) {
$scope.invalidPassword = false; var settings = $scope.settings;
var userID = Authentication.getUserDetails().ID; settings.BlackListedLabels.splice(index, 1);
var currentPassword = $sanitize($scope.formValues.currentPassword);
var newPassword = $sanitize($scope.formValues.newPassword);
UserService.updateUserPassword(userID, currentPassword, newPassword) updateSettings(settings, false);
.then(function success() { };
Notifications.success('Success', 'Password successfully updated');
$state.reload(); $scope.addFilteredContainerLabel = function() {
var settings = $scope.settings;
var label = {
name: $scope.formValues.labelName,
value: $scope.formValues.labelValue
};
settings.BlackListedLabels.push(label);
updateSettings(settings, true);
};
$scope.saveApplicationSettings = function() {
var settings = $scope.settings;
if (!$scope.formValues.customLogo) {
settings.LogoURL = '';
}
if (!$scope.formValues.customTemplates) {
settings.TemplatesURL = DEFAULT_TEMPLATES_URL;
}
settings.DisplayExternalContributors = !$scope.formValues.externalContributions;
updateSettings(settings, false);
};
function resetFormValues() {
$scope.formValues.labelName = '';
$scope.formValues.labelValue = '';
}
function updateSettings(settings, resetForm) {
$('#loadingViewSpinner').show();
SettingsService.update(settings)
.then(function success(data) {
Notifications.success('Settings updated');
StateManager.updateLogo(settings.LogoURL);
StateManager.updateExternalContributions(settings.DisplayExternalContributors);
if (resetForm) {
resetFormValues();
}
}) })
.catch(function error(err) { .catch(function error(err) {
if (err.invalidPassword) { Notifications.error('Failure', err, 'Unable to update settings');
$scope.invalidPassword = true; })
} else { .finally(function final() {
Notifications.error('Failure', err, err.msg); $('#loadingViewSpinner').hide();
}
}); });
}; }
function initView() {
$('#loadingViewSpinner').show();
SettingsService.settings()
.then(function success(data) {
var settings = data;
$scope.settings = settings;
if (settings.LogoURL !== '') {
$scope.formValues.customLogo = true;
}
if (settings.TemplatesURL !== DEFAULT_TEMPLATES_URL) {
$scope.formValues.customTemplates = true;
}
$scope.formValues.externalContributions = !settings.DisplayExternalContributors;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');
})
.finally(function final() {
$('#loadingViewSpinner').hide();
});
}
initView();
}]); }]);

View File

@ -21,7 +21,7 @@
</li> </li>
<li class="sidebar-list"> <li class="sidebar-list">
<a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a> <a ui-sref="templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket"></span></a>
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')"> <div class="sidebar-sublist" ng-if="toggle && displayExternalContributors && ($state.current.name === 'templates' || $state.current.name === 'templates_linuxserver')">
<a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a> <a ui-sref="templates_linuxserver" ui-sref-active="active">LinuxServer.io</a>
</div> </div>
</li> </li>
@ -52,7 +52,7 @@
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'"> <li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
<a ui-sref="docker" ui-sref-active="active">Docker <span class="menu-icon fa fa-th"></span></a> <a ui-sref="docker" ui-sref-active="active">Docker <span class="menu-icon fa fa-th"></span></a>
</li> </li>
<li class="sidebar-title" ng-if="isAdmin || isTeamLeader"> <li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
<span>Portainer settings</span> <span>Portainer settings</span>
</li> </li>
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)"> <li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
@ -64,6 +64,9 @@
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin"> <li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a> <a ui-sref="endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug"></span></a>
</li> </li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<a ui-sref="settings" ui-sref-active="active">Settings <span class="menu-icon fa fa-cogs"></span></a>
</li>
</ul> </ul>
<div class="sidebar-footer"> <div class="sidebar-footer">
<div class="col-xs-12"> <div class="col-xs-12">

View File

@ -1,12 +1,10 @@
angular.module('sidebar', []) angular.module('sidebar', [])
.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', .controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService',
function ($q, $scope, $state, Settings, Config, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService) { function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService) {
Config.$promise.then(function (c) { $scope.uiVersion = StateManager.getState().application.version;
$scope.logo = c.logo; $scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
}); $scope.logo = StateManager.getState().application.logo;
$scope.uiVersion = Settings.uiVersion;
$scope.endpoints = []; $scope.endpoints = [];
$scope.switchEndpoint = function(endpoint) { $scope.switchEndpoint = function(endpoint) {

View File

@ -1,6 +1,6 @@
angular.module('templates', []) angular.module('templates', [])
.controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', '$filter', 'Config', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'ControllerDataPipeline', 'FormValidator', .controller('TemplatesController', ['$scope', '$q', '$state', '$stateParams', '$anchorScroll', '$filter', 'ContainerService', 'ContainerHelper', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'Pagination', 'ResourceControlService', 'Authentication', 'ControllerDataPipeline', 'FormValidator',
function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, Config, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, ControllerDataPipeline, FormValidator) { function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, ControllerDataPipeline, FormValidator) {
$scope.state = { $scope.state = {
selectedTemplate: null, selectedTemplate: null,
showAdvancedOptions: false, showAdvancedOptions: false,
@ -159,31 +159,29 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, Config, Cont
function initTemplates() { function initTemplates() {
var templatesKey = $stateParams.key; var templatesKey = $stateParams.key;
Config.$promise.then(function (c) { $q.all({
$q.all({ templates: TemplateService.getTemplates(templatesKey),
templates: TemplateService.getTemplates(templatesKey), containers: ContainerService.getContainers(0),
containers: ContainerService.getContainers(0, c.hiddenLabels), networks: NetworkService.networks(),
networks: NetworkService.networks(), volumes: VolumeService.getVolumes()
volumes: VolumeService.getVolumes() })
}) .then(function success(data) {
.then(function success(data) { $scope.templates = data.templates;
$scope.templates = data.templates; var availableCategories = [];
var availableCategories = []; angular.forEach($scope.templates, function(template) {
angular.forEach($scope.templates, function(template) { availableCategories = availableCategories.concat(template.Categories);
availableCategories = availableCategories.concat(template.Categories);
});
$scope.availableCategories = _.sortBy(_.uniq(availableCategories));
$scope.runningContainers = data.containers;
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
$scope.availableVolumes = data.volumes.Volumes;
})
.catch(function error(err) {
$scope.templates = [];
Notifications.error('Failure', err, 'An error occured during apps initialization.');
})
.finally(function final(){
$('#loadTemplatesSpinner').hide();
}); });
$scope.availableCategories = _.sortBy(_.uniq(availableCategories));
$scope.runningContainers = data.containers;
$scope.availableNetworks = filterNetworksBasedOnProvider(data.networks);
$scope.availableVolumes = data.volumes.Volumes;
})
.catch(function error(err) {
$scope.templates = [];
Notifications.error('Failure', err, 'An error occured during apps initialization.');
})
.finally(function final(){
$('#loadTemplatesSpinner').hide();
}); });
} }

View File

@ -0,0 +1,68 @@
<rd-header>
<rd-header-title title="User settings">
</rd-header-title>
<rd-header-content>User settings</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-lock" title="Change user password"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal" style="margin-top: 15px;">
<!-- current-password-input -->
<div class="form-group">
<label for="current_password" class="col-sm-2 control-label text-left">Current password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.currentPassword" id="current_password">
</div>
</div>
</div>
<!-- !current-password-input -->
<div class="form-group" ng-if="invalidPassword">
<div class="col-sm-12">
<i class="fa fa-times red-icon" aria-hidden="true"></i>
<span class="small text-muted">Current password is not valid</span>
</div>
</div>
<!-- new-password-input -->
<div class="form-group">
<label for="new_password" class="col-sm-2 control-label text-left">New password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<input type="password" class="form-control" ng-model="formValues.newPassword" id="new_password">
</div>
</div>
</div>
<!-- !new-password-input -->
<div class="form-group">
<div class="col-sm-12">
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[formValues.newPassword.length >= 8]" aria-hidden="true"></i>
<span class="small text-muted">Your new password must be at least 8 characters long</span>
</div>
</div>
<!-- confirm-password-input -->
<div class="form-group">
<label for="confirm_password" class="col-sm-2 control-label text-left">Confirm password</label>
<div class="col-sm-8">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
<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.newPassword !== '' && formValues.newPassword === formValues.confirmPassword]" aria-hidden="true"></i></span>
</div>
</div>
</div>
<!-- !confirm-password-input -->
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.currentPassword || formValues.newPassword.length < 8 || formValues.newPassword !== formValues.confirmPassword" ng-click="updatePassword()">Update password</button>
</div>
</div>
</form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View File

@ -0,0 +1,29 @@
angular.module('userSettings', [])
.controller('UserSettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications',
function ($scope, $state, $sanitize, Authentication, UserService, Notifications) {
$scope.formValues = {
currentPassword: '',
newPassword: '',
confirmPassword: ''
};
$scope.updatePassword = function() {
$scope.invalidPassword = false;
var userID = Authentication.getUserDetails().ID;
var currentPassword = $sanitize($scope.formValues.currentPassword);
var newPassword = $sanitize($scope.formValues.newPassword);
UserService.updateUserPassword(userID, currentPassword, newPassword)
.then(function success() {
Notifications.success('Success', 'Password successfully updated');
$state.reload();
})
.catch(function error(err) {
if (err.invalidPassword) {
$scope.invalidPassword = true;
} else {
Notifications.error('Failure', err, err.msg);
}
});
};
}]);

View File

@ -7,7 +7,7 @@ angular
link: function (scope, iElement, iAttrs) { link: function (scope, iElement, iAttrs) {
scope.username = Authentication.getUserDetails().username; scope.username = Authentication.getUserDetails().username;
}, },
template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="settings" style="margin-right: 5px;"><u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u></a><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u><i class="fa fa-sign-out" aria-hidden="true"></i> log out</u></a></div></div>', template: '<div class="breadcrumb-links"><div class="pull-left" ng-transclude></div><div class="pull-right" ng-if="username"><a ui-sref="userSettings" style="margin-right: 5px;"><u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u></a><a ui-sref="auth({logout: true})" class="text-danger" style="margin-right: 25px;"><u><i class="fa fa-sign-out" aria-hidden="true"></i> log out</u></a></div></div>',
restrict: 'E' restrict: 'E'
}; };
return directive; return directive;

View File

@ -7,20 +7,5 @@ angular.module('portainer.helpers')
return splitargs(command); return splitargs(command);
}; };
helper.hideContainers = function(containers, containersToHideLabels) {
return containers.filter(function (container) {
var filterContainer = false;
containersToHideLabels.forEach(function(label, index) {
if (_.has(container.Labels, label.name) &&
container.Labels[label.name] === label.value) {
filterContainer = true;
}
});
if (!filterContainer) {
return container;
}
});
};
return helper; return helper;
}]); }]);

View File

@ -0,0 +1,6 @@
function SettingsViewModel(data) {
this.TemplatesURL = data.TemplatesURL;
this.LogoURL = data.LogoURL;
this.BlackListedLabels = data.BlackListedLabels;
this.DisplayExternalContributors = data.DisplayExternalContributors;
}

6
app/models/api/status.js Normal file
View File

@ -0,0 +1,6 @@
function StatusViewModel(data) {
this.Authentication = data.Authentication;
this.EndpointManagement = data.EndpointManagement;
this.Analytics = data.Analytics;
this.Version = data.Version;
}

8
app/rest/api/settings.js Normal file
View File

@ -0,0 +1,8 @@
angular.module('portainer.rest')
.factory('Settings', ['$resource', 'SETTINGS_ENDPOINT', function SettingsFactory($resource, SETTINGS_ENDPOINT) {
'use strict';
return $resource(SETTINGS_ENDPOINT, {}, {
get: { method: 'GET' },
update: { method: 'PUT' }
});
}]);

7
app/rest/api/status.js Normal file
View File

@ -0,0 +1,7 @@
angular.module('portainer.rest')
.factory('Status', ['$resource', 'STATUS_ENDPOINT', function StatusFactory($resource, STATUS_ENDPOINT) {
'use strict';
return $resource(STATUS_ENDPOINT, {}, {
get: { method: 'GET' }
});
}]);

View File

@ -1,4 +0,0 @@
angular.module('portainer.rest')
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
return $resource(CONFIG_ENDPOINT).get();
}]);

View File

@ -1,10 +0,0 @@
angular.module('portainer.rest')
.factory('ContainerCommit', ['$resource', 'Settings', 'EndpointProvider', function ContainerCommitFactory($resource, Settings, EndpointProvider) {
'use strict';
return $resource(Settings.url + '/:endpointId/commit', {
endpointId: EndpointProvider.endpointID
},
{
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}}
});
}]);

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Container', ['$resource', 'Settings', 'EndpointProvider', function ContainerFactory($resource, Settings, EndpointProvider) { .factory('Container', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ContainerFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/containers/:id/:action', { return $resource(DOCKER_ENDPOINT + '/:endpointId/containers/:id/:action', {
name: '@name', name: '@name',
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },

View File

@ -0,0 +1,10 @@
angular.module('portainer.rest')
.factory('ContainerCommit', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ContainerCommitFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/commit', {
endpointId: EndpointProvider.endpointID
},
{
commit: {method: 'POST', params: {container: '@id', repo: '@repo', tag: '@tag'}}
});
}]);

View File

@ -1,11 +1,11 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('ContainerLogs', ['$http', 'Settings', 'EndpointProvider', function ContainerLogsFactory($http, Settings, EndpointProvider) { .factory('ContainerLogs', ['$http', 'DOCKER_ENDPOINT', 'EndpointProvider', function ContainerLogsFactory($http, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return { return {
get: function (id, params, callback) { get: function (id, params, callback) {
$http({ $http({
method: 'GET', method: 'GET',
url: Settings.url + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/logs', url: DOCKER_ENDPOINT + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/logs',
params: { params: {
'stdout': params.stdout || 0, 'stdout': params.stdout || 0,
'stderr': params.stderr || 0, 'stderr': params.stderr || 0,

View File

@ -1,11 +1,11 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('ContainerTop', ['$http', 'Settings', 'EndpointProvider', function ($http, Settings, EndpointProvider) { .factory('ContainerTop', ['$http', 'DOCKER_ENDPOINT', 'EndpointProvider', function ($http, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return { return {
get: function (id, params, callback, errorCallback) { get: function (id, params, callback, errorCallback) {
$http({ $http({
method: 'GET', method: 'GET',
url: Settings.url + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/top', url: DOCKER_ENDPOINT + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/top',
params: { params: {
ps_args: params.ps_args ps_args: params.ps_args
} }

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Events', ['$resource', 'Settings', 'EndpointProvider', function EventFactory($resource, Settings, EndpointProvider) { .factory('Events', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function EventFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/events', { return $resource(DOCKER_ENDPOINT + '/:endpointId/events', {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },
{ {

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Exec', ['$resource', 'Settings', 'EndpointProvider', function ExecFactory($resource, Settings, EndpointProvider) { .factory('Exec', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ExecFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/exec/:id/:action', { return $resource(DOCKER_ENDPOINT + '/:endpointId/exec/:id/:action', {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },
{ {

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Image', ['$resource', 'Settings', 'EndpointProvider', function ImageFactory($resource, Settings, EndpointProvider) { .factory('Image', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ImageFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/images/:id/:action', { return $resource(DOCKER_ENDPOINT + '/:endpointId/images/:id/:action', {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },
{ {

7
app/rest/docker/info.js Normal file
View File

@ -0,0 +1,7 @@
angular.module('portainer.rest')
.factory('Info', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function InfoFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/info', {
endpointId: EndpointProvider.endpointID
});
}]);

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Network', ['$resource', 'Settings', 'EndpointProvider', function NetworkFactory($resource, Settings, EndpointProvider) { .factory('Network', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function NetworkFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/networks/:id/:action', { return $resource(DOCKER_ENDPOINT + '/:endpointId/networks/:id/:action', {
id: '@id', id: '@id',
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Node', ['$resource', 'Settings', 'EndpointProvider', function NodeFactory($resource, Settings, EndpointProvider) { .factory('Node', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function NodeFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/nodes/:id/:action', { return $resource(DOCKER_ENDPOINT + '/:endpointId/nodes/:id/:action', {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },
{ {

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Secret', ['$resource', 'Settings', 'EndpointProvider', function SecretFactory($resource, Settings, EndpointProvider) { .factory('Secret', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function SecretFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/secrets/:id/:action', { return $resource(DOCKER_ENDPOINT + '/:endpointId/secrets/:id/:action', {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, { }, {
get: { method: 'GET', params: {id: '@id'} }, get: { method: 'GET', params: {id: '@id'} },

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Service', ['$resource', 'Settings', 'EndpointProvider', function ServiceFactory($resource, Settings, EndpointProvider) { .factory('Service', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ServiceFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/services/:id/:action', { return $resource(DOCKER_ENDPOINT + '/:endpointId/services/:id/:action', {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },
{ {

10
app/rest/docker/swarm.js Normal file
View File

@ -0,0 +1,10 @@
angular.module('portainer.rest')
.factory('Swarm', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function SwarmFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/swarm', {
endpointId: EndpointProvider.endpointID
},
{
get: {method: 'GET'}
});
}]);

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Task', ['$resource', 'Settings', 'EndpointProvider', function TaskFactory($resource, Settings, EndpointProvider) { .factory('Task', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function TaskFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/tasks/:id', { return $resource(DOCKER_ENDPOINT + '/:endpointId/tasks/:id', {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },
{ {

View File

@ -0,0 +1,7 @@
angular.module('portainer.rest')
.factory('Version', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function VersionFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict';
return $resource(DOCKER_ENDPOINT + '/:endpointId/version', {
endpointId: EndpointProvider.endpointID
});
}]);

View File

@ -1,7 +1,7 @@
angular.module('portainer.rest') angular.module('portainer.rest')
.factory('Volume', ['$resource', 'Settings', 'EndpointProvider', function VolumeFactory($resource, Settings, EndpointProvider) { .factory('Volume', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function VolumeFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
'use strict'; 'use strict';
return $resource(Settings.url + '/:endpointId/volumes/:id/:action', return $resource(DOCKER_ENDPOINT + '/:endpointId/volumes/:id/:action',
{ {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID
}, },

View File

@ -1,7 +0,0 @@
angular.module('portainer.rest')
.factory('Info', ['$resource', 'Settings', 'EndpointProvider', function InfoFactory($resource, Settings, EndpointProvider) {
'use strict';
return $resource(Settings.url + '/:endpointId/info', {
endpointId: EndpointProvider.endpointID
});
}]);

View File

@ -1,10 +0,0 @@
angular.module('portainer.rest')
.factory('Swarm', ['$resource', 'Settings', 'EndpointProvider', function SwarmFactory($resource, Settings, EndpointProvider) {
'use strict';
return $resource(Settings.url + '/:endpointId/swarm', {
endpointId: EndpointProvider.endpointID
},
{
get: {method: 'GET'}
});
}]);

View File

@ -1,7 +0,0 @@
angular.module('portainer.rest')
.factory('Version', ['$resource', 'Settings', 'EndpointProvider', function VersionFactory($resource, Settings, EndpointProvider) {
'use strict';
return $resource(Settings.url + '/:endpointId/version', {
endpointId: EndpointProvider.endpointID
});
}]);

View File

@ -0,0 +1,26 @@
angular.module('portainer.services')
.factory('SettingsService', ['$q', 'Settings', function SettingsServiceFactory($q, Settings) {
'use strict';
var service = {};
service.settings = function() {
var deferred = $q.defer();
Settings.get().$promise
.then(function success(data) {
var status = new SettingsViewModel(data);
deferred.resolve(status);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve application settings', err: err });
});
return deferred.promise;
};
service.update = function(settings) {
return Settings.update({}, settings).$promise;
};
return service;
}]);

View File

@ -0,0 +1,22 @@
angular.module('portainer.services')
.factory('StatusService', ['$q', 'Status', function StatusServiceFactory($q, Status) {
'use strict';
var service = {};
service.status = function() {
var deferred = $q.defer();
Status.get().$promise
.then(function success(data) {
var status = new StatusViewModel(data);
deferred.resolve(status);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve application status', err: err });
});
return deferred.promise;
};
return service;
}]);

View File

@ -1,17 +1,14 @@
angular.module('portainer.services') angular.module('portainer.services')
.factory('ContainerService', ['$q', 'Container', 'ContainerHelper', 'ResourceControlService', function ContainerServiceFactory($q, Container, ContainerHelper, ResourceControlService) { .factory('ContainerService', ['$q', 'Container', 'ResourceControlService', function ContainerServiceFactory($q, Container, ResourceControlService) {
'use strict'; 'use strict';
var service = {}; var service = {};
service.getContainers = function (all, hiddenLabels) { service.getContainers = function (all) {
var deferred = $q.defer(); var deferred = $q.defer();
Container.query({ all: all }).$promise Container.query({ all: all }).$promise
.then(function success(data) { .then(function success(data) {
var containers = data; var containers = data;
if (hiddenLabels) { deferred.resolve(containers);
containers = ContainerHelper.hideContainers(data, hiddenLabels);
}
deferred.resolve(data);
}) })
.catch(function error(err) { .catch(function error(err) {
deferred.reject({ msg: 'Unable to retriever containers', err: err }); deferred.reject({ msg: 'Unable to retriever containers', err: err });

View File

@ -1,5 +1,5 @@
angular.module('portainer.services') angular.module('portainer.services')
.factory('LineChart', ['Settings', function LineChartFactory(Settings) { .factory('LineChart', [function LineChartFactory() {
'use strict'; 'use strict';
return { return {
build: function (id, data, getkey) { build: function (id, data, getkey) {

View File

@ -1,10 +1,10 @@
angular.module('portainer.services') angular.module('portainer.services')
.factory('Pagination', ['LocalStorage', 'Settings', function PaginationFactory(LocalStorage, Settings) { .factory('Pagination', ['LocalStorage', 'PAGINATION_MAX_ITEMS', function PaginationFactory(LocalStorage, PAGINATION_MAX_ITEMS) {
'use strict'; 'use strict';
return { return {
getPaginationCount: function(key) { getPaginationCount: function(key) {
var storedCount = LocalStorage.getPaginationCount(key); var storedCount = LocalStorage.getPaginationCount(key);
var paginationCount = Settings.pagination_count; var paginationCount = PAGINATION_MAX_ITEMS;
if (storedCount !== null) { if (storedCount !== null) {
paginationCount = storedCount; paginationCount = storedCount;
} }

View File

@ -1,17 +0,0 @@
angular.module('portainer.services')
.factory('Settings', ['DOCKER_ENDPOINT', 'DOCKER_PORT', 'UI_VERSION', 'PAGINATION_MAX_ITEMS', function SettingsFactory(DOCKER_ENDPOINT, DOCKER_PORT, UI_VERSION, PAGINATION_MAX_ITEMS) {
'use strict';
var url = DOCKER_ENDPOINT;
if (DOCKER_PORT) {
url = url + DOCKER_PORT + '\\' + DOCKER_PORT;
}
var firstLoad = (localStorage.getItem('firstLoad') || 'true') === 'true';
return {
displayAll: true,
endpoint: DOCKER_ENDPOINT,
uiVersion: UI_VERSION,
url: url,
firstLoad: firstLoad,
pagination_count: PAGINATION_MAX_ITEMS
};
}]);

View File

@ -1,67 +1,95 @@
angular.module('portainer.services') angular.module('portainer.services')
.factory('StateManager', ['$q', 'Config', 'Info', 'InfoHelper', 'Version', 'LocalStorage', function StateManagerFactory($q, Config, Info, InfoHelper, Version, LocalStorage) { .factory('StateManager', ['$q', 'Info', 'InfoHelper', 'Version', 'LocalStorage', 'SettingsService', 'StatusService', function StateManagerFactory($q, Info, InfoHelper, Version, LocalStorage, SettingsService, StatusService) {
'use strict'; 'use strict';
var manager = {};
var state = { var state = {
loading: true, loading: true,
application: {}, application: {},
endpoint: {} endpoint: {},
UI: {}
}; };
return { manager.getState = function() {
initialize: function() { return state;
var endpointState = LocalStorage.getEndpointState(); };
if (endpointState) {
state.endpoint = endpointState;
}
var deferred = $q.defer(); manager.clean = function () {
var applicationState = LocalStorage.getApplicationState(); state.endpoint = {};
if (applicationState) { };
state.application = applicationState;
state.loading = false; manager.updateLogo = function(logoURL) {
deferred.resolve(state); state.application.logo = logoURL;
} else { LocalStorage.storeApplicationState(state.application);
Config.$promise.then(function success(data) { };
state.application.authentication = data.authentication;
state.application.analytics = data.analytics; manager.updateExternalContributions = function(displayExternalContributors) {
state.application.endpointManagement = data.endpointManagement; state.application.displayExternalContributors = displayExternalContributors;
state.application.logo = data.logo; LocalStorage.storeApplicationState(state.application);
LocalStorage.storeApplicationState(state.application); };
state.loading = false;
deferred.resolve(state); manager.initialize = function () {
}, function error(err) { var deferred = $q.defer();
state.loading = false;
deferred.reject({msg: 'Unable to retrieve server configuration', err: err}); var endpointState = LocalStorage.getEndpointState();
}); if (endpointState) {
} state.endpoint = endpointState;
return deferred.promise;
},
clean: function() {
state.endpoint = {};
},
updateEndpointState: function(loading) {
var deferred = $q.defer();
if (loading) {
state.loading = true;
}
$q.all([Info.get({}).$promise, Version.get({}).$promise])
.then(function success(data) {
var endpointMode = InfoHelper.determineEndpointMode(data[0]);
var endpointAPIVersion = parseFloat(data[1].ApiVersion);
state.endpoint.mode = endpointMode;
state.endpoint.apiVersion = endpointAPIVersion;
LocalStorage.storeEndpointState(state.endpoint);
state.loading = false;
deferred.resolve();
}, function error(err) {
state.loading = false;
deferred.reject({msg: 'Unable to connect to the Docker endpoint', err: err});
});
return deferred.promise;
},
getState: function() {
return state;
} }
var applicationState = LocalStorage.getApplicationState();
if (applicationState) {
state.application = applicationState;
state.loading = false;
deferred.resolve(state);
} else {
$q.all({
settings: SettingsService.settings(),
status: StatusService.status()
})
.then(function success(data) {
var status = data.status;
var settings = data.settings;
state.application.authentication = status.Authentication;
state.application.analytics = status.Analytics;
state.application.endpointManagement = status.EndpointManagement;
state.application.version = status.Version;
state.application.logo = settings.LogoURL;
state.application.displayExternalContributors = settings.DisplayExternalContributors;
LocalStorage.storeApplicationState(state.application);
deferred.resolve(state);
})
.catch(function error(err) {
deferred.reject({msg: 'Unable to retrieve server settings and status', err: err});
})
.finally(function final() {
state.loading = false;
});
}
return deferred.promise;
}; };
manager.updateEndpointState = function(loading) {
var deferred = $q.defer();
if (loading) {
state.loading = true;
}
$q.all([Info.get({}).$promise, Version.get({}).$promise])
.then(function success(data) {
var endpointMode = InfoHelper.determineEndpointMode(data[0]);
var endpointAPIVersion = parseFloat(data[1].ApiVersion);
state.endpoint.mode = endpointMode;
state.endpoint.apiVersion = endpointAPIVersion;
LocalStorage.storeEndpointState(state.endpoint);
state.loading = false;
deferred.resolve();
}, function error(err) {
state.loading = false;
deferred.reject({msg: 'Unable to connect to the Docker endpoint', err: err});
});
return deferred.promise;
};
return manager;
}]); }]);