mirror of https://github.com/portainer/portainer
commit
14a4587f5e
|
@ -5,8 +5,9 @@
|
|||
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
|
||||
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
|
||||
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/latest/?badge=stable)
|
||||
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
||||
[![Codefresh build status]( https://g.codefresh.io/api/badges/build?repoOwner=portainer&repoName=portainer&branch=develop&pipelineName=portainer-ci&accountName=deviantony&type=cf-1)]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci)
|
||||
[![Slack](http://portainer.io/slack/badge.svg)](http://portainer.io/slack/)
|
||||
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ type Store struct {
|
|||
EndpointService *EndpointService
|
||||
ResourceControlService *ResourceControlService
|
||||
VersionService *VersionService
|
||||
SettingsService *SettingsService
|
||||
|
||||
db *bolt.DB
|
||||
checkForDataMigration bool
|
||||
|
@ -35,6 +36,7 @@ const (
|
|||
teamMembershipBucketName = "team_membership"
|
||||
endpointBucketName = "endpoints"
|
||||
resourceControlBucketName = "resource_control"
|
||||
settingsBucketName = "settings"
|
||||
)
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
|
@ -47,6 +49,7 @@ func NewStore(storePath string) (*Store, error) {
|
|||
EndpointService: &EndpointService{},
|
||||
ResourceControlService: &ResourceControlService{},
|
||||
VersionService: &VersionService{},
|
||||
SettingsService: &SettingsService{},
|
||||
}
|
||||
store.UserService.store = store
|
||||
store.TeamService.store = store
|
||||
|
@ -54,6 +57,7 @@ func NewStore(storePath string) (*Store, error) {
|
|||
store.EndpointService.store = store
|
||||
store.ResourceControlService.store = store
|
||||
store.VersionService.store = store
|
||||
store.SettingsService.store = store
|
||||
|
||||
_, err := os.Stat(storePath + "/" + databaseFileName)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
|
@ -100,6 +104,10 @@ func (store *Store) Open() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(settingsBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -57,6 +57,16 @@ func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error
|
|||
return json.Unmarshal(data, rc)
|
||||
}
|
||||
|
||||
// MarshalSettings encodes a settings object to binary format.
|
||||
func MarshalSettings(settings *portainer.Settings) ([]byte, error) {
|
||||
return json.Marshal(settings)
|
||||
}
|
||||
|
||||
// UnmarshalSettings decodes a settings object from a binary data.
|
||||
func UnmarshalSettings(data []byte, settings *portainer.Settings) error {
|
||||
return json.Unmarshal(data, settings)
|
||||
}
|
||||
|
||||
// Itob returns an 8-byte big endian representation of v.
|
||||
// This function is typically used for encoding integer IDs to byte slices
|
||||
// so that they can be used as BoltDB keys.
|
||||
|
|
|
@ -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
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
@ -29,14 +30,11 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
|
||||
flags := &portainer.CLIFlags{
|
||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
|
||||
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
|
||||
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||
|
@ -47,6 +45,10 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
// Deprecated flags
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
|
@ -79,6 +81,8 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
|||
return errNoAuthExcludeAdminPassword
|
||||
}
|
||||
|
||||
displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -122,3 +126,15 @@ func validateSyncInterval(syncInterval string) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func displayDeprecationWarnings(templates, logo string, labels []portainer.Pair) {
|
||||
if templates != "" {
|
||||
log.Println("Warning: the --templates / -t flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
if logo != "" {
|
||||
log.Println("Warning: the --logo flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
if labels != nil {
|
||||
log.Println("Warning: the --hide-label / -l flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ const (
|
|||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
|
|
|
@ -4,7 +4,6 @@ const (
|
|||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
|
|
|
@ -82,16 +82,43 @@ func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpo
|
|||
return authorizeEndpointMgmt
|
||||
}
|
||||
|
||||
func initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Settings {
|
||||
return &portainer.Settings{
|
||||
HiddenLabels: *flags.Labels,
|
||||
Logo: *flags.Logo,
|
||||
func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
|
||||
return &portainer.Status{
|
||||
Analytics: !*flags.NoAnalytics,
|
||||
Authentication: !*flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
Version: portainer.APIVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
|
||||
_, err := settingsService.Settings()
|
||||
if err == portainer.ErrSettingsNotFound {
|
||||
settings := &portainer.Settings{
|
||||
LogoURL: *flags.Logo,
|
||||
DisplayExternalContributors: true,
|
||||
}
|
||||
|
||||
if *flags.Templates != "" {
|
||||
settings.TemplatesURL = *flags.Templates
|
||||
} else {
|
||||
settings.TemplatesURL = portainer.DefaultTemplatesURL
|
||||
}
|
||||
|
||||
if *flags.Labels != nil {
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
} else {
|
||||
settings.BlackListedLabels = make([]portainer.Pair, 0)
|
||||
}
|
||||
|
||||
return settingsService.StoreSettings(settings)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
|
||||
endpoints, err := endpointService.Endpoints()
|
||||
if err != nil {
|
||||
|
@ -114,7 +141,12 @@ func main() {
|
|||
|
||||
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
|
||||
|
||||
settings := initSettings(authorizeEndpointMgmt, flags)
|
||||
err := initSettings(store.SettingsService, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
|
||||
|
||||
if *flags.Endpoint != "" {
|
||||
var endpoints []portainer.Endpoint
|
||||
|
@ -156,10 +188,9 @@ func main() {
|
|||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
Settings: settings,
|
||||
TemplatesURL: *flags.Templates,
|
||||
AuthDisabled: *flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
UserService: store.UserService,
|
||||
|
@ -167,6 +198,7 @@ func main() {
|
|||
TeamMembershipService: store.TeamMembershipService,
|
||||
EndpointService: store.EndpointService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
|
@ -176,7 +208,7 @@ func main() {
|
|||
}
|
||||
|
||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||
err := server.Start()
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ package portainer
|
|||
const (
|
||||
ErrUnauthorized = Error("Unauthorized")
|
||||
ErrResourceAccessDenied = Error("Access denied to resource")
|
||||
ErrResourceNotFound = Error("Unable to find resource")
|
||||
ErrUnsupportedDockerAPI = Error("Unsupported Docker API response")
|
||||
ErrMissingSecurityContext = Error("Unable to find security details in request context")
|
||||
)
|
||||
|
@ -46,6 +47,11 @@ const (
|
|||
ErrDBVersionNotFound = Error("DB version not found")
|
||||
)
|
||||
|
||||
// Settings errors.
|
||||
const (
|
||||
ErrSettingsNotFound = Error("Settings not found")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
const (
|
||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||
|
|
|
@ -1,19 +1,36 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileHandler represents an HTTP API handler for managing static files.
|
||||
type FileHandler struct {
|
||||
http.Handler
|
||||
Logger *log.Logger
|
||||
allowedDirectories map[string]bool
|
||||
}
|
||||
|
||||
// NewFileHandler returns a new instance of FileHandler.
|
||||
func NewFileHandler(assetPath string) *FileHandler {
|
||||
h := &FileHandler{
|
||||
Handler: http.FileServer(http.Dir(assetPath)),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
allowedDirectories: map[string]bool{
|
||||
"/": true,
|
||||
"/css": true,
|
||||
"/js": true,
|
||||
"/images": true,
|
||||
"/fonts": true,
|
||||
},
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
@ -27,11 +44,18 @@ func isHTML(acceptContent []string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (fileHandler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (handler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
requestDirectory := path.Dir(r.URL.Path)
|
||||
if !handler.allowedDirectories[requestDirectory] {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceNotFound, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !isHTML(r.Header["Accept"]) {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
fileHandler.Handler.ServeHTTP(w, r)
|
||||
|
||||
handler.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ type Handler struct {
|
|||
TeamMembershipHandler *TeamMembershipHandler
|
||||
EndpointHandler *EndpointHandler
|
||||
ResourceHandler *ResourceHandler
|
||||
StatusHandler *StatusHandler
|
||||
SettingsHandler *SettingsHandler
|
||||
TemplatesHandler *TemplatesHandler
|
||||
DockerHandler *DockerHandler
|
||||
|
@ -53,6 +54,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
http.StripPrefix("/api", h.ResourceHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
|
||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/status") {
|
||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/upload") {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
@ -12,32 +15,69 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||
// SettingsHandler represents an HTTP API handler for managing Settings.
|
||||
type SettingsHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
settings *portainer.Settings
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewSettingsHandler returns a new instance of SettingsHandler.
|
||||
func NewSettingsHandler(bouncer *security.RequestBouncer, settings *portainer.Settings) *SettingsHandler {
|
||||
// NewSettingsHandler returns a new instance of OldSettingsHandler.
|
||||
func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
|
||||
h := &SettingsHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
settings: settings,
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetSettings)))
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet)
|
||||
h.Handle("/settings",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettings))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetSettings handles GET requests on /settings
|
||||
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
httperror.WriteMethodNotAllowedResponse(w, []string{http.MethodGet})
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, handler.settings, handler.Logger)
|
||||
encodeJSON(w, settings, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// handlePutSettings handles PUT requests on /settings
|
||||
func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var req putSettingsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
settings := &portainer.Settings{
|
||||
TemplatesURL: req.TemplatesURL,
|
||||
LogoURL: req.LogoURL,
|
||||
BlackListedLabels: req.BlackListedLabels,
|
||||
DisplayExternalContributors: req.DisplayExternalContributors,
|
||||
}
|
||||
|
||||
err = handler.SettingsService.StoreSettings(settings)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
type putSettingsRequest struct {
|
||||
TemplatesURL string `valid:"required"`
|
||||
LogoURL string `valid:""`
|
||||
BlackListedLabels []portainer.Pair `valid:""`
|
||||
DisplayExternalContributors bool `valid:""`
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
@ -15,7 +16,7 @@ import (
|
|||
type TemplatesHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
containerTemplatesURL string
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -23,11 +24,10 @@ const (
|
|||
)
|
||||
|
||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||
func NewTemplatesHandler(bouncer *security.RequestBouncer, containerTemplatesURL string) *TemplatesHandler {
|
||||
func NewTemplatesHandler(bouncer *security.RequestBouncer) *TemplatesHandler {
|
||||
h := &TemplatesHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
containerTemplatesURL: containerTemplatesURL,
|
||||
}
|
||||
h.Handle("/templates",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates)))
|
||||
|
@ -49,7 +49,12 @@ func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *ht
|
|||
|
||||
var templatesURL string
|
||||
if key == "containers" {
|
||||
templatesURL = handler.containerTemplatesURL
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
templatesURL = settings.TemplatesURL
|
||||
} else if key == "linuxserver.io" {
|
||||
templatesURL = containerTemplatesURLLinuxServerIo
|
||||
} else {
|
||||
|
|
|
@ -15,7 +15,7 @@ const (
|
|||
|
||||
// containerListOperation extracts the response as a JSON object, loop through the containers array
|
||||
// decorate and/or filter the containers based on resource controls before rewriting the response
|
||||
func containerListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func containerListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// ContainerList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
|
@ -24,22 +24,30 @@ func containerListOperation(request *http.Request, response *http.Response, oper
|
|||
return err
|
||||
}
|
||||
|
||||
if operationContext.isAdmin {
|
||||
responseArray, err = decorateContainerList(responseArray, operationContext.resourceControls)
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterContainerList(responseArray, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
|
||||
responseArray, err = filterContainerList(responseArray, executor.operationContext.resourceControls,
|
||||
executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.labelBlackList != nil {
|
||||
responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// containerInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the container based on resource control (check are done based on the containerID and optional Swarm service ID)
|
||||
// and either rewrite an access denied response or a decorated container.
|
||||
func containerInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func containerInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// ContainerInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
@ -52,9 +60,10 @@ func containerInspectOperation(request *http.Request, response *http.Response, o
|
|||
}
|
||||
containerID := responseObject[containerIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(containerID, operationContext.resourceControls)
|
||||
resourceControl := getResourceControlByResourceID(containerID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
|
||||
executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
|
@ -64,9 +73,10 @@ func containerInspectOperation(request *http.Request, response *http.Response, o
|
|||
containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject)
|
||||
if containerLabels != nil && containerLabels[containerLabelForServiceIdentifier] != nil {
|
||||
serviceID := containerLabels[containerLabelForServiceIdentifier].(string)
|
||||
resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls)
|
||||
resourceControl := getResourceControlByResourceID(serviceID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
|
||||
executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
type proxyFactory struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||
|
@ -37,6 +38,7 @@ func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
|
|||
transport := &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
dockerTransport: newSocketTransport(path),
|
||||
}
|
||||
proxy.Transport = transport
|
||||
|
@ -48,6 +50,7 @@ func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReversePro
|
|||
transport := &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
dockerTransport: newHTTPTransport(),
|
||||
}
|
||||
proxy.Transport = transport
|
||||
|
|
|
@ -65,6 +65,27 @@ func filterContainerList(containerData []interface{}, resourceControls []portain
|
|||
return filteredContainerData, nil
|
||||
}
|
||||
|
||||
// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains
|
||||
// any labels in the labels black list.
|
||||
func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) {
|
||||
filteredContainerData := make([]interface{}, 0)
|
||||
|
||||
for _, container := range containerData {
|
||||
containerObject := container.(map[string]interface{})
|
||||
|
||||
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
|
||||
if containerLabels != nil {
|
||||
if !containerHasBlackListedLabel(containerLabels, labelBlackList) {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
} else {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredContainerData, nil
|
||||
}
|
||||
|
||||
// filterServiceList loops through all services, filters services without any resource control (public resources) or with
|
||||
// any resource control giving access to the user (these services will be decorated).
|
||||
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
|
|
|
@ -15,12 +15,13 @@ type Manager struct {
|
|||
}
|
||||
|
||||
// NewManager initializes a new proxy Service
|
||||
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService) *Manager {
|
||||
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager {
|
||||
return &Manager{
|
||||
proxies: cmap.New(),
|
||||
proxyFactory: &proxyFactory{
|
||||
ResourceControlService: resourceControlService,
|
||||
TeamMembershipService: teamMembershipService,
|
||||
SettingsService: settingsService,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ const (
|
|||
|
||||
// serviceListOperation extracts the response as a JSON array, loop through the service array
|
||||
// decorate and/or filter the services based on resource controls before rewriting the response
|
||||
func serviceListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func serviceListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// ServiceList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
|
@ -23,10 +23,10 @@ func serviceListOperation(request *http.Request, response *http.Response, operat
|
|||
return err
|
||||
}
|
||||
|
||||
if operationContext.isAdmin {
|
||||
responseArray, err = decorateServiceList(responseArray, operationContext.resourceControls)
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterServiceList(responseArray, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
|
||||
responseArray, err = filterServiceList(responseArray, executor.operationContext.resourceControls, executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -38,7 +38,7 @@ func serviceListOperation(request *http.Request, response *http.Response, operat
|
|||
// serviceInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the service based on resource control and either rewrite an access denied response
|
||||
// or a decorated service.
|
||||
func serviceInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func serviceInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// ServiceInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
@ -51,9 +51,9 @@ func serviceInspectOperation(request *http.Request, response *http.Response, ope
|
|||
}
|
||||
serviceID := responseObject[serviceIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(serviceID, operationContext.resourceControls)
|
||||
resourceControl := getResourceControlByResourceID(serviceID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
|
|
|
@ -15,6 +15,7 @@ type (
|
|||
dockerTransport *http.Transport
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
restrictedOperationContext struct {
|
||||
isAdmin bool
|
||||
|
@ -22,7 +23,11 @@ type (
|
|||
userTeamIDs []portainer.TeamID
|
||||
resourceControls []portainer.ResourceControl
|
||||
}
|
||||
restrictedOperationRequest func(*http.Request, *http.Response, *restrictedOperationContext) error
|
||||
operationExecutor struct {
|
||||
operationContext *restrictedOperationContext
|
||||
labelBlackList []portainer.Pair
|
||||
}
|
||||
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
|
||||
)
|
||||
|
||||
func newSocketTransport(socketPath string) *http.Transport {
|
||||
|
@ -60,7 +65,6 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
|
|||
}
|
||||
|
||||
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
||||
// return p.executeDockerRequest(request)
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/containers/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
@ -69,7 +73,7 @@ func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Res
|
|||
return p.administratorOperation(request)
|
||||
|
||||
case "/containers/json":
|
||||
return p.rewriteOperation(request, containerListOperation)
|
||||
return p.rewriteOperationWithLabelFiltering(request, containerListOperation)
|
||||
|
||||
default:
|
||||
// This section assumes /containers/**
|
||||
|
@ -96,9 +100,6 @@ func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Respo
|
|||
case "/services/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/volumes/prune":
|
||||
return p.administratorOperation(request)
|
||||
|
||||
case "/services":
|
||||
return p.rewriteOperation(request, serviceListOperation)
|
||||
|
||||
|
@ -177,9 +178,69 @@ func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID s
|
|||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
// rewriteOperation will create a new operation context with data that will be used
|
||||
// to decorate the original request's response as well as retrieve all the black listed labels
|
||||
// to filter the resources.
|
||||
func (p *proxyTransport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||
operationContext, err := p.createOperationContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings, err := p.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executor := &operationExecutor{
|
||||
operationContext: operationContext,
|
||||
labelBlackList: settings.BlackListedLabels,
|
||||
}
|
||||
|
||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
||||
}
|
||||
|
||||
// rewriteOperation will create a new operation context with data that will be used
|
||||
// to decorate the original request's response.
|
||||
func (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||
operationContext, err := p.createOperationContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executor := &operationExecutor{
|
||||
operationContext: operationContext,
|
||||
}
|
||||
|
||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
||||
response, err := p.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = operation(request, response, executor)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// administratorOperation ensures that the user has administrator privileges
|
||||
// before executing the original request.
|
||||
func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse()
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) {
|
||||
var err error
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
|
@ -212,26 +273,5 @@ func (p *proxyTransport) rewriteOperation(request *http.Request, operation restr
|
|||
operationContext.userTeamIDs = userTeamIDs
|
||||
}
|
||||
|
||||
response, err := p.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = operation(request, response, operationContext)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// administratorOperation ensures that the user has administrator privileges
|
||||
// before executing the original request.
|
||||
func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse()
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
return operationContext, nil
|
||||
}
|
||||
|
|
|
@ -15,3 +15,18 @@ func getResourceControlByResourceID(resourceID string, resourceControls []portai
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool {
|
||||
for key, value := range containerLabels {
|
||||
labelName := key
|
||||
labelValue := value.(string)
|
||||
|
||||
for _, blackListedLabel := range labelBlackList {
|
||||
if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ const (
|
|||
|
||||
// volumeListOperation extracts the response as a JSON object, loop through the volume array
|
||||
// decorate and/or filter the volumes based on resource controls before rewriting the response
|
||||
func volumeListOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func volumeListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// VolumeList response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
|
@ -28,10 +28,10 @@ func volumeListOperation(request *http.Request, response *http.Response, operati
|
|||
if responseObject["Volumes"] != nil {
|
||||
volumeData := responseObject["Volumes"].([]interface{})
|
||||
|
||||
if operationContext.isAdmin {
|
||||
volumeData, err = decorateVolumeList(volumeData, operationContext.resourceControls)
|
||||
if executor.operationContext.isAdmin {
|
||||
volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
volumeData, err = filterVolumeList(volumeData, operationContext.resourceControls, operationContext.userID, operationContext.userTeamIDs)
|
||||
volumeData, err = filterVolumeList(volumeData, executor.operationContext.resourceControls, executor.operationContext.userID, executor.operationContext.userTeamIDs)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -47,7 +47,7 @@ func volumeListOperation(request *http.Request, response *http.Response, operati
|
|||
// volumeInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the volume based on resource control and either rewrite an access denied response
|
||||
// or a decorated volume.
|
||||
func volumeInspectOperation(request *http.Request, response *http.Response, operationContext *restrictedOperationContext) error {
|
||||
func volumeInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// VolumeInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
|
@ -60,9 +60,9 @@ func volumeInspectOperation(request *http.Request, response *http.Response, oper
|
|||
}
|
||||
volumeID := responseObject[volumeIdentifier].(string)
|
||||
|
||||
resourceControl := getResourceControlByResourceID(volumeID, operationContext.resourceControls)
|
||||
resourceControl := getResourceControlByResourceID(volumeID, executor.operationContext.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if operationContext.isAdmin || canUserAccessResource(operationContext.userID, operationContext.userTeamIDs, resourceControl) {
|
||||
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
} else {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
|
|
|
@ -15,16 +15,16 @@ type Server struct {
|
|||
AssetsPath string
|
||||
AuthDisabled bool
|
||||
EndpointManagement bool
|
||||
Status *portainer.Status
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
EndpointService portainer.EndpointService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
SettingsService portainer.SettingsService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
SSLCert string
|
||||
|
@ -34,7 +34,7 @@ type Server struct {
|
|||
// Start starts the HTTP server
|
||||
func (server *Server) Start() error {
|
||||
requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled)
|
||||
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService)
|
||||
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService)
|
||||
|
||||
var authHandler = handler.NewAuthHandler(requestBouncer, server.AuthDisabled)
|
||||
authHandler.UserService = server.UserService
|
||||
|
@ -51,8 +51,11 @@ func (server *Server) Start() error {
|
|||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var teamMembershipHandler = handler.NewTeamMembershipHandler(requestBouncer)
|
||||
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var settingsHandler = handler.NewSettingsHandler(requestBouncer, server.Settings)
|
||||
var templatesHandler = handler.NewTemplatesHandler(requestBouncer, server.TemplatesURL)
|
||||
var statusHandler = handler.NewStatusHandler(requestBouncer, server.Status)
|
||||
var settingsHandler = handler.NewSettingsHandler(requestBouncer)
|
||||
settingsHandler.SettingsService = server.SettingsService
|
||||
var templatesHandler = handler.NewTemplatesHandler(requestBouncer)
|
||||
templatesHandler.SettingsService = server.SettingsService
|
||||
var dockerHandler = handler.NewDockerHandler(requestBouncer)
|
||||
dockerHandler.EndpointService = server.EndpointService
|
||||
dockerHandler.TeamMembershipService = server.TeamMembershipService
|
||||
|
@ -77,6 +80,7 @@ func (server *Server) Start() error {
|
|||
EndpointHandler: endpointHandler,
|
||||
ResourceHandler: resourceHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
StatusHandler: statusHandler,
|
||||
TemplatesHandler: templatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
|
|
|
@ -17,9 +17,6 @@ type (
|
|||
ExternalEndpoints *string
|
||||
SyncInterval *string
|
||||
Endpoint *string
|
||||
Labels *[]Pair
|
||||
Logo *string
|
||||
Templates *string
|
||||
NoAuth *bool
|
||||
NoAnalytics *bool
|
||||
TLSVerify *bool
|
||||
|
@ -30,15 +27,26 @@ type (
|
|||
SSLCert *string
|
||||
SSLKey *string
|
||||
AdminPassword *string
|
||||
// Deprecated fields
|
||||
Logo *string
|
||||
Templates *string
|
||||
Labels *[]Pair
|
||||
}
|
||||
|
||||
// Settings represents Portainer settings.
|
||||
// Status represents the application status.
|
||||
Status struct {
|
||||
Authentication bool `json:"Authentication"`
|
||||
EndpointManagement bool `json:"EndpointManagement"`
|
||||
Analytics bool `json:"Analytics"`
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
|
||||
// Settings represents the application settings.
|
||||
Settings struct {
|
||||
HiddenLabels []Pair `json:"hiddenLabels"`
|
||||
Logo string `json:"logo"`
|
||||
Authentication bool `json:"authentication"`
|
||||
Analytics bool `json:"analytics"`
|
||||
EndpointManagement bool `json:"endpointManagement"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
}
|
||||
|
||||
// User represents a user account.
|
||||
|
@ -209,6 +217,12 @@ type (
|
|||
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
|
||||
}
|
||||
|
||||
// SettingsService represents a service for managing application settings.
|
||||
SettingsService interface {
|
||||
Settings() (*Settings, error)
|
||||
StoreSettings(settings *Settings) error
|
||||
}
|
||||
|
||||
// VersionService represents a service for managing version data.
|
||||
VersionService interface {
|
||||
DBVersion() (int, error)
|
||||
|
@ -252,9 +266,11 @@ type (
|
|||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API.
|
||||
APIVersion = "1.13.1"
|
||||
APIVersion = "1.13.2"
|
||||
// DBVersion is the version number of the Portainer database.
|
||||
DBVersion = 2
|
||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
67
app/app.js
67
app/app.js
|
@ -28,6 +28,7 @@ angular.module('portainer', [
|
|||
'containers',
|
||||
'createContainer',
|
||||
'createNetwork',
|
||||
'createSecret',
|
||||
'createService',
|
||||
'createVolume',
|
||||
'docker',
|
||||
|
@ -42,6 +43,8 @@ angular.module('portainer', [
|
|||
'network',
|
||||
'networks',
|
||||
'node',
|
||||
'secrets',
|
||||
'secret',
|
||||
'service',
|
||||
'services',
|
||||
'settings',
|
||||
|
@ -54,6 +57,7 @@ angular.module('portainer', [
|
|||
'templates',
|
||||
'user',
|
||||
'users',
|
||||
'userSettings',
|
||||
'volume',
|
||||
'volumes'])
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider) {
|
||||
|
@ -249,6 +253,19 @@ angular.module('portainer', [
|
|||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.secret', {
|
||||
url: '/secret',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/createSecret/createsecret.html',
|
||||
controller: 'CreateSecretController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.service', {
|
||||
url: '/service',
|
||||
views: {
|
||||
|
@ -414,6 +431,32 @@ angular.module('portainer', [
|
|||
}
|
||||
}
|
||||
})
|
||||
.state('secrets', {
|
||||
url: '^/secrets/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/secrets/secrets.html',
|
||||
controller: 'SecretsController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('secret', {
|
||||
url: '^/secret/:id/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/secret/secret.html',
|
||||
controller: 'SecretController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('services', {
|
||||
url: '/services/',
|
||||
views: {
|
||||
|
@ -552,6 +595,19 @@ angular.module('portainer', [
|
|||
}
|
||||
}
|
||||
})
|
||||
.state('userSettings', {
|
||||
url: '/userSettings/',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/components/userSettings/userSettings.html',
|
||||
controller: 'UserSettingsController'
|
||||
},
|
||||
'sidebar@': {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('teams', {
|
||||
url: '/teams/',
|
||||
views: {
|
||||
|
@ -620,9 +676,11 @@ angular.module('portainer', [
|
|||
}])
|
||||
// This is your docker url that the api will use to make requests
|
||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||
// .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('DOCKER_ENDPOINT', 'api/docker')
|
||||
.constant('CONFIG_ENDPOINT', 'api/settings')
|
||||
.constant('CONFIG_ENDPOINT', 'api/old_settings')
|
||||
.constant('SETTINGS_ENDPOINT', 'api/settings')
|
||||
.constant('STATUS_ENDPOINT', 'api/status')
|
||||
.constant('AUTH_ENDPOINT', 'api/auth')
|
||||
.constant('USERS_ENDPOINT', 'api/users')
|
||||
.constant('TEAMS_ENDPOINT', 'api/teams')
|
||||
|
@ -630,5 +688,6 @@ angular.module('portainer', [
|
|||
.constant('RESOURCE_CONTROL_ENDPOINT', 'api/resource_controls')
|
||||
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
|
||||
.constant('TEMPLATES_ENDPOINT', 'api/templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.13.1');
|
||||
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10);
|
||||
// .constant('UI_VERSION', 'v1.13.2');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('auth', [])
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) {
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Authentication, Users, EndpointService, StateManager, EndpointProvider, Notifications) {
|
||||
|
||||
$scope.authData = {
|
||||
username: 'admin',
|
||||
|
@ -13,6 +13,8 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
|
|||
error: false
|
||||
};
|
||||
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
||||
if (!$scope.applicationState.application.authentication) {
|
||||
EndpointService.endpoints()
|
||||
.then(function success(data) {
|
||||
|
@ -59,10 +61,6 @@ function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Au
|
|||
$state.go('dashboard');
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
});
|
||||
|
||||
$scope.createAdminUser = function() {
|
||||
var password = $sanitize($scope.initPasswordData.password);
|
||||
Users.initAdminUser({password: password}, function (d) {
|
||||
|
|
|
@ -165,13 +165,14 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Con
|
|||
};
|
||||
|
||||
$scope.renameContainer = function () {
|
||||
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
|
||||
if (container.message) {
|
||||
$scope.container.newContainerName = $scope.container.Name;
|
||||
Notifications.error('Unable to rename container', {}, container.message);
|
||||
var container = $scope.container;
|
||||
Container.rename({id: $stateParams.id, 'name': container.newContainerName}, function (d) {
|
||||
if (d.message) {
|
||||
container.newContainerName = container.Name;
|
||||
Notifications.error('Unable to rename container', {}, d.message);
|
||||
} else {
|
||||
$scope.container.Name = $scope.container.newContainerName;
|
||||
Notifications.success('Container successfully renamed', container.name);
|
||||
container.Name = container.newContainerName;
|
||||
Notifications.success('Container successfully renamed', container.Name);
|
||||
}
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to rename container');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('containerConsole', [])
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications',
|
||||
function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, EndpointProvider, Notifications) {
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Notifications',
|
||||
function ($scope, $stateParams, Container, Image, Exec, $timeout, EndpointProvider, Notifications) {
|
||||
$scope.state = {};
|
||||
$scope.state.loaded = false;
|
||||
$scope.state.connected = false;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
angular.module('containers', [])
|
||||
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'Info', 'Settings', 'Notifications', 'Config', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider',
|
||||
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, Info, Settings, Notifications, Config, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
|
||||
.controller('ContainersController', ['$q', '$scope', '$filter', 'Container', 'ContainerService', 'ContainerHelper', 'Info', 'Notifications', 'Pagination', 'EntityListService', 'ModalService', 'ResourceControlService', 'EndpointProvider',
|
||||
function ($q, $scope, $filter, Container, ContainerService, ContainerHelper, Info, Notifications, Pagination, EntityListService, ModalService, ResourceControlService, EndpointProvider) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('containers');
|
||||
$scope.state.displayAll = Settings.displayAll;
|
||||
$scope.state.displayAll = true;
|
||||
$scope.state.displayIP = false;
|
||||
$scope.sortType = 'State';
|
||||
$scope.sortReverse = false;
|
||||
|
@ -25,9 +25,6 @@ angular.module('containers', [])
|
|||
$scope.state.selectedItemCount = 0;
|
||||
Container.query(data, function (d) {
|
||||
var containers = d;
|
||||
if ($scope.containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, $scope.containersToHideLabels);
|
||||
}
|
||||
$scope.containers = containers.map(function (container) {
|
||||
var model = new ContainerViewModel(container);
|
||||
model.Status = $filter('containerstatus')(model.Status);
|
||||
|
@ -59,7 +56,7 @@ angular.module('containers', [])
|
|||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadContainersSpinner').hide();
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
update({all: $scope.state.displayAll ? 1 : 0});
|
||||
}
|
||||
};
|
||||
angular.forEach(items, function (c) {
|
||||
|
@ -134,8 +131,7 @@ angular.module('containers', [])
|
|||
};
|
||||
|
||||
$scope.toggleGetAll = function () {
|
||||
Settings.displayAll = $scope.state.displayAll;
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
update({all: $scope.state.displayAll ? 1 : 0});
|
||||
};
|
||||
|
||||
$scope.startAction = function () {
|
||||
|
@ -206,15 +202,16 @@ angular.module('containers', [])
|
|||
return swarm_hosts;
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
function initView(){
|
||||
if ($scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM') {
|
||||
Info.get({}, function (d) {
|
||||
$scope.swarm_hosts = retrieveSwarmHostsInfo(d);
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
update({all: $scope.state.displayAll ? 1 : 0});
|
||||
});
|
||||
} else {
|
||||
update({all: Settings.displayAll ? 1 : 0});
|
||||
update({all: $scope.state.displayAll ? 1 : 0});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
|
||||
// See app/components/templates/templatesController.js as a reference.
|
||||
angular.module('createContainer', [])
|
||||
.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Config', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator',
|
||||
function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) {
|
||||
.controller('CreateContainerController', ['$q', '$scope', '$state', '$stateParams', '$filter', 'Info', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'Network', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'ControllerDataPipeline', 'FormValidator',
|
||||
function ($q, $scope, $state, $stateParams, $filter, Info, Container, ContainerHelper, Image, ImageHelper, Volume, Network, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, ControllerDataPipeline, FormValidator) {
|
||||
|
||||
$scope.formValues = {
|
||||
alwaysPull: true,
|
||||
|
@ -233,9 +233,6 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
|
|||
}
|
||||
|
||||
function initView() {
|
||||
Config.$promise.then(function (c) {
|
||||
var containersToHideLabels = c.hiddenLabels;
|
||||
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
|
@ -266,14 +263,11 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
|
|||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
|
@ -305,10 +299,9 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
|
|||
};
|
||||
|
||||
function createContainer(config, accessControlData) {
|
||||
$q.when($scope.formValues.alwaysPull ? ImageService.pullImage($scope.config.Image, $scope.formValues.Registry) : null)
|
||||
.then(function success() {
|
||||
return ContainerService.createAndStartContainer(config);
|
||||
})
|
||||
$q.when(!$scope.formValues.alwaysPull || ImageService.pullImage($scope.config.Image, $scope.formValues.Registry))
|
||||
.finally(function final() {
|
||||
ContainerService.createAndStartContainer(config)
|
||||
.then(function success(data) {
|
||||
var containerIdentifier = data.Id;
|
||||
var userId = Authentication.getUserDetails().ID;
|
||||
|
@ -324,8 +317,8 @@ function ($q, $scope, $state, $stateParams, $filter, Config, Info, Container, Co
|
|||
.finally(function final() {
|
||||
$('#createContainerSpinner').hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
|
||||
}]);
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
angular.module('createSecret', [])
|
||||
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService',
|
||||
function ($scope, $state, Notifications, SecretService) {
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
Data: '',
|
||||
Labels: [],
|
||||
encodeSecret: true
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.formValues.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
function prepareLabelsConfig(config) {
|
||||
var labels = {};
|
||||
$scope.formValues.Labels.forEach(function (label) {
|
||||
if (label.name && label.value) {
|
||||
labels[label.name] = label.value;
|
||||
}
|
||||
});
|
||||
config.Labels = labels;
|
||||
}
|
||||
|
||||
function prepareSecretData(config) {
|
||||
if ($scope.formValues.encodeSecret) {
|
||||
config.Data = btoa(unescape(encodeURIComponent($scope.formValues.Data)));
|
||||
} else {
|
||||
config.Data = $scope.formValues.Data;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var config = {};
|
||||
config.Name = $scope.formValues.Name;
|
||||
prepareSecretData(config);
|
||||
prepareLabelsConfig(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
function createSecret(config) {
|
||||
$('#createSecretSpinner').show();
|
||||
SecretService.create(config)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Secret successfully created');
|
||||
$state.go('secrets', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create secret');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#createSecretSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.create = function () {
|
||||
var config = prepareConfiguration();
|
||||
createSecret(config);
|
||||
};
|
||||
}]);
|
|
@ -0,0 +1,85 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Create secret"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="secrets">Secrets</a> > Add secret
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="secret_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="formValues.Name" id="secret_name" placeholder="e.g. mySecret">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- secret-data -->
|
||||
<div class="form-group">
|
||||
<label for="secret_data" class="col-sm-1 control-label text-left">Secret</label>
|
||||
<div class="col-sm-11">
|
||||
<textarea class="form-control" rows="5" ng-model="formValues.Data"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !secret-data -->
|
||||
<!-- encode-secret -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="encode_secret" class="control-label text-left">
|
||||
Encode secret
|
||||
<portainer-tooltip position="bottom" message="Secrets need to be base64 encoded. Disable this if your secret is already base64 encoded."></portainer-tooltip>
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="encode_secret" ng-model="formValues.encodeSecret"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !encode-secret -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addLabel()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in formValues.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. com.example.foo">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. bar">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeLabel($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels-->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.Data" ng-click="create()">Create secret</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="secrets">Cancel</a>
|
||||
<i id="createSecretSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -1,8 +1,8 @@
|
|||
// @@OLD_SERVICE_CONTROLLER: this service should be rewritten to use services.
|
||||
// See app/components/templates/templatesController.js as a reference.
|
||||
angular.module('createService', [])
|
||||
.controller('CreateServiceController', ['$scope', '$state', 'Service', 'ServiceHelper', 'Volume', 'Network', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
|
||||
function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator) {
|
||||
.controller('CreateServiceController', ['$q', '$scope', '$state', 'Service', 'ServiceHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'ControllerDataPipeline', 'FormValidator',
|
||||
function ($q, $scope, $state, Service, ServiceHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, Authentication, ResourceControlService, Notifications, ControllerDataPipeline, FormValidator) {
|
||||
|
||||
$scope.formValues = {
|
||||
Name: '',
|
||||
|
@ -24,7 +24,8 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
|||
Parallelism: 1,
|
||||
PlacementConstraints: [],
|
||||
UpdateDelay: 0,
|
||||
FailureAction: 'pause'
|
||||
FailureAction: 'pause',
|
||||
Secrets: []
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
|
@ -55,6 +56,14 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
|||
$scope.formValues.Volumes.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addSecret = function() {
|
||||
$scope.formValues.Secrets.push({});
|
||||
};
|
||||
|
||||
$scope.removeSecret = function(index) {
|
||||
$scope.formValues.Secrets.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addEnvironmentVariable = function() {
|
||||
$scope.formValues.Env.push({ name: '', value: ''});
|
||||
};
|
||||
|
@ -62,18 +71,23 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
|||
$scope.removeEnvironmentVariable = function(index) {
|
||||
$scope.formValues.Env.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addPlacementConstraint = function() {
|
||||
$scope.formValues.PlacementConstraints.push({ key: '', operator: '==', value: '' });
|
||||
};
|
||||
|
||||
$scope.removePlacementConstraint = function(index) {
|
||||
$scope.formValues.PlacementConstraints.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addPlacementPreference = function() {
|
||||
$scope.formValues.PlacementPreferences.push({ key: '', operator: '==', value: '' });
|
||||
};
|
||||
|
||||
$scope.removePlacementPreference = function(index) {
|
||||
$scope.formValues.PlacementPreferences.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.formValues.Labels.push({ name: '', value: ''});
|
||||
};
|
||||
|
@ -203,6 +217,18 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
|||
config.TaskTemplate.Placement.Constraints = ServiceHelper.translateKeyValueToPlacementConstraints(input.PlacementConstraints);
|
||||
}
|
||||
|
||||
function prepareSecretConfig(config, input) {
|
||||
if (input.Secrets) {
|
||||
var secrets = [];
|
||||
angular.forEach(input.Secrets, function(secret) {
|
||||
if (secret.model) {
|
||||
secrets.push(SecretHelper.secretConfig(secret.model));
|
||||
}
|
||||
});
|
||||
config.TaskTemplate.ContainerSpec.Secrets = secrets;
|
||||
}
|
||||
}
|
||||
|
||||
function prepareConfiguration() {
|
||||
var input = $scope.formValues;
|
||||
var config = {
|
||||
|
@ -225,6 +251,7 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
|||
prepareVolumes(config, input);
|
||||
prepareNetworks(config, input);
|
||||
prepareUpdateConfig(config, input);
|
||||
prepareSecretConfig(config, input);
|
||||
preparePlacementConfig(config, input);
|
||||
return config;
|
||||
}
|
||||
|
@ -277,20 +304,22 @@ function ($scope, $state, Service, ServiceHelper, Volume, Network, ImageHelper,
|
|||
};
|
||||
|
||||
function initView() {
|
||||
Volume.query({}, function (d) {
|
||||
$scope.availableVolumes = d.Volumes;
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve volumes');
|
||||
});
|
||||
|
||||
Network.query({}, function (d) {
|
||||
$scope.availableNetworks = d.filter(function (network) {
|
||||
if (network.Scope === 'swarm') {
|
||||
return network;
|
||||
}
|
||||
});
|
||||
}, function (e) {
|
||||
Notifications.error('Failure', e, 'Unable to retrieve networks');
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all({
|
||||
volumes: VolumeService.volumes(),
|
||||
networks: NetworkService.retrieveSwarmNetworks(),
|
||||
secrets: SecretService.secrets()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.availableVolumes = data.volumes;
|
||||
$scope.availableNetworks = data.networks;
|
||||
$scope.availableSecrets = data.secrets;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to initialize view');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Create service"></rd-header-title>
|
||||
<rd-header-title title="Create service">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="services">Services</a> > Add service
|
||||
</rd-header-content>
|
||||
|
@ -140,6 +142,7 @@
|
|||
<li class="interactive"><a data-target="#network" data-toggle="tab">Network</a></li>
|
||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||
<li class="interactive"><a data-target="#update-config" data-toggle="tab">Update config</a></li>
|
||||
<li class="interactive"><a data-target="#secrets" data-toggle="tab" ng-if="applicationState.endpoint.apiVersion >= 1.25">Secrets</a></li>
|
||||
<li class="interactive"><a data-target="#placement" data-toggle="tab">Placement</a></li>
|
||||
</ul>
|
||||
<!-- tab-content -->
|
||||
|
@ -422,7 +425,9 @@
|
|||
</form>
|
||||
</div>
|
||||
<!-- !tab-update-config -->
|
||||
|
||||
<!-- tab-secrets -->
|
||||
<div class="tab-pane" id="secrets" ng-if="applicationState.endpoint.apiVersion >= 1.25" ng-include="'app/components/createService/includes/secret.html'"></div>
|
||||
<!-- !tab-secrets -->
|
||||
<!-- tab-placement -->
|
||||
<div class="tab-pane" id="placement" ng-include="'app/components/createService/includes/placement.html'"></div>
|
||||
<!-- !tab-placement -->
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Placement constraints</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint(service)">
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addPlacementConstraint()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> placement constraint
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
Secrets will be available under <code>/run/secrets/$SECRET_NAME</code> in containers.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Secrets</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addSecret()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add a secret
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="secret in formValues.Secrets" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-4 input-group-sm">
|
||||
<span class="input-group-addon">secret</span>
|
||||
<select class="form-control" ng-model="secret.model" ng-options="secret.Name for secret in availableSecrets">
|
||||
<option value="" selected="selected">Select a secret</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="removeSecret($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('dashboard', [])
|
||||
.controller('DashboardController', ['$scope', '$q', 'Config', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications',
|
||||
function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) {
|
||||
.controller('DashboardController', ['$scope', '$q', 'Container', 'ContainerHelper', 'Image', 'Network', 'Volume', 'Info', 'Notifications',
|
||||
function ($scope, $q, Container, ContainerHelper, Image, Network, Volume, Info, Notifications) {
|
||||
|
||||
$scope.containerData = {
|
||||
total: 0
|
||||
|
@ -15,14 +15,10 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
|||
total: 0
|
||||
};
|
||||
|
||||
function prepareContainerData(d, containersToHideLabels) {
|
||||
function prepareContainerData(d) {
|
||||
var running = 0;
|
||||
var stopped = 0;
|
||||
|
||||
var containers = d;
|
||||
if (containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(d, containersToHideLabels);
|
||||
}
|
||||
|
||||
for (var i = 0; i < containers.length; i++) {
|
||||
var item = containers[i];
|
||||
|
@ -65,7 +61,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
|||
$scope.infoData = info;
|
||||
}
|
||||
|
||||
function fetchDashboardData(containersToHideLabels) {
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all([
|
||||
Container.query({all: 1}).$promise,
|
||||
|
@ -74,7 +70,7 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
|||
Network.query({}).$promise,
|
||||
Info.get({}).$promise
|
||||
]).then(function (d) {
|
||||
prepareContainerData(d[0], containersToHideLabels);
|
||||
prepareContainerData(d[0]);
|
||||
prepareImageData(d[1]);
|
||||
prepareVolumeData(d[2]);
|
||||
prepareNetworkData(d[3]);
|
||||
|
@ -86,7 +82,5 @@ function ($scope, $q, Config, Container, ContainerHelper, Image, Network, Volume
|
|||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
fetchDashboardData(c.hiddenLabels);
|
||||
});
|
||||
initView();
|
||||
}]);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('images', [])
|
||||
.controller('ImagesController', ['$scope', '$state', 'Config', 'ImageService', 'Notifications', 'Pagination', 'ModalService',
|
||||
function ($scope, $state, Config, ImageService, Notifications, Pagination, ModalService) {
|
||||
.controller('ImagesController', ['$scope', '$state', 'ImageService', 'Notifications', 'Pagination', 'ModalService',
|
||||
function ($scope, $state, ImageService, Notifications, Pagination, ModalService) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('images');
|
||||
$scope.sortType = 'RepoTags';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('network', [])
|
||||
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Config', 'Network', 'Container', 'ContainerHelper', 'Notifications',
|
||||
function ($scope, $state, $stateParams, $filter, Config, Network, Container, ContainerHelper, Notifications) {
|
||||
.controller('NetworkController', ['$scope', '$state', '$stateParams', '$filter', 'Network', 'Container', 'ContainerHelper', 'Notifications',
|
||||
function ($scope, $state, $stateParams, $filter, Network, Container, ContainerHelper, Notifications) {
|
||||
|
||||
$scope.removeNetwork = function removeNetwork(networkId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
|
@ -36,21 +36,7 @@ function ($scope, $state, $stateParams, $filter, Config, Network, Container, Con
|
|||
});
|
||||
};
|
||||
|
||||
function getNetwork() {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.get({id: $stateParams.id}, function success(data) {
|
||||
$scope.network = data;
|
||||
getContainersInNetwork(data);
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve network info');
|
||||
});
|
||||
}
|
||||
|
||||
function filterContainersInNetwork(network, containers) {
|
||||
if ($scope.containersToHideLabels) {
|
||||
containers = ContainerHelper.hideContainers(containers, $scope.containersToHideLabels);
|
||||
}
|
||||
var containersInNetwork = [];
|
||||
containers.forEach(function(container) {
|
||||
var containerInNetwork = network.Containers[container.Id];
|
||||
|
@ -93,8 +79,16 @@ function ($scope, $state, $stateParams, $filter, Config, Network, Container, Con
|
|||
}
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.containersToHideLabels = c.hiddenLabels;
|
||||
getNetwork();
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.get({id: $stateParams.id}, function success(data) {
|
||||
$scope.network = data;
|
||||
getContainersInNetwork(data);
|
||||
}, function error(err) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve network info');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('networks', [])
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Config', 'Notifications', 'Pagination',
|
||||
function ($scope, $state, Network, Config, Notifications, Pagination) {
|
||||
.controller('NetworksController', ['$scope', '$state', 'Network', 'Notifications', 'Pagination',
|
||||
function ($scope, $state, Network, Notifications, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('networks');
|
||||
$scope.state.selectedItemCount = 0;
|
||||
|
@ -97,7 +97,7 @@ function ($scope, $state, Network, Config, Notifications, Pagination) {
|
|||
});
|
||||
};
|
||||
|
||||
function fetchNetworks() {
|
||||
function initView() {
|
||||
$('#loadNetworksSpinner').show();
|
||||
Network.query({}, function (d) {
|
||||
$scope.networks = d;
|
||||
|
@ -109,7 +109,5 @@ function ($scope, $state, Network, Config, Notifications, Pagination) {
|
|||
});
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
fetchNetworks();
|
||||
});
|
||||
initView();
|
||||
}]);
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Secret details">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="secret({id: secret.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="secrets">Secrets</a> > <a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a>
|
||||
</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-user-secret" title="Secret details"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ secret.Name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>
|
||||
{{ secret.Id }}
|
||||
<button class="btn btn-xs btn-danger" ng-click="removeSecret(secret.Id)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Delete this secret</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ secret.CreatedAt | getisodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last updated</td>
|
||||
<td>{{ secret.UpdatedAt | getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!(secret.Labels | emptyobject)">
|
||||
<td>Labels</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="(k, v) in secret.Labels">
|
||||
<td>{{ k }}</td>
|
||||
<td>{{ v }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,35 @@
|
|||
angular.module('secret', [])
|
||||
.controller('SecretController', ['$scope', '$stateParams', '$state', 'SecretService', 'Notifications',
|
||||
function ($scope, $stateParams, $state, SecretService, Notifications) {
|
||||
|
||||
$scope.removeSecret = function removeSecret(secretId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
SecretService.remove(secretId)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Secret successfully removed');
|
||||
$state.go('secrets', {});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove secret');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
SecretService.secret($stateParams.id)
|
||||
.then(function success(data) {
|
||||
$scope.secret = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve secret details');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -0,0 +1,68 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Secrets list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="secrets" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Secrets</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-user-secret" title="Secrets">
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-lg-12 col-md-12 col-xs-12">
|
||||
<div class="pull-left">
|
||||
<button type="button" class="btn btn-danger" ng-click="removeAction()" ng-disabled="!state.selectedItemCount"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
<a class="btn btn-primary" type="button" ui-sref="actions.create.secret">Add secret</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<input type="text" id="filter" ng-model="state.filter" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<th>
|
||||
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="secrets" ng-click="order('Name')">
|
||||
Name
|
||||
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ui-sref="secrets" ng-click="order('CreatedAt')">
|
||||
Created at
|
||||
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="secret in (state.filteredSecrets = ( secrets | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
|
||||
<td><input type="checkbox" ng-model="secret.Checked" ng-change="selectItem(secret)"/></td>
|
||||
<td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
|
||||
<td>{{ secret.CreatedAt | getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!secrets">
|
||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="secrets.length == 0">
|
||||
<td colspan="3" class="text-center text-muted">No secrets available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="secrets" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
<rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,76 @@
|
|||
angular.module('secrets', [])
|
||||
.controller('SecretsController', ['$scope', '$stateParams', '$state', 'SecretService', 'Notifications', 'Pagination',
|
||||
function ($scope, $stateParams, $state, SecretService, Notifications, Pagination) {
|
||||
$scope.state = {};
|
||||
$scope.state.selectedItemCount = 0;
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('secrets');
|
||||
$scope.sortType = 'Name';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.selectItems = function (allSelected) {
|
||||
angular.forEach($scope.state.filteredSecrets, function (secret) {
|
||||
if (secret.Checked !== allSelected) {
|
||||
secret.Checked = allSelected;
|
||||
$scope.selectItem(secret);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectItem = function (item) {
|
||||
if (item.Checked) {
|
||||
$scope.state.selectedItemCount++;
|
||||
} else {
|
||||
$scope.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeAction = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
var counter = 0;
|
||||
var complete = function () {
|
||||
counter = counter - 1;
|
||||
if (counter === 0) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
}
|
||||
};
|
||||
angular.forEach($scope.secrets, function (secret) {
|
||||
if (secret.Checked) {
|
||||
counter = counter + 1;
|
||||
SecretService.remove(secret.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Secret deleted', secret.Id);
|
||||
var index = $scope.secrets.indexOf(secret);
|
||||
$scope.secrets.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove secret');
|
||||
})
|
||||
.finally(function final() {
|
||||
complete();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
SecretService.secrets()
|
||||
.then(function success(data) {
|
||||
$scope.secrets = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.secrets = [];
|
||||
Notifications.error('Failure', err, 'Unable to retrieve secrets');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -0,0 +1,60 @@
|
|||
<div ng-if="applicationState.endpoint.apiVersion >= 1.25">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Secrets">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="form-inline" style="padding: 10px;">
|
||||
Add a secret:
|
||||
<select class="form-control" ng-options="secret.Name for secret in secrets" ng-model="newSecret">
|
||||
<option selected disabled hidden value="">Select a secret</option>
|
||||
</select>
|
||||
<a class="btn btn-default btn-sm" ng-click="addSecret(service, newSecret)">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add secret
|
||||
</a>
|
||||
</div>
|
||||
<table class="table" style="margin-top: 5px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>File name</th>
|
||||
<th>UID</th>
|
||||
<th>GID</th>
|
||||
<th>Mode</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="secret in service.ServiceSecrets">
|
||||
<td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
|
||||
<td>{{ secret.FileName }}</td>
|
||||
<td>{{ secret.Uid }}</td>
|
||||
<td>{{ secret.Gid }}</td>
|
||||
<td>{{ secret.Mode }}</td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-danger pull-right" type="button" ng-click="removeSecret(service, $index)" ng-disabled="isUpdating">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i> Remove secret
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="service.ServiceSecrets.length === 0">
|
||||
<td colspan="6" class="text-center text-muted">No secrets associated to this service.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
<rd-widget-footer>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!hasChanges(service, ['ServiceSecrets'])" ng-click="updateService(service)">Apply changes</button>
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="cancelChanges(service, ['ServiceSecrets'])">Reset changes</a></li>
|
||||
<li><a ng-click="cancelChanges(service)">Reset all changes</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-footer>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -109,6 +109,7 @@
|
|||
<li><a href ng-click="goToItem('service-restart-policy')">Restart policy</a></li>
|
||||
<li><a href ng-click="goToItem('service-update-config')">Update configuration</a></li>
|
||||
<li><a href ng-click="goToItem('service-labels')">Service labels</a></li>
|
||||
<li><a href ng-click="goToItem('service-secrets')">Secrets</a></li>
|
||||
<li><a href ng-click="goToItem('service-tasks')">Tasks</a></li>
|
||||
<ul>
|
||||
</rd-widget-body>
|
||||
|
@ -147,6 +148,7 @@
|
|||
<div id="service-restart-policy" class="padding-top" ng-include="'app/components/service/includes/restart.html'"></div>
|
||||
<div id="service-update-config" class="padding-top" ng-include="'app/components/service/includes/updateconfig.html'"></div>
|
||||
<div id="service-labels" class="padding-top" ng-include="'app/components/service/includes/servicelabels.html'"></div>
|
||||
<div id="service-secrets" class="padding-top" ng-include="'app/components/service/includes/secrets.html'"></div>
|
||||
<div id="service-tasks" class="padding-top" ng-include="'app/components/service/includes/tasks.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('service', [])
|
||||
.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'ServiceService', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
|
||||
function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceService, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
|
||||
.controller('ServiceController', ['$q', '$scope', '$stateParams', '$state', '$location', '$anchorScroll', 'ServiceService', 'Secret', 'SecretHelper', 'Service', 'ServiceHelper', 'TaskService', 'NodeService', 'Notifications', 'Pagination', 'ModalService', 'ControllerDataPipeline',
|
||||
function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceService, Secret, SecretHelper, Service, ServiceHelper, TaskService, NodeService, Notifications, Pagination, ModalService, ControllerDataPipeline) {
|
||||
|
||||
$scope.state = {};
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('service_tasks');
|
||||
|
@ -55,6 +55,18 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
|
|||
updateServiceArray(service, 'EnvironmentVariables', service.EnvironmentVariables);
|
||||
}
|
||||
};
|
||||
$scope.addSecret = function addSecret(service, secret) {
|
||||
if (secret && service.ServiceSecrets.filter(function(serviceSecret) { return serviceSecret.Id === secret.Id;}).length === 0) {
|
||||
service.ServiceSecrets.push({ Id: secret.Id, Name: secret.Name, FileName: secret.Name, Uid: '0', Gid: '0', Mode: 444 });
|
||||
updateServiceArray(service, 'ServiceSecrets', service.ServiceSecrets);
|
||||
}
|
||||
};
|
||||
$scope.removeSecret = function removeSecret(service, index) {
|
||||
var removedElement = service.ServiceSecrets.splice(index, 1);
|
||||
if (removedElement !== null) {
|
||||
updateServiceArray(service, 'ServiceSecrets', service.ServiceSecrets);
|
||||
}
|
||||
};
|
||||
$scope.addLabel = function addLabel(service) {
|
||||
service.ServiceLabels.push({ key: '', value: '', originalValue: '' });
|
||||
updateServiceArray(service, 'ServiceLabels', service.ServiceLabels);
|
||||
|
@ -162,7 +174,7 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
|
|||
config.TaskTemplate.ContainerSpec.Env = translateEnvironmentVariablesToEnv(service.EnvironmentVariables);
|
||||
config.TaskTemplate.ContainerSpec.Labels = translateServiceLabelsToLabels(service.ServiceContainerLabels);
|
||||
config.TaskTemplate.ContainerSpec.Image = service.Image;
|
||||
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets;
|
||||
config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : [];
|
||||
|
||||
if (service.Mode === 'replicated') {
|
||||
config.Mode.Replicated.Replicas = service.Replicas;
|
||||
|
@ -246,7 +258,7 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
|
|||
}
|
||||
|
||||
function translateServiceArrays(service) {
|
||||
service.ServiceSecrets = service.Secrets;
|
||||
service.ServiceSecrets = service.Secrets ? service.Secrets.map(SecretHelper.flattenSecret) : [];
|
||||
service.EnvironmentVariables = translateEnvironmentVariables(service.Env);
|
||||
service.ServiceLabels = translateLabelsToServiceLabels(service.Labels);
|
||||
service.ServiceContainerLabels = translateLabelsToServiceLabels(service.ContainerLabels);
|
||||
|
@ -272,14 +284,19 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
|
|||
|
||||
return $q.all({
|
||||
tasks: TaskService.serviceTasks(service.Name),
|
||||
nodes: NodeService.nodes()
|
||||
nodes: NodeService.nodes(),
|
||||
secrets: Secret.query({}).$promise
|
||||
});
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.tasks = data.tasks;
|
||||
$scope.nodes = data.nodes;
|
||||
$scope.secrets = data.secrets.map(function (secret) {
|
||||
return new SecretViewModel(secret);
|
||||
});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.secrets = [];
|
||||
Notifications.error('Failure', err, 'Unable to retrieve service details');
|
||||
})
|
||||
.finally(function final() {
|
||||
|
@ -287,6 +304,20 @@ function ($q, $scope, $stateParams, $state, $location, $anchorScroll, ServiceSer
|
|||
});
|
||||
}
|
||||
|
||||
function fetchSecrets() {
|
||||
$('#loadSecretsSpinner').show();
|
||||
Secret.query({}, function (d) {
|
||||
$scope.secrets = d.map(function (secret) {
|
||||
return new SecretViewModel(secret);
|
||||
});
|
||||
$('#loadSecretsSpinner').hide();
|
||||
}, function(e) {
|
||||
$('#loadSecretsSpinner').hide();
|
||||
Notifications.error('Failure', e, 'Unable to retrieve secrets');
|
||||
$scope.secrets = [];
|
||||
});
|
||||
}
|
||||
|
||||
$scope.updateServiceAttribute = function updateServiceAttribute(service, name) {
|
||||
if (service[name] !== originalService[name] || !(name in originalService)) {
|
||||
service.hasChanges = true;
|
||||
|
|
|
@ -1,66 +1,153 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Settings">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px;"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Settings</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<div class="col-sm-12">
|
||||
<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>
|
||||
<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">
|
||||
<form class="form-horizontal">
|
||||
<!-- logo -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Logo
|
||||
</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>
|
||||
<label for="toggle_logo" class="control-label text-left">
|
||||
Use custom logo
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- confirm-password-input -->
|
||||
<div ng-if="formValues.customLogo">
|
||||
<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>
|
||||
<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>
|
||||
<!-- !confirm-password-input -->
|
||||
<!-- !logo -->
|
||||
<!-- app-templates -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
App Templates
|
||||
</div>
|
||||
<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>
|
||||
<label for="toggle_templates" class="control-label text-left">
|
||||
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 ng-if="formValues.customTemplates">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
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.
|
||||
</span>
|
||||
</div>
|
||||
<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 class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_external_contrib" class="control-label text-left">
|
||||
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>
|
||||
<!-- !app-templates -->
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<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>
|
||||
<!-- !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="Hidden 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()" ng-disabled="!formValues.labelValue || !formValues.labelName"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add filter</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="3" class="text-center text-muted">No filter available.</td>
|
||||
</tr>
|
||||
<tr ng-if="!settings.BlackListedLabels">
|
||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !filtered-labels -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -1,29 +1,94 @@
|
|||
angular.module('settings', [])
|
||||
.controller('SettingsController', ['$scope', '$state', '$sanitize', 'Authentication', 'UserService', 'Notifications',
|
||||
function ($scope, $state, $sanitize, Authentication, UserService, Notifications) {
|
||||
.controller('SettingsController', ['$scope', '$state', 'Notifications', 'SettingsService', 'StateManager', 'DEFAULT_TEMPLATES_URL',
|
||||
function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_TEMPLATES_URL) {
|
||||
|
||||
$scope.formValues = {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
customLogo: false,
|
||||
customTemplates: false,
|
||||
externalContributions: false,
|
||||
labelName: '',
|
||||
labelValue: ''
|
||||
};
|
||||
|
||||
$scope.updatePassword = function() {
|
||||
$scope.invalidPassword = false;
|
||||
var userID = Authentication.getUserDetails().ID;
|
||||
var currentPassword = $sanitize($scope.formValues.currentPassword);
|
||||
var newPassword = $sanitize($scope.formValues.newPassword);
|
||||
$scope.removeFilteredContainerLabel = function(index) {
|
||||
var settings = $scope.settings;
|
||||
settings.BlackListedLabels.splice(index, 1);
|
||||
|
||||
UserService.updateUserPassword(userID, currentPassword, newPassword)
|
||||
.then(function success() {
|
||||
Notifications.success('Success', 'Password successfully updated');
|
||||
$state.reload();
|
||||
updateSettings(settings, false);
|
||||
};
|
||||
|
||||
$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) {
|
||||
if (err.invalidPassword) {
|
||||
$scope.invalidPassword = true;
|
||||
} else {
|
||||
Notifications.error('Failure', err, err.msg);
|
||||
}
|
||||
Notifications.error('Failure', err, 'Unable to update settings');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#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 class="sidebar-list">
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -40,6 +40,9 @@
|
|||
<li class="sidebar-list">
|
||||
<a ui-sref="volumes" ui-sref-active="active">Volumes <span class="menu-icon fa fa-cubes"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.apiVersion >= 1.25 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'">
|
||||
<a ui-sref="secrets" ui-sref-active="active">Secrets <span class="menu-icon fa fa-user-secret"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'">
|
||||
<a ui-sref="events" ui-sref-active="active">Events <span class="menu-icon fa fa-history"></span></a>
|
||||
</li>
|
||||
|
@ -49,7 +52,7 @@
|
|||
<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>
|
||||
</li>
|
||||
<li class="sidebar-title" ng-if="isAdmin || isTeamLeader">
|
||||
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
||||
<span>Portainer settings</span>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
|
||||
|
@ -61,6 +64,9 @@
|
|||
<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>
|
||||
</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>
|
||||
<div class="sidebar-footer">
|
||||
<div class="col-xs-12">
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
angular.module('sidebar', [])
|
||||
.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'Config', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService',
|
||||
function ($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, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService) {
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
});
|
||||
|
||||
$scope.uiVersion = Settings.uiVersion;
|
||||
$scope.uiVersion = StateManager.getState().application.version;
|
||||
$scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
$scope.endpoints = [];
|
||||
|
||||
$scope.switchEndpoint = function(endpoint) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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',
|
||||
function ($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, ContainerService, ContainerHelper, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, Pagination, ResourceControlService, Authentication, ControllerDataPipeline, FormValidator) {
|
||||
$scope.state = {
|
||||
selectedTemplate: null,
|
||||
showAdvancedOptions: false,
|
||||
|
@ -159,11 +159,10 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, Config, Cont
|
|||
|
||||
function initTemplates() {
|
||||
var templatesKey = $stateParams.key;
|
||||
Config.$promise.then(function (c) {
|
||||
$q.all({
|
||||
templates: TemplateService.getTemplates(templatesKey),
|
||||
containers: ContainerService.getContainers(0, c.hiddenLabels),
|
||||
networks: NetworkService.getNetworks(),
|
||||
containers: ContainerService.getContainers(0),
|
||||
networks: NetworkService.networks(),
|
||||
volumes: VolumeService.getVolumes()
|
||||
})
|
||||
.then(function success(data) {
|
||||
|
@ -184,7 +183,6 @@ function ($scope, $q, $state, $stateParams, $anchorScroll, $filter, Config, Cont
|
|||
.finally(function final(){
|
||||
$('#loadTemplatesSpinner').hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initTemplates();
|
||||
|
|
|
@ -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) {
|
||||
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'
|
||||
};
|
||||
return directive;
|
||||
|
|
|
@ -7,20 +7,5 @@ angular.module('portainer.helpers')
|
|||
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;
|
||||
}]);
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
angular.module('portainer.helpers')
|
||||
.factory('SecretHelper', [function SecretHelperFactory() {
|
||||
'use strict';
|
||||
return {
|
||||
flattenSecret: function(secret) {
|
||||
if (secret) {
|
||||
return {
|
||||
Id: secret.SecretID,
|
||||
Name: secret.SecretName,
|
||||
FileName: secret.File.Name,
|
||||
Uid: secret.File.UID,
|
||||
Gid: secret.File.GID,
|
||||
Mode: secret.File.Mode
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
secretConfig: function(secret) {
|
||||
if (secret) {
|
||||
return {
|
||||
SecretID: secret.Id,
|
||||
SecretName: secret.Name,
|
||||
File: {
|
||||
Name: secret.Name,
|
||||
UID: '0',
|
||||
GID: '0',
|
||||
Mode: 444
|
||||
}
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
}]);
|
|
@ -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;
|
||||
}
|
|
@ -11,8 +11,9 @@ function TemplateViewModel(data) {
|
|||
this.Network = data.network ? data.network : '';
|
||||
this.Env = data.env ? data.env : [];
|
||||
this.Privileged = data.privileged ? data.privileged : false;
|
||||
this.Volumes = [];
|
||||
this.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
this.Volumes = [];
|
||||
if (data.volumes) {
|
||||
this.Volumes = data.volumes.map(function (v) {
|
||||
return {
|
||||
|
|
|
@ -10,6 +10,8 @@ function TemplateLSIOViewModel(data) {
|
|||
this.Network = data.network ? data.network : '';
|
||||
this.Env = data.env ? data.env : [];
|
||||
this.Privileged = data.privileged ? data.privileged : false;
|
||||
this.Interactive = data.interactive ? data.interactive : false;
|
||||
this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always';
|
||||
this.Volumes = [];
|
||||
if (data.volumes) {
|
||||
this.Volumes = data.volumes.map(function (v) {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
function SecretViewModel(data) {
|
||||
this.Id = data.ID;
|
||||
this.CreatedAt = data.CreatedAt;
|
||||
this.UpdatedAt = data.UpdatedAt;
|
||||
this.Version = data.Version.Index;
|
||||
this.Name = data.Spec.Name;
|
||||
this.Labels = data.Spec.Labels;
|
||||
}
|
|
@ -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')
|
||||
.factory('Container', ['$resource', 'Settings', 'EndpointProvider', function ContainerFactory($resource, Settings, EndpointProvider) {
|
||||
.factory('Container', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function ContainerFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(Settings.url + '/:endpointId/containers/:id/:action', {
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/containers/:id/:action', {
|
||||
name: '@name',
|
||||
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')
|
||||
.factory('ContainerLogs', ['$http', 'Settings', 'EndpointProvider', function ContainerLogsFactory($http, Settings, EndpointProvider) {
|
||||
.factory('ContainerLogs', ['$http', 'DOCKER_ENDPOINT', 'EndpointProvider', function ContainerLogsFactory($http, DOCKER_ENDPOINT, EndpointProvider) {
|
||||
'use strict';
|
||||
return {
|
||||
get: function (id, params, callback) {
|
||||
$http({
|
||||
method: 'GET',
|
||||
url: Settings.url + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/logs',
|
||||
url: DOCKER_ENDPOINT + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/logs',
|
||||
params: {
|
||||
'stdout': params.stdout || 0,
|
||||
'stderr': params.stderr || 0,
|
|
@ -1,11 +1,11 @@
|
|||
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';
|
||||
return {
|
||||
get: function (id, params, callback, errorCallback) {
|
||||
$http({
|
||||
method: 'GET',
|
||||
url: Settings.url + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/top',
|
||||
url: DOCKER_ENDPOINT + '/' + EndpointProvider.endpointID() + '/containers/' + id + '/top',
|
||||
params: {
|
||||
ps_args: params.ps_args
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
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';
|
||||
return $resource(Settings.url + '/:endpointId/events', {
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/events', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
|
@ -1,7 +1,7 @@
|
|||
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';
|
||||
return $resource(Settings.url + '/:endpointId/exec/:id/:action', {
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/exec/:id/:action', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
|
@ -1,7 +1,7 @@
|
|||
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';
|
||||
return $resource(Settings.url + '/:endpointId/images/:id/:action', {
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/images/:id/:action', {
|
||||
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')
|
||||
.factory('Network', ['$resource', 'Settings', 'EndpointProvider', function NetworkFactory($resource, Settings, EndpointProvider) {
|
||||
.factory('Network', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function NetworkFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(Settings.url + '/:endpointId/networks/:id/:action', {
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/networks/:id/:action', {
|
||||
id: '@id',
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
|
@ -1,7 +1,7 @@
|
|||
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';
|
||||
return $resource(Settings.url + '/:endpointId/nodes/:id/:action', {
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/nodes/:id/:action', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
|
@ -0,0 +1,12 @@
|
|||
angular.module('portainer.rest')
|
||||
.factory('Secret', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function SecretFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/secrets/:id/:action', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
}, {
|
||||
get: { method: 'GET', params: {id: '@id'} },
|
||||
query: { method: 'GET', isArray: true },
|
||||
create: { method: 'POST', params: {action: 'create'} },
|
||||
remove: { method: 'DELETE', params: {id: '@id'} }
|
||||
});
|
||||
}]);
|
|
@ -1,7 +1,7 @@
|
|||
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';
|
||||
return $resource(Settings.url + '/:endpointId/services/:id/:action', {
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/services/:id/:action', {
|
||||
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')
|
||||
.factory('Task', ['$resource', 'Settings', 'EndpointProvider', function TaskFactory($resource, Settings, EndpointProvider) {
|
||||
.factory('Task', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function TaskFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(Settings.url + '/:endpointId/tasks/:id', {
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/tasks/:id', {
|
||||
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')
|
||||
.factory('Volume', ['$resource', 'Settings', 'EndpointProvider', function VolumeFactory($resource, Settings, EndpointProvider) {
|
||||
.factory('Volume', ['$resource', 'DOCKER_ENDPOINT', 'EndpointProvider', function VolumeFactory($resource, DOCKER_ENDPOINT, EndpointProvider) {
|
||||
'use strict';
|
||||
return $resource(Settings.url + '/:endpointId/volumes/:id/:action',
|
||||
return $resource(DOCKER_ENDPOINT + '/:endpointId/volumes/:id/:action',
|
||||
{
|
||||
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;
|
||||
}]);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue