mirror of https://github.com/portainer/portainer
feat(settings): add settings management (#906)
parent
5e74a3993b
commit
c7e306841a
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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:""`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
25
app/app.js
25
app/app.js
|
@ -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');
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
function SettingsViewModel(data) {
|
||||||
|
this.TemplatesURL = data.TemplatesURL;
|
||||||
|
this.LogoURL = data.LogoURL;
|
||||||
|
this.BlackListedLabels = data.BlackListedLabels;
|
||||||
|
this.DisplayExternalContributors = data.DisplayExternalContributors;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
function StatusViewModel(data) {
|
||||||
|
this.Authentication = data.Authentication;
|
||||||
|
this.EndpointManagement = data.EndpointManagement;
|
||||||
|
this.Analytics = data.Analytics;
|
||||||
|
this.Version = data.Version;
|
||||||
|
}
|
|
@ -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' }
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -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' }
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -1,4 +0,0 @@
|
||||||
angular.module('portainer.rest')
|
|
||||||
.factory('Config', ['$resource', 'CONFIG_ENDPOINT', function ConfigFactory($resource, CONFIG_ENDPOINT) {
|
|
||||||
return $resource(CONFIG_ENDPOINT).get();
|
|
||||||
}]);
|
|
|
@ -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'}}
|
|
||||||
});
|
|
||||||
}]);
|
|
|
@ -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
|
||||||
},
|
},
|
|
@ -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'}}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -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,
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
|
@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
|
@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
|
@ -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
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -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
|
||||||
},
|
},
|
|
@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
|
@ -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'} },
|
|
@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
|
@ -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'}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
|
@ -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
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -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
|
||||||
},
|
},
|
|
@ -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
|
|
||||||
});
|
|
||||||
}]);
|
|
|
@ -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'}
|
|
||||||
});
|
|
||||||
}]);
|
|
|
@ -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
|
|
||||||
});
|
|
||||||
}]);
|
|
|
@ -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;
|
||||||
|
}]);
|
|
@ -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;
|
||||||
|
}]);
|
|
@ -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 });
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
}]);
|
|
|
@ -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;
|
||||||
}]);
|
}]);
|
||||||
|
|
Loading…
Reference in New Issue