From 5563ff60fc6232229b8adc6b0381ac1c2b832da2 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 15 Apr 2020 17:49:34 +1200 Subject: [PATCH 001/195] feat(templates): remove template management features (#3719) * feat(api): remove template management features * feat(templates): remove template management features --- api/bolt/datastore.go | 9 - api/bolt/migrator/migrate_dbversion14.go | 21 +-- api/bolt/migrator/migrator.go | 4 - api/bolt/template/template.go | 95 ---------- api/cli/cli.go | 19 +- api/cli/defaults.go | 1 - api/cli/defaults_windows.go | 1 - api/cmd/portainer/main.go | 47 +---- api/go.sum | 1 + .../edgetemplates/edgetemplate_list.go | 2 +- api/http/handler/settings/settings_public.go | 6 - api/http/handler/templates/handler.go | 29 ---- api/http/handler/templates/template_create.go | 122 ------------- api/http/handler/templates/template_delete.go | 25 --- .../handler/templates/template_inspect.go | 27 --- api/http/handler/templates/template_list.go | 39 ++--- api/http/handler/templates/template_update.go | 164 ------------------ api/http/security/filter.go | 18 -- api/http/server.go | 2 - api/portainer.go | 18 +- app/portainer/__module.js | 24 --- .../template-item/template-item.js | 3 - .../template-item/templateItem.html | 9 - .../components/template-list/template-list.js | 4 - .../template-list/templateList.html | 3 - app/portainer/models/settings.js | 2 - app/portainer/models/template.js | 52 ------ app/portainer/rest/template.js | 6 +- app/portainer/services/api/templateService.js | 38 +--- .../create/createTemplateController.js | 53 ------ .../templates/create/createtemplate.html | 22 --- .../views/templates/edit/template.html | 26 --- .../templates/edit/templateController.js | 66 ------- app/portainer/views/templates/templates.html | 4 - .../views/templates/templatesController.js | 27 +-- gruntfile.js | 2 +- 36 files changed, 26 insertions(+), 965 deletions(-) delete mode 100644 api/bolt/template/template.go delete mode 100644 api/http/handler/templates/template_create.go delete mode 100644 api/http/handler/templates/template_delete.go delete mode 100644 api/http/handler/templates/template_inspect.go delete mode 100644 api/http/handler/templates/template_update.go delete mode 100644 app/portainer/views/templates/create/createTemplateController.js delete mode 100644 app/portainer/views/templates/create/createtemplate.html delete mode 100644 app/portainer/views/templates/edit/template.html delete mode 100644 app/portainer/views/templates/edit/templateController.js diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index f80fd5921..18f025d73 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -26,7 +26,6 @@ import ( "github.com/portainer/portainer/api/bolt/tag" "github.com/portainer/portainer/api/bolt/team" "github.com/portainer/portainer/api/bolt/teammembership" - "github.com/portainer/portainer/api/bolt/template" "github.com/portainer/portainer/api/bolt/user" "github.com/portainer/portainer/api/bolt/version" "github.com/portainer/portainer/api/bolt/webhook" @@ -58,7 +57,6 @@ type Store struct { TagService *tag.Service TeamMembershipService *teammembership.Service TeamService *team.Service - TemplateService *template.Service TunnelServerService *tunnelserver.Service UserService *user.Service VersionService *version.Service @@ -137,7 +135,6 @@ func (store *Store) MigrateData() error { StackService: store.StackService, TagService: store.TagService, TeamMembershipService: store.TeamMembershipService, - TemplateService: store.TemplateService, UserService: store.UserService, VersionService: store.VersionService, FileService: store.fileService, @@ -246,12 +243,6 @@ func (store *Store) initServices() error { } store.TeamService = teamService - templateService, err := template.NewService(store.db) - if err != nil { - return err - } - store.TemplateService = templateService - tunnelServerService, err := tunnelserver.NewService(store.db) if err != nil { return err diff --git a/api/bolt/migrator/migrate_dbversion14.go b/api/bolt/migrator/migrate_dbversion14.go index 5ec13cd9f..d5a205d4c 100644 --- a/api/bolt/migrator/migrate_dbversion14.go +++ b/api/bolt/migrator/migrate_dbversion14.go @@ -1,11 +1,5 @@ package migrator -import ( - "strings" - - "github.com/portainer/portainer/api" -) - func (m *Migrator) updateSettingsToDBVersion15() error { legacySettings, err := m.settingsService.Settings() if err != nil { @@ -17,19 +11,6 @@ func (m *Migrator) updateSettingsToDBVersion15() error { } func (m *Migrator) updateTemplatesToVersion15() error { - legacyTemplates, err := m.templateService.Templates() - if err != nil { - return err - } - - for _, template := range legacyTemplates { - template.Logo = strings.Replace(template.Logo, "https://portainer.io/images", portainer.AssetsServerURL, -1) - - err = m.templateService.UpdateTemplate(template.ID, &template) - if err != nil { - return err - } - } - + // Removed with the entire template management layer, part of https://github.com/portainer/portainer/issues/3707 return nil } diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index f9028ccc8..1f4fa7bb0 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -15,7 +15,6 @@ import ( "github.com/portainer/portainer/api/bolt/stack" "github.com/portainer/portainer/api/bolt/tag" "github.com/portainer/portainer/api/bolt/teammembership" - "github.com/portainer/portainer/api/bolt/template" "github.com/portainer/portainer/api/bolt/user" "github.com/portainer/portainer/api/bolt/version" ) @@ -37,7 +36,6 @@ type ( stackService *stack.Service tagService *tag.Service teamMembershipService *teammembership.Service - templateService *template.Service userService *user.Service versionService *version.Service fileService portainer.FileService @@ -59,7 +57,6 @@ type ( StackService *stack.Service TagService *tag.Service TeamMembershipService *teammembership.Service - TemplateService *template.Service UserService *user.Service VersionService *version.Service FileService portainer.FileService @@ -82,7 +79,6 @@ func NewMigrator(parameters *Parameters) *Migrator { settingsService: parameters.SettingsService, tagService: parameters.TagService, teamMembershipService: parameters.TeamMembershipService, - templateService: parameters.TemplateService, stackService: parameters.StackService, userService: parameters.UserService, versionService: parameters.VersionService, diff --git a/api/bolt/template/template.go b/api/bolt/template/template.go deleted file mode 100644 index e5f7a4cf5..000000000 --- a/api/bolt/template/template.go +++ /dev/null @@ -1,95 +0,0 @@ -package template - -import ( - "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/bolt/internal" - - "github.com/boltdb/bolt" -) - -const ( - // BucketName represents the name of the bucket where this service stores data. - BucketName = "templates" -) - -// Service represents a service for managing endpoint data. -type Service struct { - db *bolt.DB -} - -// NewService creates a new instance of a service. -func NewService(db *bolt.DB) (*Service, error) { - err := internal.CreateBucket(db, BucketName) - if err != nil { - return nil, err - } - - return &Service{ - db: db, - }, nil -} - -// Templates return an array containing all the templates. -func (service *Service) Templates() ([]portainer.Template, error) { - var templates = make([]portainer.Template, 0) - - err := service.db.View(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(BucketName)) - - cursor := bucket.Cursor() - for k, v := cursor.First(); k != nil; k, v = cursor.Next() { - var template portainer.Template - err := internal.UnmarshalObject(v, &template) - if err != nil { - return err - } - templates = append(templates, template) - } - - return nil - }) - - return templates, err -} - -// Template returns a template by ID. -func (service *Service) Template(ID portainer.TemplateID) (*portainer.Template, error) { - var template portainer.Template - identifier := internal.Itob(int(ID)) - - err := internal.GetObject(service.db, BucketName, identifier, &template) - if err != nil { - return nil, err - } - - return &template, nil -} - -// CreateTemplate creates a new template. -func (service *Service) CreateTemplate(template *portainer.Template) error { - return service.db.Update(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(BucketName)) - - id, _ := bucket.NextSequence() - template.ID = portainer.TemplateID(id) - - data, err := internal.MarshalObject(template) - if err != nil { - return err - } - - return bucket.Put(internal.Itob(int(template.ID)), data) - }) -} - -// UpdateTemplate saves a template. -func (service *Service) UpdateTemplate(ID portainer.TemplateID, template *portainer.Template) error { - identifier := internal.Itob(int(ID)) - return internal.UpdateObject(service.db, BucketName, identifier, template) -} - -// DeleteTemplate deletes a template. -func (service *Service) DeleteTemplate(ID portainer.TemplateID) error { - identifier := internal.Itob(int(ID)) - return internal.DeleteObject(service.db, BucketName, identifier) -} diff --git a/api/cli/cli.go b/api/cli/cli.go index 775aa9242..91d4b3f47 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -20,7 +20,6 @@ const ( errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://") errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe") errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file") - errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk") errInvalidSyncInterval = portainer.Error("Invalid synchronization interval") errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval") errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints") @@ -58,7 +57,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { 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 definitions.").Short('t').String(), - TemplateFile: kingpin.Flag("template-file", "Path to the App templates definitions on the filesystem (deprecated)").Default(defaultTemplateFile).String(), } kingpin.Parse() @@ -83,12 +81,7 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { return errEndpointExcludeExternal } - err := validateTemplateFile(*flags.TemplateFile) - if err != nil { - return err - } - - err = validateEndpointURL(*flags.EndpointURL) + err := validateEndpointURL(*flags.EndpointURL) if err != nil { return err } @@ -173,16 +166,6 @@ func validateExternalEndpoints(externalEndpoints string) error { return nil } -func validateTemplateFile(templateFile string) error { - if _, err := os.Stat(templateFile); err != nil { - if os.IsNotExist(err) { - return errTemplateFileNotFound - } - return err - } - return nil -} - func validateSyncInterval(syncInterval string) error { if syncInterval != defaultSyncInterval { _, err := time.ParseDuration(syncInterval) diff --git a/api/cli/defaults.go b/api/cli/defaults.go index 504742771..f644e1ff6 100644 --- a/api/cli/defaults.go +++ b/api/cli/defaults.go @@ -21,5 +21,4 @@ const ( defaultSyncInterval = "60s" defaultSnapshot = "true" defaultSnapshotInterval = "5m" - defaultTemplateFile = "/templates.json" ) diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go index 4e7ce7c3e..b9e13d571 100644 --- a/api/cli/defaults_windows.go +++ b/api/cli/defaults_windows.go @@ -19,5 +19,4 @@ const ( defaultSyncInterval = "60s" defaultSnapshot = "true" defaultSnapshotInterval = "5m" - defaultTemplateFile = "/templates.json" ) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index a606fefd4..39c9bac92 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "log" "os" "strings" @@ -276,6 +275,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL EnableHostManagementFeatures: false, SnapshotInterval: *flags.SnapshotInterval, EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + TemplatesURL: portainer.DefaultTemplatesURL, } if *flags.Templates != "" { @@ -296,45 +296,6 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL return nil } -func initTemplates(templateService portainer.TemplateService, fileService portainer.FileService, templateURL, templateFile string) error { - if templateURL != "" { - log.Printf("Portainer started with the --templates flag. Using external templates, template management will be disabled.") - return nil - } - - existingTemplates, err := templateService.Templates() - if err != nil { - return err - } - - if len(existingTemplates) != 0 { - log.Printf("Templates already registered inside the database. Skipping template import.") - return nil - } - - templatesJSON, err := fileService.GetFileContent(templateFile) - if err != nil { - log.Println("Unable to retrieve template definitions via filesystem") - return err - } - - var templates []portainer.Template - err = json.Unmarshal(templatesJSON, &templates) - if err != nil { - log.Println("Unable to parse templates file. Please review your template definition file.") - return err - } - - for _, template := range templates { - err := templateService.CreateTemplate(&template) - if err != nil { - return err - } - } - - return nil -} - func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint { endpoints, err := endpointService.Endpoints() if err != nil { @@ -561,11 +522,6 @@ func main() { composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService) - err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile) - if err != nil { - log.Fatal(err) - } - err = initSettings(store.SettingsService, flags) if err != nil { log.Fatal(err) @@ -674,7 +630,6 @@ func main() { StackService: store.StackService, ScheduleService: store.ScheduleService, TagService: store.TagService, - TemplateService: store.TemplateService, WebhookService: store.WebhookService, SwarmStackManager: swarmStackManager, ComposeStackManager: composeStackManager, diff --git a/api/go.sum b/api/go.sum index 80c8f01f4..0d61838ad 100644 --- a/api/go.sum +++ b/api/go.sum @@ -175,6 +175,7 @@ github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yH github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0= +github.com/portainer/portainer v0.10.1 h1:I8K345CjGWfUGsVA8c8/gqamwLCC6CIAjxZXSklAFq0= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= diff --git a/api/http/handler/edgetemplates/edgetemplate_list.go b/api/http/handler/edgetemplates/edgetemplate_list.go index 4b82f19c2..f2ec34a4a 100644 --- a/api/http/handler/edgetemplates/edgetemplate_list.go +++ b/api/http/handler/edgetemplates/edgetemplate_list.go @@ -18,7 +18,7 @@ func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } - url := portainer.EdgeTemplatesURL + url := portainer.DefaultTemplatesURL if settings.TemplatesURL != "" { url = settings.TemplatesURL } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index e70125afe..a5cd0ac57 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -17,7 +17,6 @@ type publicSettingsResponse struct { AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - ExternalTemplates bool `json:"ExternalTemplates"` OAuthLoginURI string `json:"OAuthLoginURI"` } @@ -36,7 +35,6 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, - ExternalTemplates: false, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", settings.OAuthSettings.AuthorizationURI, settings.OAuthSettings.ClientID, @@ -44,9 +42,5 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * settings.OAuthSettings.Scopes), } - if settings.TemplatesURL != "" { - publicSettings.ExternalTemplates = true - } - return response.JSON(w, publicSettings) } diff --git a/api/http/handler/templates/handler.go b/api/http/handler/templates/handler.go index 3eac57b4a..994d27306 100644 --- a/api/http/handler/templates/handler.go +++ b/api/http/handler/templates/handler.go @@ -9,14 +9,9 @@ import ( "github.com/portainer/portainer/api/http/security" ) -const ( - errTemplateManagementDisabled = portainer.Error("Template management is disabled") -) - // Handler represents an HTTP API handler for managing templates. type Handler struct { *mux.Router - TemplateService portainer.TemplateService SettingsService portainer.SettingsService } @@ -28,29 +23,5 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { h.Handle("/templates", bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) - h.Handle("/templates", - bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost) - h.Handle("/templates/{id}", - bouncer.RestrictedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet) - h.Handle("/templates/{id}", - bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut) - h.Handle("/templates/{id}", - bouncer.AdminAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete) return h } - -func (handler *Handler) templateManagementCheck(next http.Handler) http.Handler { - return httperror.LoggerHandler(func(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} - } - - if settings.TemplatesURL != "" { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Portainer is configured to use external templates, template management is disabled", errTemplateManagementDisabled} - } - - next.ServeHTTP(rw, r) - return nil - }) -} diff --git a/api/http/handler/templates/template_create.go b/api/http/handler/templates/template_create.go deleted file mode 100644 index 6c4d21a25..000000000 --- a/api/http/handler/templates/template_create.go +++ /dev/null @@ -1,122 +0,0 @@ -package templates - -import ( - "net/http" - - "github.com/asaskevich/govalidator" - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/filesystem" -) - -type templateCreatePayload struct { - // Mandatory - Type int - Title string - Description string - AdministratorOnly bool - - // Opt stack/container - Name string - Logo string - Note string - Platform string - Categories []string - Env []portainer.TemplateEnv - - // Mandatory container - Image string - - // Mandatory stack - Repository portainer.TemplateRepository - - // Opt container - Registry string - Command string - Network string - Volumes []portainer.TemplateVolume - Ports []string - Labels []portainer.Pair - Privileged bool - Interactive bool - RestartPolicy string - Hostname string -} - -func (payload *templateCreatePayload) Validate(r *http.Request) error { - if payload.Type == 0 || (payload.Type != 1 && payload.Type != 2 && payload.Type != 3) { - return portainer.Error("Invalid template type. Valid values are: 1 (container), 2 (Swarm stack template) or 3 (Compose stack template).") - } - if govalidator.IsNull(payload.Title) { - return portainer.Error("Invalid template title") - } - if govalidator.IsNull(payload.Description) { - return portainer.Error("Invalid template description") - } - - if payload.Type == 1 { - if govalidator.IsNull(payload.Image) { - return portainer.Error("Invalid template image") - } - } - - if payload.Type == 2 || payload.Type == 3 { - if govalidator.IsNull(payload.Repository.URL) { - return portainer.Error("Invalid template repository URL") - } - if govalidator.IsNull(payload.Repository.StackFile) { - payload.Repository.StackFile = filesystem.ComposeFileDefaultName - } - } - - return nil -} - -// POST request on /api/templates -func (handler *Handler) templateCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - var payload templateCreatePayload - err := request.DecodeAndValidateJSONPayload(r, &payload) - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} - } - - template := &portainer.Template{ - Type: portainer.TemplateType(payload.Type), - Title: payload.Title, - Description: payload.Description, - AdministratorOnly: payload.AdministratorOnly, - Name: payload.Name, - Logo: payload.Logo, - Note: payload.Note, - Platform: payload.Platform, - Categories: payload.Categories, - Env: payload.Env, - } - - if template.Type == portainer.ContainerTemplate { - template.Image = payload.Image - template.Registry = payload.Registry - template.Command = payload.Command - template.Network = payload.Network - template.Volumes = payload.Volumes - template.Ports = payload.Ports - template.Labels = payload.Labels - template.Privileged = payload.Privileged - template.Interactive = payload.Interactive - template.RestartPolicy = payload.RestartPolicy - template.Hostname = payload.Hostname - } - - if template.Type == portainer.SwarmStackTemplate || template.Type == portainer.ComposeStackTemplate { - template.Repository = payload.Repository - } - - err = handler.TemplateService.CreateTemplate(template) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the template inside the database", err} - } - - return response.JSON(w, template) -} diff --git a/api/http/handler/templates/template_delete.go b/api/http/handler/templates/template_delete.go deleted file mode 100644 index cf82e7889..000000000 --- a/api/http/handler/templates/template_delete.go +++ /dev/null @@ -1,25 +0,0 @@ -package templates - -import ( - "net/http" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" -) - -// DELETE request on /api/templates/:id -func (handler *Handler) templateDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - id, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid template identifier route variable", err} - } - - err = handler.TemplateService.DeleteTemplate(portainer.TemplateID(id)) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the template from the database", err} - } - - return response.Empty(w) -} diff --git a/api/http/handler/templates/template_inspect.go b/api/http/handler/templates/template_inspect.go deleted file mode 100644 index bac836421..000000000 --- a/api/http/handler/templates/template_inspect.go +++ /dev/null @@ -1,27 +0,0 @@ -package templates - -import ( - "net/http" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" -) - -// GET request on /api/templates/:id -func (handler *Handler) templateInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - templateID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid template identifier route variable", err} - } - - template, err := handler.TemplateService.Template(portainer.TemplateID(templateID)) - if err == portainer.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find a template with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a template with the specified identifier inside the database", err} - } - - return response.JSON(w, template) -} diff --git a/api/http/handler/templates/template_list.go b/api/http/handler/templates/template_list.go index 41013c8ff..5d86941dd 100644 --- a/api/http/handler/templates/template_list.go +++ b/api/http/handler/templates/template_list.go @@ -1,14 +1,10 @@ package templates import ( - "encoding/json" + "io" "net/http" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/client" - "github.com/portainer/portainer/api/http/security" ) // GET request on /api/templates @@ -18,30 +14,17 @@ func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } - var templates []portainer.Template - if settings.TemplatesURL == "" { - templates, err = handler.TemplateService.Templates() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates from the database", err} - } - } else { - var templateData []byte - templateData, err = client.Get(settings.TemplatesURL, 0) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err} - } - - err = json.Unmarshal(templateData, &templates) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external templates", err} - } - } - - securityContext, err := security.RetrieveRestrictedRequestContext(r) + resp, err := http.Get(settings.TemplatesURL) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates via the network", err} + } + defer resp.Body.Close() + + w.Header().Set("Content-Type", "application/json") + _, err = io.Copy(w, resp.Body) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to write templates from templates URL", err} } - filteredTemplates := security.FilterTemplates(templates, securityContext) - return response.JSON(w, filteredTemplates) + return nil } diff --git a/api/http/handler/templates/template_update.go b/api/http/handler/templates/template_update.go deleted file mode 100644 index fb0568aa3..000000000 --- a/api/http/handler/templates/template_update.go +++ /dev/null @@ -1,164 +0,0 @@ -package templates - -import ( - "net/http" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" -) - -type templateUpdatePayload struct { - Title *string - Description *string - AdministratorOnly *bool - Name *string - Logo *string - Note *string - Platform *string - Categories []string - Env []portainer.TemplateEnv - Image *string - Registry *string - Repository portainer.TemplateRepository - Command *string - Network *string - Volumes []portainer.TemplateVolume - Ports []string - Labels []portainer.Pair - Privileged *bool - Interactive *bool - RestartPolicy *string - Hostname *string -} - -func (payload *templateUpdatePayload) Validate(r *http.Request) error { - return nil -} - -// PUT request on /api/templates/:id -func (handler *Handler) templateUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - templateID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid template identifier route variable", err} - } - - template, err := handler.TemplateService.Template(portainer.TemplateID(templateID)) - if err == portainer.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find a template with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a template with the specified identifier inside the database", err} - } - - var payload templateUpdatePayload - err = request.DecodeAndValidateJSONPayload(r, &payload) - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} - } - - updateTemplate(template, &payload) - - err = handler.TemplateService.UpdateTemplate(template.ID, template) - if err != nil { - return &httperror.HandlerError{http.StatusNotFound, "Unable to persist template changes inside the database", err} - } - - return response.JSON(w, template) -} - -func updateContainerProperties(template *portainer.Template, payload *templateUpdatePayload) { - if payload.Image != nil { - template.Image = *payload.Image - } - - if payload.Registry != nil { - template.Registry = *payload.Registry - } - - if payload.Command != nil { - template.Command = *payload.Command - } - - if payload.Network != nil { - template.Network = *payload.Network - } - - if payload.Volumes != nil { - template.Volumes = payload.Volumes - } - - if payload.Ports != nil { - template.Ports = payload.Ports - } - - if payload.Labels != nil { - template.Labels = payload.Labels - } - - if payload.Privileged != nil { - template.Privileged = *payload.Privileged - } - - if payload.Interactive != nil { - template.Interactive = *payload.Interactive - } - - if payload.RestartPolicy != nil { - template.RestartPolicy = *payload.RestartPolicy - } - - if payload.Hostname != nil { - template.Hostname = *payload.Hostname - } -} - -func updateStackProperties(template *portainer.Template, payload *templateUpdatePayload) { - if payload.Repository.URL != "" && payload.Repository.StackFile != "" { - template.Repository = payload.Repository - } -} - -func updateTemplate(template *portainer.Template, payload *templateUpdatePayload) { - if payload.Title != nil { - template.Title = *payload.Title - } - - if payload.Description != nil { - template.Description = *payload.Description - } - - if payload.Name != nil { - template.Name = *payload.Name - } - - if payload.Logo != nil { - template.Logo = *payload.Logo - } - - if payload.Note != nil { - template.Note = *payload.Note - } - - if payload.Platform != nil { - template.Platform = *payload.Platform - } - - if payload.Categories != nil { - template.Categories = payload.Categories - } - - if payload.Env != nil { - template.Env = payload.Env - } - - if payload.AdministratorOnly != nil { - template.AdministratorOnly = *payload.AdministratorOnly - } - - if template.Type == portainer.ContainerTemplate { - updateContainerProperties(template, payload) - } else if template.Type == portainer.SwarmStackTemplate || template.Type == portainer.ComposeStackTemplate { - updateStackProperties(template, payload) - } -} diff --git a/api/http/security/filter.go b/api/http/security/filter.go index ba7872c39..1716b043e 100644 --- a/api/http/security/filter.go +++ b/api/http/security/filter.go @@ -79,24 +79,6 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques return filteredRegistries } -// FilterTemplates filters templates based on the user role. -// Non-administrator template do not have access to templates where the AdministratorOnly flag is set to true. -func FilterTemplates(templates []portainer.Template, context *RestrictedRequestContext) []portainer.Template { - filteredTemplates := templates - - if !context.IsAdmin { - filteredTemplates = make([]portainer.Template, 0) - - for _, template := range templates { - if !template.AdministratorOnly { - filteredTemplates = append(filteredTemplates, template) - } - } - } - - return filteredTemplates -} - // FilterEndpoints filters endpoints based on user role and team memberships. // Non administrator users only have access to authorized endpoints (can be inherited via endoint groups). func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint { diff --git a/api/http/server.go b/api/http/server.go index f1c98ee5f..214796070 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -78,7 +78,6 @@ type Server struct { TagService portainer.TagService TeamService portainer.TeamService TeamMembershipService portainer.TeamMembershipService - TemplateService portainer.TemplateService UserService portainer.UserService WebhookService portainer.WebhookService Handler *handler.Handler @@ -282,7 +281,6 @@ func (server *Server) Start() error { var supportHandler = support.NewHandler(requestBouncer) var templatesHandler = templates.NewHandler(requestBouncer) - templatesHandler.TemplateService = server.TemplateService templatesHandler.SettingsService = server.SettingsService var uploadHandler = upload.NewHandler(requestBouncer) diff --git a/api/portainer.go b/api/portainer.go index 9b6149555..0fbb6dbea 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -538,7 +538,8 @@ type ( AccessLevel ResourceAccessLevel `json:"AccessLevel"` } - // Template represents an application template + // Template represents an application template that can be used as an App Template + // or an Edge template Template struct { // Mandatory container/stack fields ID TemplateID `json:"Id"` @@ -553,7 +554,7 @@ type ( // Mandatory stack fields Repository TemplateRepository `json:"repository"` - // Mandatory edge stack fields + // Mandatory Edge stack fields StackFile string `json:"stackFile"` // Optional stack/container fields @@ -943,15 +944,6 @@ type ( DeleteTeamMembershipByTeamID(teamID TeamID) error } - // TemplateService represents a service for managing template data - TemplateService interface { - Templates() ([]Template, error) - Template(ID TemplateID) (*Template, error) - CreateTemplate(template *Template) error - UpdateTemplate(ID TemplateID, template *Template) error - DeleteTemplate(ID TemplateID) error - } - // TunnelServerService represents a service for managing data associated to the tunnel server TunnelServerService interface { Info() (*TunnelServerInfo, error) @@ -1039,8 +1031,8 @@ const ( DefaultEdgeAgentCheckinIntervalInSeconds = 5 // LocalExtensionManifestFile represents the name of the local manifest file for extensions LocalExtensionManifestFile = "/extensions.json" - // EdgeTemplatesURL represents the URL used to retrieve Edge templates - EdgeTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-1.20.0.json" + // DefaultTemplatesURL represents the URL to the official templates supported by Portainer + DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json" ) const ( diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 9605c670d..6d03c1d8f 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -533,28 +533,6 @@ angular.module('portainer.app', []).config([ }, }; - var template = { - name: 'portainer.templates.template', - url: '/:id', - views: { - 'content@': { - templateUrl: './views/templates/edit/template.html', - controller: 'TemplateController', - }, - }, - }; - - var templateCreation = { - name: 'portainer.templates.new', - url: '/new', - views: { - 'content@': { - templateUrl: './views/templates/create/createtemplate.html', - controller: 'CreateTemplateController', - }, - }, - }; - $stateRegistryProvider.register(root); $stateRegistryProvider.register(portainer); $stateRegistryProvider.register(about); @@ -595,7 +573,5 @@ angular.module('portainer.app', []).config([ $stateRegistryProvider.register(teams); $stateRegistryProvider.register(team); $stateRegistryProvider.register(templates); - $stateRegistryProvider.register(template); - $stateRegistryProvider.register(templateCreation); }, ]); diff --git a/app/portainer/components/template-list/template-item/template-item.js b/app/portainer/components/template-list/template-item/template-item.js index 4bfabd437..7b398f8dc 100644 --- a/app/portainer/components/template-list/template-item/template-item.js +++ b/app/portainer/components/template-list/template-item/template-item.js @@ -3,8 +3,5 @@ angular.module('portainer.app').component('templateItem', { bindings: { model: '=', onSelect: '<', - onDelete: '<', - showUpdateAction: '<', - showDeleteAction: '<', }, }); diff --git a/app/portainer/components/template-list/template-item/templateItem.html b/app/portainer/components/template-list/template-item/templateItem.html index e287c36c9..cabab77ff 100644 --- a/app/portainer/components/template-list/template-item/templateItem.html +++ b/app/portainer/components/template-list/template-item/templateItem.html @@ -28,15 +28,6 @@ - - - - Update - - - Delete - - diff --git a/app/portainer/components/template-list/template-list.js b/app/portainer/components/template-list/template-list.js index 40f8784b6..6871b65ea 100644 --- a/app/portainer/components/template-list/template-list.js +++ b/app/portainer/components/template-list/template-list.js @@ -7,10 +7,6 @@ angular.module('portainer.app').component('templateList', { templates: '<', tableKey: '@', selectAction: '<', - deleteAction: '<', showSwarmStacks: '<', - showAddAction: '<', - showUpdateAction: '<', - showDeleteAction: '<', }, }); diff --git a/app/portainer/components/template-list/templateList.html b/app/portainer/components/template-list/templateList.html index 8983f0dbd..a2be808f8 100644 --- a/app/portainer/components/template-list/templateList.html +++ b/app/portainer/components/template-list/templateList.html @@ -49,10 +49,7 @@
Loading... diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 7a6324005..8e0247db9 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -9,7 +9,6 @@ export function SettingsViewModel(data) { this.AllowVolumeBrowserForRegularUsers = data.AllowVolumeBrowserForRegularUsers; this.SnapshotInterval = data.SnapshotInterval; this.TemplatesURL = data.TemplatesURL; - this.ExternalTemplates = data.ExternalTemplates; this.EnableHostManagementFeatures = data.EnableHostManagementFeatures; this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; @@ -21,7 +20,6 @@ export function PublicSettingsViewModel(settings) { this.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers; this.AuthenticationMethod = settings.AuthenticationMethod; this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures; - this.ExternalTemplates = settings.ExternalTemplates; this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; this.LogoURL = settings.LogoURL; this.OAuthLoginURI = settings.OAuthLoginURI; diff --git a/app/portainer/models/template.js b/app/portainer/models/template.js index 218430612..f0bd75fd4 100644 --- a/app/portainer/models/template.js +++ b/app/portainer/models/template.js @@ -1,58 +1,6 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -export function TemplateDefaultModel() { - this.Type = 1; - this.AdministratorOnly = false; - this.Title = ''; - this.Description = ''; - this.Volumes = []; - this.Ports = []; - this.Env = []; - this.Labels = []; - this.RestartPolicy = 'always'; - this.RegistryModel = new PorImageRegistryModel(); -} - -export function TemplateCreateRequest(model) { - this.Type = model.Type; - this.Name = model.Name; - this.Hostname = model.Hostname; - this.Title = model.Title; - this.Description = model.Description; - this.Note = model.Note; - this.Categories = model.Categories; - this.Platform = model.Platform; - this.Logo = model.Logo; - this.Image = model.RegistryModel.Image; - this.Registry = model.RegistryModel.Registry.URL; - this.Command = model.Command; - this.Network = model.Network && model.Network.Name; - this.Privileged = model.Privileged; - this.Interactive = model.Interactive; - this.RestartPolicy = model.RestartPolicy; - this.Labels = model.Labels; - this.Repository = model.Repository; - this.Env = model.Env; - this.AdministratorOnly = model.AdministratorOnly; - - this.Ports = []; - for (var i = 0; i < model.Ports.length; i++) { - var binding = model.Ports[i]; - if (binding.containerPort && binding.protocol) { - var port = binding.hostPort ? binding.hostPort + ':' + binding.containerPort + '/' + binding.protocol : binding.containerPort + '/' + binding.protocol; - this.Ports.push(port); - } - } - - this.Volumes = model.Volumes; -} - -export function TemplateUpdateRequest(model) { - TemplateCreateRequest.call(this, model); - this.id = model.Id; -} - export function TemplateViewModel(data) { this.Id = data.Id; this.Title = data.title; diff --git a/app/portainer/rest/template.js b/app/portainer/rest/template.js index ece15b5dc..8d79157cb 100644 --- a/app/portainer/rest/template.js +++ b/app/portainer/rest/template.js @@ -6,11 +6,7 @@ angular.module('portainer.app').factory('Templates', [ API_ENDPOINT_TEMPLATES + '/:id', {}, { - create: { method: 'POST' }, - query: { method: 'GET', isArray: true }, - get: { method: 'GET', params: { id: '@id' } }, - update: { method: 'PUT', params: { id: '@id' } }, - remove: { method: 'DELETE', params: { id: '@id' } }, + query: { method: 'GET' }, } ); }, diff --git a/app/portainer/services/api/templateService.js b/app/portainer/services/api/templateService.js index ddcf0dfd3..7788ae19c 100644 --- a/app/portainer/services/api/templateService.js +++ b/app/portainer/services/api/templateService.js @@ -1,4 +1,4 @@ -import { TemplateViewModel, TemplateCreateRequest, TemplateUpdateRequest } from '../../models/template'; +import { TemplateViewModel } from '../../models/template'; angular.module('portainer.app').factory('TemplateService', [ '$q', @@ -21,7 +21,7 @@ angular.module('portainer.app').factory('TemplateService', [ dockerhub: DockerHubService.dockerhub(), }) .then(function success(data) { - const templates = data.templates.map(function (item) { + const templates = data.templates.templates.map(function (item) { const res = new TemplateViewModel(item); const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(res.RegistryModel.Registry.URL, data.registries, data.dockerhub); registry.Image = res.RegistryModel.Image; @@ -37,40 +37,6 @@ angular.module('portainer.app').factory('TemplateService', [ return deferred.promise; }; - service.template = function (id) { - var deferred = $q.defer(); - let template; - Templates.get({ id: id }) - .$promise.then(function success(data) { - template = new TemplateViewModel(data); - return RegistryService.retrievePorRegistryModelFromRepository(template.RegistryModel.Registry.URL); - }) - .then((registry) => { - registry.Image = template.RegistryModel.Image; - template.RegistryModel = registry; - deferred.resolve(template); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve template details', err: err }); - }); - - return deferred.promise; - }; - - service.delete = function (id) { - return Templates.remove({ id: id }).$promise; - }; - - service.create = function (model) { - var payload = new TemplateCreateRequest(model); - return Templates.create(payload).$promise; - }; - - service.update = function (model) { - var payload = new TemplateUpdateRequest(model); - return Templates.update(payload).$promise; - }; - service.createTemplateConfiguration = function (template, containerName, network) { var imageConfiguration = ImageHelper.createImageConfigForContainer(template.RegistryModel); var containerConfiguration = createContainerConfiguration(template, containerName, network); diff --git a/app/portainer/views/templates/create/createTemplateController.js b/app/portainer/views/templates/create/createTemplateController.js deleted file mode 100644 index 267ce0016..000000000 --- a/app/portainer/views/templates/create/createTemplateController.js +++ /dev/null @@ -1,53 +0,0 @@ -import { TemplateDefaultModel } from '../../../models/template'; - -angular.module('portainer.app').controller('CreateTemplateController', [ - '$q', - '$scope', - '$state', - 'TemplateService', - 'TemplateHelper', - 'NetworkService', - 'Notifications', - function ($q, $scope, $state, TemplateService, TemplateHelper, NetworkService, Notifications) { - $scope.state = { - actionInProgress: false, - }; - - $scope.create = function () { - var model = $scope.model; - - $scope.state.actionInProgress = true; - TemplateService.create(model) - .then(function success() { - Notifications.success('Template successfully created', model.Title); - $state.go('portainer.templates'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create template'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; - - function initView() { - $scope.model = new TemplateDefaultModel(); - var provider = $scope.applicationState.endpoint.mode.provider; - var apiVersion = $scope.applicationState.endpoint.apiVersion; - - $q.all({ - templates: TemplateService.templates(), - networks: NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25), - }) - .then(function success(data) { - $scope.categories = TemplateHelper.getUniqueCategories(data.templates); - $scope.networks = data.networks; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve template details'); - }); - } - - initView(); - }, -]); diff --git a/app/portainer/views/templates/create/createtemplate.html b/app/portainer/views/templates/create/createtemplate.html deleted file mode 100644 index 25dd94bd7..000000000 --- a/app/portainer/views/templates/create/createtemplate.html +++ /dev/null @@ -1,22 +0,0 @@ - - - Templates > Add template - - -
-
- - - - - -
-
diff --git a/app/portainer/views/templates/edit/template.html b/app/portainer/views/templates/edit/template.html deleted file mode 100644 index faa42d789..000000000 --- a/app/portainer/views/templates/edit/template.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - Templates > {{ ::template.Title }} - - -
-
- - - - - -
-
diff --git a/app/portainer/views/templates/edit/templateController.js b/app/portainer/views/templates/edit/templateController.js deleted file mode 100644 index 0db2a6435..000000000 --- a/app/portainer/views/templates/edit/templateController.js +++ /dev/null @@ -1,66 +0,0 @@ -import _ from 'lodash-es'; - -angular.module('portainer.app').controller('TemplateController', [ - '$q', - '$scope', - '$state', - '$transition$', - 'TemplateService', - 'TemplateHelper', - 'NetworkService', - 'Notifications', - function ($q, $scope, $state, $transition$, TemplateService, TemplateHelper, NetworkService, Notifications) { - $scope.state = { - actionInProgress: false, - }; - - $scope.update = function () { - var model = $scope.template; - - $scope.state.actionInProgress = true; - TemplateService.update(model) - .then(function success() { - Notifications.success('Template successfully updated', model.Title); - $state.go('portainer.templates'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update template'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; - - function initView() { - var provider = $scope.applicationState.endpoint.mode.provider; - var apiVersion = $scope.applicationState.endpoint.apiVersion; - - var templateId = $transition$.params().id; - $q.all({ - templates: TemplateService.templates(), - template: TemplateService.template(templateId), - networks: NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25), - }) - .then(function success(data) { - var template = data.template; - if (template.Network) { - template.Network = _.find(data.networks, function (o) { - return o.Name === template.Network; - }); - } else { - template.Network = _.find(data.networks, function (o) { - return o.Name === 'bridge'; - }); - } - $scope.categories = TemplateHelper.getUniqueCategories(data.templates); - $scope.template = data.template; - $scope.networks = data.networks; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve template details'); - }); - } - - initView(); - }, -]); diff --git a/app/portainer/views/templates/templates.html b/app/portainer/views/templates/templates.html index 772c3f500..e5dbd8518 100644 --- a/app/portainer/views/templates/templates.html +++ b/app/portainer/views/templates/templates.html @@ -366,10 +366,6 @@ templates="templates" table-key="templates" select-action="selectTemplate" - delete-action="deleteTemplate" - show-add-action="state.templateManagement && isAdmin" - show-update-action="state.templateManagement && isAdmin" - show-delete-action="state.templateManagement && isAdmin" show-swarm-stacks="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER' && applicationState.endpoint.apiVersion >= 1.25" >
diff --git a/app/portainer/views/templates/templatesController.js b/app/portainer/views/templates/templatesController.js index d2919866b..9805a76ce 100644 --- a/app/portainer/views/templates/templatesController.js +++ b/app/portainer/views/templates/templatesController.js @@ -20,7 +20,6 @@ angular.module('portainer.app').controller('TemplatesController', [ 'SettingsService', 'StackService', 'EndpointProvider', - 'ModalService', function ( $scope, $q, @@ -39,15 +38,13 @@ angular.module('portainer.app').controller('TemplatesController', [ FormValidator, SettingsService, StackService, - EndpointProvider, - ModalService + EndpointProvider ) { $scope.state = { selectedTemplate: null, showAdvancedOptions: false, formValidationError: '', actionInProgress: false, - templateManagement: true, }; $scope.formValues = { @@ -255,27 +252,6 @@ angular.module('portainer.app').controller('TemplatesController', [ return TemplateService.createTemplateConfiguration(template, name, network); } - $scope.deleteTemplate = function (template) { - ModalService.confirmDeletion('Do you want to delete this template?', function onConfirm(confirmed) { - if (!confirmed) { - return; - } - deleteTemplate(template); - }); - }; - - function deleteTemplate(template) { - TemplateService.delete(template.Id) - .then(function success() { - Notifications.success('Template successfully deleted'); - var idx = $scope.templates.indexOf(template); - $scope.templates.splice(idx, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove template'); - }); - } - function initView() { $scope.isAdmin = Authentication.isAdmin(); @@ -300,7 +276,6 @@ angular.module('portainer.app').controller('TemplatesController', [ $scope.availableNetworks = networks; var settings = data.settings; $scope.allowBindMounts = settings.AllowBindMountsForRegularUsers; - $scope.state.templateManagement = !settings.ExternalTemplates; }) .catch(function error(err) { $scope.templates = []; diff --git a/gruntfile.js b/gruntfile.js index 069440c4f..6794bd54e 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -154,7 +154,7 @@ function shell_run_container() { 'docker rm -f portainer', 'docker run -d -p 8000:8000 -p 9000:9000 -v $(pwd)/dist:/app -v ' + portainer_data + - ':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics --template-file /app/templates.json', + ':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics', ].join(';'); } From f371dc5402ef52e1d230054839550c3cf45aba60 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 16 Apr 2020 12:22:08 +1200 Subject: [PATCH 002/195] feat(templates): fix an issue with templates initialization and update settings view --- api/bolt/datastore.go | 27 +++--- api/bolt/init.go | 49 ++++++++++ api/cmd/portainer/main.go | 94 +++++-------------- api/portainer.go | 1 + app/portainer/views/settings/settings.html | 15 +-- .../views/settings/settingsController.js | 9 +- 6 files changed, 91 insertions(+), 104 deletions(-) diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 18f025d73..c4b51ba31 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -5,16 +5,14 @@ import ( "path" "time" - "github.com/portainer/portainer/api/bolt/edgegroup" - "github.com/portainer/portainer/api/bolt/edgestack" - "github.com/portainer/portainer/api/bolt/endpointrelation" - "github.com/portainer/portainer/api/bolt/tunnelserver" - "github.com/boltdb/bolt" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/dockerhub" + "github.com/portainer/portainer/api/bolt/edgegroup" + "github.com/portainer/portainer/api/bolt/edgestack" "github.com/portainer/portainer/api/bolt/endpoint" "github.com/portainer/portainer/api/bolt/endpointgroup" + "github.com/portainer/portainer/api/bolt/endpointrelation" "github.com/portainer/portainer/api/bolt/extension" "github.com/portainer/portainer/api/bolt/migrator" "github.com/portainer/portainer/api/bolt/registry" @@ -26,6 +24,7 @@ import ( "github.com/portainer/portainer/api/bolt/tag" "github.com/portainer/portainer/api/bolt/team" "github.com/portainer/portainer/api/bolt/teammembership" + "github.com/portainer/portainer/api/bolt/tunnelserver" "github.com/portainer/portainer/api/bolt/user" "github.com/portainer/portainer/api/bolt/version" "github.com/portainer/portainer/api/bolt/webhook" @@ -40,7 +39,7 @@ const ( type Store struct { path string db *bolt.DB - checkForDataMigration bool + isNew bool fileService portainer.FileService RoleService *role.Service DockerHubService *dockerhub.Service @@ -69,6 +68,7 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro store := &Store{ path: storePath, fileService: fileService, + isNew: true, } databasePath := path.Join(storePath, databaseFileName) @@ -77,10 +77,8 @@ func NewStore(storePath string, fileService portainer.FileService) (*Store, erro return nil, err } - if !databaseFileExists { - store.checkForDataMigration = false - } else { - store.checkForDataMigration = true + if databaseFileExists { + store.isNew = false } return store, nil @@ -106,9 +104,16 @@ func (store *Store) Close() error { return nil } +// IsNew returns true if the database was just created and false if it is re-using +// existing data. +func (store *Store) IsNew() bool { + return store.isNew +} + // MigrateData automatically migrate the data based on the DBVersion. +// This process is only triggered on an existing database, not if the database was just created. func (store *Store) MigrateData() error { - if !store.checkForDataMigration { + if store.isNew { return store.VersionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/bolt/init.go b/api/bolt/init.go index 8e1a0661c..cc0a0f261 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -4,6 +4,55 @@ import portainer "github.com/portainer/portainer/api" // Init creates the default data set. func (store *Store) Init() error { + _, err := store.SettingsService.Settings() + if err == portainer.ErrObjectNotFound { + defaultSettings := &portainer.Settings{ + AuthenticationMethod: portainer.AuthenticationInternal, + BlackListedLabels: make([]portainer.Pair, 0), + LDAPSettings: portainer.LDAPSettings{ + AnonymousMode: true, + AutoCreateUsers: true, + TLSConfig: portainer.TLSConfiguration{}, + SearchSettings: []portainer.LDAPSearchSettings{ + portainer.LDAPSearchSettings{}, + }, + GroupSearchSettings: []portainer.LDAPGroupSearchSettings{ + portainer.LDAPGroupSearchSettings{}, + }, + }, + OAuthSettings: portainer.OAuthSettings{}, + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowVolumeBrowserForRegularUsers: false, + EnableHostManagementFeatures: false, + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + TemplatesURL: portainer.DefaultTemplatesURL, + } + + err = store.SettingsService.UpdateSettings(defaultSettings) + if err != nil { + return err + } + } else if err != nil { + return err + } + + _, err = store.DockerHubService.DockerHub() + if err == portainer.ErrObjectNotFound { + defaultDockerHub := &portainer.DockerHub{ + Authentication: false, + Username: "", + Password: "", + } + + err := store.DockerHubService.UpdateDockerHub(defaultDockerHub) + if err != nil { + return err + } + } else if err != nil { + return err + } + groups, err := store.EndpointGroupService.EndpointGroups() if err != nil { return err diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 39c9bac92..795c2b473 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -235,73 +235,24 @@ func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *p } } -func initDockerHub(dockerHubService portainer.DockerHubService) error { - _, err := dockerHubService.DockerHub() - if err == portainer.ErrObjectNotFound { - dockerhub := &portainer.DockerHub{ - Authentication: false, - Username: "", - Password: "", - } - return dockerHubService.UpdateDockerHub(dockerhub) - } else if err != nil { - return err - } - - return nil -} - -func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error { - _, err := settingsService.Settings() - if err == portainer.ErrObjectNotFound { - settings := &portainer.Settings{ - LogoURL: *flags.Logo, - AuthenticationMethod: portainer.AuthenticationInternal, - LDAPSettings: portainer.LDAPSettings{ - AnonymousMode: true, - AutoCreateUsers: true, - TLSConfig: portainer.TLSConfiguration{}, - SearchSettings: []portainer.LDAPSearchSettings{ - portainer.LDAPSearchSettings{}, - }, - GroupSearchSettings: []portainer.LDAPGroupSearchSettings{ - portainer.LDAPGroupSearchSettings{}, - }, - }, - OAuthSettings: portainer.OAuthSettings{}, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowVolumeBrowserForRegularUsers: false, - EnableHostManagementFeatures: false, - SnapshotInterval: *flags.SnapshotInterval, - EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, - TemplatesURL: portainer.DefaultTemplatesURL, - } - - if *flags.Templates != "" { - settings.TemplatesURL = *flags.Templates - } - - if *flags.Labels != nil { - settings.BlackListedLabels = *flags.Labels - } else { - settings.BlackListedLabels = make([]portainer.Pair, 0) - } - - return settingsService.UpdateSettings(settings) - } else if err != nil { - return err - } - - return nil -} - -func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint { - endpoints, err := endpointService.Endpoints() +func updateSettingsFromFlags(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error { + settings, err := settingsService.Settings() if err != nil { - log.Fatal(err) + return err } - return &endpoints[0] + + settings.LogoURL = *flags.Logo + settings.SnapshotInterval = *flags.SnapshotInterval + + if *flags.Templates != "" { + settings.TemplatesURL = *flags.Templates + } + + if *flags.Labels != nil { + settings.BlackListedLabels = *flags.Labels + } + + return settingsService.UpdateSettings(settings) } func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error { @@ -522,9 +473,11 @@ func main() { composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService) - err = initSettings(store.SettingsService, flags) - if err != nil { - log.Fatal(err) + if store.IsNew() { + err = updateSettingsFromFlags(store.SettingsService, flags) + if err != nil { + log.Fatal(err) + } } jobScheduler := initJobScheduler() @@ -548,11 +501,6 @@ func main() { jobScheduler.Start() - err = initDockerHub(store.DockerHubService) - if err != nil { - log.Fatal(err) - } - applicationStatus := initStatus(endpointManagement, *flags.Snapshot, flags) err = initEndpoint(flags, store.EndpointService, snapshotter) diff --git a/api/portainer.go b/api/portainer.go index 0fbb6dbea..2fb6cb6f2 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -73,6 +73,7 @@ type ( Open() error Init() error Close() error + IsNew() bool MigrateData() error } diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 3f3da4b74..0de6b5fea 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -46,16 +46,7 @@
App Templates
-
-
- - -
-
-
+
You can specify the URL to your own template definitions file here. See @@ -67,7 +58,7 @@ URL
- +
@@ -157,7 +148,7 @@ type="button" class="btn btn-primary btn-sm" ng-click="saveApplicationSettings()" - ng-disabled="state.actionInProgress" + ng-disabled="state.actionInProgress || !settings.TemplatesURL" button-spinner="state.actionInProgress" > Save settings diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index eba237506..f2c766c34 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -25,7 +25,6 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues = { customLogo: false, - externalTemplates: false, restrictBindMounts: false, restrictPrivilegedMode: false, labelName: '', @@ -60,10 +59,6 @@ angular.module('portainer.app').controller('SettingsController', [ settings.LogoURL = ''; } - if (!$scope.formValues.externalTemplates) { - settings.TemplatesURL = ''; - } - settings.AllowBindMountsForRegularUsers = !$scope.formValues.restrictBindMounts; settings.AllowPrivilegedModeForRegularUsers = !$scope.formValues.restrictPrivilegedMode; settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser; @@ -98,12 +93,10 @@ angular.module('portainer.app').controller('SettingsController', [ .then(function success(data) { var settings = data; $scope.settings = settings; + if (settings.LogoURL !== '') { $scope.formValues.customLogo = true; } - if (settings.TemplatesURL !== '') { - $scope.formValues.externalTemplates = true; - } $scope.formValues.restrictBindMounts = !settings.AllowBindMountsForRegularUsers; $scope.formValues.restrictPrivilegedMode = !settings.AllowPrivilegedModeForRegularUsers; $scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers; From 54621ced9e6dbbf64f00360ec352f5cbbacde9fa Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Tue, 21 Apr 2020 02:05:30 +0200 Subject: [PATCH 003/195] feat(templates): support templates versioning (#3729) * feat(templates): Support templates versioning format * Update app/portainer/models/template.js Co-authored-by: Anthony Lapenna --- app/portainer/models/template.js | 64 +++++++++++-------- app/portainer/services/api/templateService.js | 20 ++++-- 2 files changed, 51 insertions(+), 33 deletions(-) diff --git a/app/portainer/models/template.js b/app/portainer/models/template.js index f0bd75fd4..5acc99bb1 100644 --- a/app/portainer/models/template.js +++ b/app/portainer/models/template.js @@ -1,32 +1,44 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -export function TemplateViewModel(data) { - this.Id = data.Id; - this.Title = data.title; - this.Type = data.type; - this.Description = data.description; - this.AdministratorOnly = data.AdministratorOnly; - this.Name = data.name; - this.Note = data.note; - this.Categories = data.categories ? data.categories : []; - this.Platform = data.platform ? data.platform : ''; - this.Logo = data.logo; - this.Repository = data.repository; - this.Hostname = data.hostname; - this.RegistryModel = new PorImageRegistryModel(); - this.RegistryModel.Image = data.image; - this.RegistryModel.Registry.URL = data.registry || ''; - this.Command = data.command ? data.command : ''; - this.Network = data.network ? data.network : ''; - this.Privileged = data.privileged ? data.privileged : false; - this.Interactive = data.interactive ? data.interactive : false; - this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always'; - this.Labels = data.labels ? data.labels : []; - this.Hosts = data.hosts ? data.hosts : []; - this.Env = templateEnv(data); - this.Volumes = templateVolumes(data); - this.Ports = templatePorts(data); +export class TemplateViewModel { + constructor(data, version) { + switch (version) { + case '2': + this.setTemplatesV2(data); + break; + default: + throw new Error('Unsupported template version'); + } + } + + setTemplatesV2(data) { + this.Id = data.Id; + this.Title = data.title; + this.Type = data.type; + this.Description = data.description; + this.AdministratorOnly = data.AdministratorOnly; + this.Name = data.name; + this.Note = data.note; + this.Categories = data.categories ? data.categories : []; + this.Platform = data.platform ? data.platform : ''; + this.Logo = data.logo; + this.Repository = data.repository; + this.Hostname = data.hostname; + this.RegistryModel = new PorImageRegistryModel(); + this.RegistryModel.Image = data.image; + this.RegistryModel.Registry.URL = data.registry || ''; + this.Command = data.command ? data.command : ''; + this.Network = data.network ? data.network : ''; + this.Privileged = data.privileged ? data.privileged : false; + this.Interactive = data.interactive ? data.interactive : false; + this.RestartPolicy = data.restart_policy ? data.restart_policy : 'always'; + this.Labels = data.labels ? data.labels : []; + this.Hosts = data.hosts ? data.hosts : []; + this.Env = templateEnv(data); + this.Volumes = templateVolumes(data); + this.Ports = templatePorts(data); + } } function templatePorts(data) { diff --git a/app/portainer/services/api/templateService.js b/app/portainer/services/api/templateService.js index 7788ae19c..973f73168 100644 --- a/app/portainer/services/api/templateService.js +++ b/app/portainer/services/api/templateService.js @@ -1,3 +1,4 @@ +import _ from 'lodash-es'; import { TemplateViewModel } from '../../models/template'; angular.module('portainer.app').factory('TemplateService', [ @@ -13,7 +14,7 @@ angular.module('portainer.app').factory('TemplateService', [ var service = {}; service.templates = function () { - var deferred = $q.defer(); + const deferred = $q.defer(); $q.all({ templates: Templates.query().$promise, @@ -21,12 +22,17 @@ angular.module('portainer.app').factory('TemplateService', [ dockerhub: DockerHubService.dockerhub(), }) .then(function success(data) { - const templates = data.templates.templates.map(function (item) { - const res = new TemplateViewModel(item); - const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(res.RegistryModel.Registry.URL, data.registries, data.dockerhub); - registry.Image = res.RegistryModel.Image; - res.RegistryModel = registry; - return res; + const version = data.templates.version; + const templates = _.map(data.templates.templates, (item) => { + try { + const template = new TemplateViewModel(item, version); + const registry = RegistryService.retrievePorRegistryModelFromRepositoryWithRegistries(template.RegistryModel.Registry.URL, data.registries, data.dockerhub); + registry.Image = template.RegistryModel.Image; + template.RegistryModel = registry; + return template; + } catch (err) { + deferred.reject({ msg: 'Unable to retrieve templates', err: err }); + } }); deferred.resolve(templates); }) From 4c4cec73d797b08e48ee49d8c2cd1088b8f003b7 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 21 Apr 2020 12:10:26 +1200 Subject: [PATCH 004/195] chore(version): bump version number --- api/portainer.go | 2 +- api/swagger.yaml | 4 ++-- api/swagger_config.json | 2 +- distribution/portainer.spec | 2 +- package.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/portainer.go b/api/portainer.go index 2fb6cb6f2..8bc57ccef 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1000,7 +1000,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "1.24.0" + APIVersion = "2.0.0-dev" // DBVersion is the version number of the Portainer database DBVersion = 23 // AssetsServerURL represents the URL of the Portainer asset server diff --git a/api/swagger.yaml b/api/swagger.yaml index 6e39642e9..1aad1e30b 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -54,7 +54,7 @@ info: **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). - version: "1.24.0" + version: "2.0.0-dev" title: "Portainer API" contact: email: "info@portainer.io" @@ -3174,7 +3174,7 @@ definitions: description: "Is analytics enabled" Version: type: "string" - example: "1.24.0" + example: "2.0.0-dev" description: "Portainer API version" PublicSettingsInspectResponse: type: "object" diff --git a/api/swagger_config.json b/api/swagger_config.json index 905e134a3..a27505b08 100644 --- a/api/swagger_config.json +++ b/api/swagger_config.json @@ -1,5 +1,5 @@ { "packageName": "portainer", - "packageVersion": "1.24.0", + "packageVersion": "2.0.0-dev", "projectName": "portainer" } diff --git a/distribution/portainer.spec b/distribution/portainer.spec index c30f2c69a..7fd9e49dd 100644 --- a/distribution/portainer.spec +++ b/distribution/portainer.spec @@ -1,5 +1,5 @@ Name: portainer -Version: 1.24.0 +Version: 2.0.0-dev Release: 0 License: Zlib Summary: A lightweight docker management UI diff --git a/package.json b/package.json index 392aab881..53cb12546 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "1.24.0", + "version": "2.0.0-dev", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git" From 800b357041f87166c14ed0fb6414cdb281dda538 Mon Sep 17 00:00:00 2001 From: Simone Cattaneo Date: Mon, 27 Apr 2020 04:14:27 +0200 Subject: [PATCH 005/195] fix(api): updated LDAP library to v3 (portainer#3244) (#3386) Co-authored-by: Anthony Lapenna --- api/ldap/ldap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go index e4ccbe848..11fe22f8f 100644 --- a/api/ldap/ldap.go +++ b/api/ldap/ldap.go @@ -7,7 +7,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" - "gopkg.in/ldap.v2" + "gopkg.in/ldap.v3" ) const ( From 070be463520c1805d1415d8c8ab69daae14d4835 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 27 Apr 2020 17:58:24 +1200 Subject: [PATCH 006/195] feat(templates): leftovers cleanup (#3762) * feat(templates): leftovers cleanup * feat(templates): update CLIFlags structure --- api/portainer.go | 1 - app/constants.js | 1 - gruntfile.js | 5 - templates.json | 891 ----------------------------------------------- 4 files changed, 898 deletions(-) delete mode 100644 templates.json diff --git a/api/portainer.go b/api/portainer.go index 8bc57ccef..c1f4f9d47 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -48,7 +48,6 @@ type ( NoAuth *bool NoAnalytics *bool Templates *string - TemplateFile *string TLS *bool TLSSkipVerify *bool TLSCacert *string diff --git a/app/constants.js b/app/constants.js index e615db918..3df08083c 100644 --- a/app/constants.js +++ b/app/constants.js @@ -22,7 +22,6 @@ angular .constant('API_ENDPOINT_TEAM_MEMBERSHIPS', 'api/team_memberships') .constant('API_ENDPOINT_TEMPLATES', 'api/templates') .constant('API_ENDPOINT_WEBHOOKS', 'api/webhooks') - .constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json') .constant('PAGINATION_MAX_ITEMS', 10) .constant('APPLICATION_CACHE_VALIDITY', 3600) .constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.') diff --git a/gruntfile.js b/gruntfile.js index 6794bd54e..95fc0cb66 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -105,11 +105,6 @@ gruntfile_cfg.eslint = { gruntfile_cfg.copy = { assets: { files: [ - { - dest: '<%= root %>/', - src: 'templates.json', - cwd: '', - }, { dest: '<%= root %>/', src: 'extensions.json', diff --git a/templates.json b/templates.json deleted file mode 100644 index c2dfe94a3..000000000 --- a/templates.json +++ /dev/null @@ -1,891 +0,0 @@ -[ - { - "type": 1, - "title": "Registry", - "description": "Docker image registry", - "categories": ["docker"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/registry.png", - "image": "registry:latest", - "ports": [ - "5000/tcp" - ], - "volumes": [{ "container": "/var/lib/registry"}] - }, - { - "type": 1, - "title": "Nginx", - "description": "High performance web server", - "categories": ["webserver"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/nginx.png", - "image": "nginx:latest", - "ports": [ - "80/tcp", - "443/tcp" - ], - "volumes": [{"container": "/etc/nginx"}, {"container": "/usr/share/nginx/html"}] - }, - { - "type": 1, - "title": "Httpd", - "description": "Open-source HTTP server", - "categories": ["webserver"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/httpd.png", - "image": "httpd:latest", - "ports": [ - "80/tcp" - ], - "volumes": [{"container": "/usr/local/apache2/htdocs/"}] - }, - { - "type": 1, - "title": "Caddy", - "description": "HTTP/2 web server with automatic HTTPS", - "categories": ["webserver"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/caddy.png", - "image": "abiosoft/caddy:latest", - "ports": [ - "80/tcp", "443/tcp", "2015/tcp" - ], - "volumes": [{"container": "/root/.caddy"}] - }, - { - "type": 1, - "title": "MySQL", - "description": "The most popular open-source database", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/mysql.png", - "image": "mysql:latest", - "env": [ - { - "name": "MYSQL_ROOT_PASSWORD", - "label": "Root password" - } - ], - "ports": [ - "3306/tcp" - ], - "volumes": [{"container": "/var/lib/mysql"}] - }, - { - "type": 1, - "title": "MariaDB", - "description": "Performance beyond MySQL", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/mariadb.png", - "image": "mariadb:latest", - "env": [ - { - "name": "MYSQL_ROOT_PASSWORD", - "label": "Root password" - } - ], - "ports": [ - "3306/tcp" - ], - "volumes": [{"container": "/var/lib/mysql"}] - }, - { - "type": 1, - "title": "PostgreSQL", - "description": "The most advanced open-source database", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/postgres.png", - "image": "postgres:latest", - "env": [ - { - "name": "POSTGRES_USER", - "label": "Superuser" - }, - { - "name": "POSTGRES_PASSWORD", - "label": "Superuser password" - } - ], - "ports": [ - "5432/tcp" - ], - "volumes": [{"container": "/var/lib/postgresql/data"}] - }, - { - "type": 1, - "title": "Mongo", - "description": "Open-source document-oriented database", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/mongo.png", - "image": "mongo:latest", - "ports": [ - "27017/tcp" - ], - "volumes": [{"container": "/data/db"}] - }, - { - "type": 1, - "title": "CockroachDB", - "description": "An open-source, survivable, strongly consistent, scale-out SQL database", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/cockroachdb.png", - "image": "cockroachdb/cockroach:latest", - "ports": [ - "26257/tcp", - "8080/tcp" - ], - "volumes": [{"container": "/cockroach/cockroach-data"}], - "command": "start --insecure" - }, - { - "type": 1, - "title": "CrateDB", - "description": "An open-source distributed SQL database", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/cratedb.png", - "image": "crate:latest", - "ports": [ - "4200/tcp", - "4300/tcp" - ], - "volumes": [{"container": "/data"}] - }, - { - "type": 1, - "title": "Elasticsearch", - "description": "Open-source search and analytics engine", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/elasticsearch.png", - "image": "elasticsearch:latest", - "ports": [ - "9200/tcp", - "9300/tcp" - ], - "volumes": [{"container": "/usr/share/elasticsearch/data"}] - }, - { - "type": 1, - "title": "Gitlab CE", - "description": "Open-source end-to-end software development platform", - "note": "Default username is root. Check the Gitlab documentation to get started.", - "categories": ["development", "project-management"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/gitlab_ce.png", - "image": "gitlab/gitlab-ce:latest", - "ports": [ - "80/tcp", - "443/tcp", - "22/tcp" - ], - "volumes": [ - { "container": "/etc/gitlab" }, - { "container": "/var/log/gitlab" }, - { "container": "/var/opt/gitlab" } - ] - }, - { - "type": 1, - "title": "Minio", - "description": "A distributed object storage server built for cloud applications and devops", - "categories": ["storage"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/minio.png", - "image": "minio/minio:latest", - "ports": [ - "9000/tcp" - ], - "env": [ - { - "name": "MINIO_ACCESS_KEY", - "label": "Minio access key" - }, - { - "name": "MINIO_SECRET_KEY", - "label": "Minio secret key" - } - ], - "volumes": [{"container": "/data"}, {"container": "/root/.minio"}], - "command": "server /data" - }, - { - "type": 1, - "title": "Scality S3", - "description": "Standalone AWS S3 protocol server", - "categories": ["storage"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/scality-s3.png", - "image": "scality/s3server", - "ports": [ - "8000/tcp" - ], - "env": [ - { - "name": "SCALITY_ACCESS_KEY", - "label": "Scality S3 access key" - }, - { - "name": "SCALITY_SECRET_KEY", - "label": "Scality S3 secret key" - } - ], - "volumes": [{"container": "/usr/src/app/localData"}, {"container": "/usr/src/app/localMetadata"}] - }, - { - "type": 1, - "title": "SQL Server", - "description": "Microsoft SQL Server on Linux", - "categories": ["database"], - "platform": "linux", - "note": "Password needs to include at least 8 characters including uppercase, lowercase letters, base-10 digits and/or non-alphanumeric symbols.", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png", - "image": "microsoft/mssql-server-linux:2017-GA", - "ports": [ - "1433/tcp" - ], - "env": [ - { - "name": "ACCEPT_EULA", - "set": "Y" - }, - { - "name": "SA_PASSWORD", - "label": "SA password" - } - ] - }, - { - "type": 1, - "title": "SQL Server", - "description": "Microsoft SQL Server Developer for Windows containers", - "categories": ["database"], - "platform": "windows", - "note": "Password needs to include at least 8 characters including uppercase, lowercase letters, base-10 digits and/or non-alphanumeric symbols.", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png", - "image": "microsoft/mssql-server-windows-developer:latest", - "ports": [ - "1433/tcp" - ], - "env": [ - { - "name": "ACCEPT_EULA", - "set": "Y" - }, - { - "name": "sa_password", - "label": "SA password" - } - ], - "volumes": [{"container": "C:/temp/"}] - }, - { - "type": 1, - "title": "SQL Server Express", - "description": "Microsoft SQL Server Express for Windows containers", - "categories": ["database"], - "platform": "windows", - "note": "Password needs to include at least 8 characters including uppercase, lowercase letters, base-10 digits and/or non-alphanumeric symbols.", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png", - "image": "microsoft/mssql-server-windows-express:latest", - "ports": [ - "1433/tcp" - ], - "env": [ - { - "name": "ACCEPT_EULA", - "set": "Y" - }, - { - "name": "sa_password", - "label": "SA password" - } - ], - "volumes": [{"container": "C:/temp/"}] - }, - { - "type": 1, - "title": "IronFunctions API", - "description": "Open-source serverless computing platform", - "categories": ["serverless"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ironfunctions.png", - "image": "iron/functions:latest", - "ports": [ - "8080/tcp" - ], - "volumes": [{"container": "/app/data"}], - "privileged": true - }, - { - "type": 1, - "title": "IronFunctions UI", - "description": "Open-source user interface for IronFunctions", - "categories": ["serverless"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ironfunctions.png", - "image": "iron/functions-ui:latest", - "ports": [ - "4000/tcp" - ], - "volumes": [{"container": "/app/data"}], - "env": [ - { - "name": "API_URL", - "label": "API URL" - } - ], - "privileged": true - }, - { - "type": 1, - "title": "Solr", - "description": "Open-source enterprise search platform", - "categories": ["search-engine"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/solr.png", - "image": "solr:latest", - "ports": [ - "8983/tcp" - ], - "volumes": [{"container": "/opt/solr/mydata"}] - }, - { - "type": 1, - "title": "Redis", - "description": "Open-source in-memory data structure store", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/redis.png", - "image": "redis:latest", - "ports": [ - "6379/tcp" - ], - "volumes": [{"container": "/data"}] - }, - { - "type": 1, - "title": "RabbitMQ", - "description": "Highly reliable enterprise messaging system", - "categories": ["messaging"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/rabbitmq.png", - "image": "rabbitmq:latest", - "ports": [ - "5671/tcp", - "5672/tcp" - ], - "volumes": [{"container": "/var/lib/rabbitmq"}] - }, - { - "type": 1, - "title": "Ghost", - "description": "Free and open-source blogging platform", - "categories": ["blog"], - "note": "Access the blog management interface under /ghost/.", - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ghost.png", - "image": "ghost:latest", - "ports": [ - "2368/tcp" - ], - "volumes": [{"container": "/var/lib/ghost/content"}] - }, - { - "type": 1, - "title": "Plesk", - "description": "WebOps platform and hosting control panel", - "categories": ["CMS"], - "platform": "linux", - "note": "Default credentials: admin / changeme", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/plesk.png", - "image": "plesk/plesk:preview", - "ports": [ - "21/tcp", "80/tcp", "443/tcp", "8880/tcp", "8443/tcp", "8447/tcp" - ] - }, - { - "type": 1, - "title": "Joomla", - "description": "Another free and open-source CMS", - "categories": ["CMS"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/joomla.png", - "image": "joomla:latest", - "env": [ - { - "name": "JOOMLA_DB_HOST", - "label": "MySQL database host", - "type": "container" - }, - { - "name": "JOOMLA_DB_PASSWORD", - "label": "Database password" - } - ], - "ports": [ - "80/tcp" - ], - "volumes": [{"container": "/var/www/html"}] - }, - { - "type": 1, - "title": "Drupal", - "description": "Open-source content management framework", - "categories": ["CMS"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/drupal.png", - "image": "drupal:latest", - "ports": [ - "80/tcp" - ], - "volumes": [{"container": "/var/www/html"}] - }, - { - "type": 1, - "title": "Plone", - "description": "A free and open-source CMS built on top of Zope", - "categories": ["CMS"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/plone.png", - "image": "plone:latest", - "ports": [ - "8080/tcp" - ], - "volumes": [{"container": "/data"}] - }, - { - "type": 1, - "title": "Magento 2", - "description": "Open-source e-commerce platform", - "categories": ["CMS"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/magento.png", - "image": "alankent/gsd:latest", - "ports": [ - "80/tcp", - "3000/tcp", - "3001/tcp" - ], - "volumes": [{"container": "/var/www/html/"}] - }, - { - "type": 1, - "title": "Sematext Docker Agent", - "description": "Collect logs, metrics and docker events", - "categories": ["Log Management", "Monitoring"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/sematext_agent.png", - "image": "sematext/sematext-agent-docker:latest", - "name": "sematext-agent", - "privileged": true, - "env": [ - { - "name": "LOGSENE_TOKEN", - "label": "Logs token" - }, - { - "name": "SPM_TOKEN", - "label": "SPM monitoring token" - } - ], - "volumes": [ - { - "container": "/var/run/docker.sock", - "bind": "/var/run/docker.sock" - } - ] - }, - { - "type": 1, - "title": "Datadog agent", - "description": "Collect events and metrics", - "categories": ["Monitoring"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/datadog_agent.png", - "image": "datadog/agent:latest", - "env": [ - { - "name": "DD_API_KEY", - "label": "Datadog API key" - } - ], - "volumes": [ - { - "container": "/var/run/docker.sock", - "bind": "/var/run/docker.sock", - "readonly": true - }, - { - "container": "/host/sys/fs/cgroup", - "bind": "/sys/fs/cgroup", - "readonly": true - }, - { - "container": "/host/proc", - "bind": "/proc", - "readonly": true - } - ] - }, - { - "type": 1, - "title": "Mautic", - "description": "Open-source marketing automation platform", - "categories": ["marketing"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/mautic.png", - "image": "mautic/mautic:latest", - "env": [ - { - "name": "MAUTIC_DB_HOST", - "label": "MySQL database host", - "type": "container" - }, - { - "name": "MAUTIC_DB_PASSWORD", - "label": "Database password" - } - ], - "ports": [ - "80/tcp" - ], - "volumes": [{"container": "/var/www/html"}] - }, - { - "type": 1, - "title": "Wowza", - "description": "Streaming media server", - "categories": ["streaming"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/wowza.png", - "image": "sameersbn/wowza:4.1.2-8", - "env": [ - { - "name": "WOWZA_ACCEPT_LICENSE", - "label": "Agree to Wowza EULA", - "set": "yes" - }, - { - "name": "WOWZA_KEY", - "label": "License key" - } - ], - "ports": [ - "1935/tcp", - "8086/tcp", - "8087/tcp", - "8088/tcp" - ], - "volumes": [{"container": "/var/lib/wowza"}] - }, - { - "type": 1, - "title": "Jenkins", - "description": "Open-source continuous integration tool", - "categories": ["continuous-integration"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/jenkins.png", - "image": "jenkins/jenkins:lts", - "ports": [ - "8080/tcp", - "50000/tcp" - ], - "env": [ - { - "name": "JENKINS_OPTS", - "label": "Jenkins options" - } - ], - "volumes": [{"container": "/var/jenkins_home"}] - }, - { - "type": 1, - "title": "Redmine", - "description": "Open-source project management tool", - "categories": ["project-management"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/redmine.png", - "image": "redmine:latest", - "ports": [ - "3000/tcp" - ], - "volumes": [{"container": "/usr/src/redmine/files"}] - }, - { - "type": 1, - "title": "Odoo", - "description": "Open-source business apps", - "categories": ["project-management"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/odoo.png", - "image": "odoo:latest", - "env": [ - { - "name": "HOST", - "label": "PostgreSQL database host", - "type": "container" - }, - { - "name": "USER", - "label": "Database user" - }, - { - "name": "PASSWORD", - "label": "Database password" - } - ], - "ports": [ - "8069/tcp" - ], - "volumes": [{"container": "/var/lib/odoo"}, {"container": "/mnt/extra-addons"}] - }, - { - "type": 1, - "title": "Urbackup", - "description": "Open-source network backup", - "categories": ["backup"], - "platform": "linux", - "note": "This application web interface is exposed on the port 55414 inside the container.", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/urbackup.png", - "image": "cfstras/urbackup", - "ports": [ - "55413/tcp", "55414/tcp", "55415/tcp", "35622/tcp" - ], - "volumes": [{"container": "/var/urbackup"}] - }, - { - "type": 1, - "title": "File browser", - "description": "A web file manager", - "note": "Default credentials: admin/admin", - "categories": ["filesystem", "storage"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/filebrowser.png", - "image": "filebrowser/filebrowser:latest", - "ports": [ - "80/tcp" - ], - "volumes": [{"container": "/data"}, {"container": "/srv"}], - "command": "--port 80 --database /data/database.db --scope /srv" - }, - { - "type": 1, - "title": "CommandBox", - "description": "ColdFusion (CFML) CLI", - "categories": ["development"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ortussolutions-commandbox.png", - "image": "ortussolutions/commandbox:latest", - "env": [ - { - "name": "CFENGINE", - "set": "lucee@4.5" - } - ], - "ports": [ - "8080/tcp", "8443/tcp" - ] - }, - { - "type": 1, - "title": "ContentBox", - "description": "Open-source modular CMS", - "categories": ["CMS"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ortussolutions-contentbox.png", - "image": "ortussolutions/contentbox:latest", - "env": [ - { - "name": "express", - "set": "true" - }, - { - "name": "install", - "set": "true" - }, - { - "name": "CFENGINE", - "set": "lucee@4.5" - } - ], - "ports": [ - "8080/tcp", "8443/tcp" - ], - "volumes": [{"container": "/data/contentbox/db"}, {"container": "/app/includes/shared/media"}] - }, - { - "type": 2, - "title": "Portainer Agent", - "description": "Manage all the resources in your Swarm cluster", - "note": "The agent will be deployed globally inside your cluster and available on port 9001.", - "categories": ["portainer"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/portainer.png", - "repository": { - "url": "https://github.com/portainer/templates", - "stackfile": "stacks/portainer-agent/docker-stack.yml" - } - }, - { - "type": 2, - "title": "OpenFaaS", - "name": "func", - "description": "Serverless functions made simple", - "note": "Deploys the API gateway and sample functions. You can access the UI on port 8080. Warning: the name of the stack must be 'func'.", - "categories": ["serverless"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/openfaas.png", - "repository": { - "url": "https://github.com/openfaas/faas", - "stackfile": "docker-compose.yml" - } - }, - { - "type": 2, - "title": "IronFunctions", - "description": "Open-source serverless computing platform", - "note": "Deploys the IronFunctions API and UI.", - "categories": ["serverless"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/ironfunctions.png", - "repository": { - "url": "https://github.com/portainer/templates", - "stackfile": "stacks/ironfunctions/docker-stack.yml" - } - }, - { - "type": 2, - "title": "CockroachDB", - "description": "CockroachDB cluster", - "note": "Deploys an insecure CockroachDB cluster, please refer to CockroachDB documentation for production deployments.", - "categories": ["database"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/cockroachdb.png", - "repository": { - "url": "https://github.com/portainer/templates", - "stackfile": "stacks/cockroachdb/docker-stack.yml" - } - }, - { - "type": 2, - "title": "Wordpress", - "description": "Wordpress setup with a MySQL database", - "note": "Deploys a Wordpress instance connected to a MySQL database.", - "categories": ["CMS"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/wordpress.png", - "repository": { - "url": "https://github.com/portainer/templates", - "stackfile": "stacks/wordpress/docker-stack.yml" - }, - "env": [ - { - "name": "MYSQL_DATABASE_PASSWORD", - "label": "Database root password", - "description": "Password used by the MySQL root user." - } - ] - }, - { - "type": 3, - "title": "Wordpress", - "description": "Wordpress setup with a MySQL database", - "note": "Deploys a Wordpress instance connected to a MySQL database.", - "categories": ["CMS"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/wordpress.png", - "repository": { - "url": "https://github.com/portainer/templates", - "stackfile": "stacks/wordpress/docker-compose.yml" - }, - "env": [ - { - "name": "MYSQL_DATABASE_PASSWORD", - "label": "Database root password", - "description": "Password used by the MySQL root user." - } - ] - }, - { - "type": 2, - "title": "Microsoft OMS Agent", - "description": "Microsoft Operations Management Suite Linux agent.", - "categories": ["OPS"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/microsoft.png", - "repository": { - "url": "https://github.com/portainer/templates", - "stackfile": "stacks/microsoft-oms/docker-stack.yml" - }, - "env": [ - { - "name": "AZURE_WORKSPACE_ID", - "label": "Workspace ID", - "description": "Azure Workspace ID" - }, - { - "name": "AZURE_PRIMARY_KEY", - "label": "Primary key", - "description": "Azure primary key" - } - ] - }, - { - "title": "Sematext Docker Agent", - "type": 2, - "categories": ["Log Management", "Monitoring"], - "description": "Collect logs, metrics and docker events", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/sematext_agent.png", - "platform": "linux", - "repository": { - "url": "https://github.com/portainer/templates", - "stackfile": "stacks/sematext-agent-docker/docker-stack.yml" - }, - "env": [ - { - "name": "LOGSENE_TOKEN", - "label": "Logs token" - }, - { - "name": "SPM_TOKEN", - "label": "SPM monitoring token" - } - ] - }, - { - "title": "Datadog agent", - "type": 2, - "categories": ["Monitoring"], - "description": "Collect events and metrics", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/datadog_agent.png", - "platform": "linux", - "repository": { - "url": "https://github.com/portainer/templates", - "stackfile": "stacks/datadog-agent/docker-stack.yml" - }, - "env": [ - { - "name": "API_KEY", - "label": "Datadog API key" - } - ] - }, - { - "type": 1, - "title": "Sonatype Nexus3", - "description": "Sonatype Nexus3 registry manager", - "categories": ["docker"], - "platform": "linux", - "logo": "https://portainer-io-assets.sfo2.digitaloceanspaces.com/logos/sonatype.png", - "image": "sonatype/nexus3:latest", - "ports": [ - "8081/tcp" - ], - "volumes": [{ "container": "/nexus-data"}] - } -] From 8986e284fd1ca0a16faeee67f8ed99d1e36cdaae Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 29 Apr 2020 14:25:25 +1200 Subject: [PATCH 007/195] feat(api): bump ldap library version (#3772) * feat(api): bump ldap library version * feat(api): fix ldap v3 import --- api/go.mod | 5 +---- api/go.sum | 13 ++----------- api/ldap/ldap.go | 3 +-- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/api/go.mod b/api/go.mod index 773fbff55..c8b71965a 100644 --- a/api/go.mod +++ b/api/go.mod @@ -13,6 +13,7 @@ require ( github.com/docker/cli v0.0.0-20191126203649-54d085b857e9 github.com/docker/docker v0.0.0-00010101000000-000000000000 github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 + github.com/go-ldap/ldap/v3 v3.1.8 github.com/gofrs/uuid v3.2.0+incompatible github.com/gorilla/mux v1.7.3 github.com/gorilla/securecookie v1.1.1 @@ -30,11 +31,7 @@ require ( github.com/robfig/cron/v3 v3.0.0 golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 gopkg.in/alecthomas/kingpin.v2 v2.2.6 - gopkg.in/asn1-ber.v1 v1.0.0-00010101000000-000000000000 // indirect - gopkg.in/ldap.v2 v2.5.1 gopkg.in/src-d/go-git.v4 v4.13.1 ) replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 - -replace gopkg.in/asn1-ber.v1 => github.com/go-asn1-ber/asn1-ber v1.3.1 diff --git a/api/go.sum b/api/go.sum index 0d61838ad..621d3a831 100644 --- a/api/go.sum +++ b/api/go.sum @@ -50,12 +50,8 @@ github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BU github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= -github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292 h1:qQ7mw+CVWpRj5DWBL4CVHtBbGQdlPCj4j1evDh0ethw= -github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 h1:QeBh8wW8pIZKlXxlMOQ8hSCMdJA+2Z/bD/iDyCAS8XU= github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= -github.com/docker/engine v1.13.1 h1:Cks33UT9YBW5Xyc3MtGDq2IPgqfJtJ+qkFaxc2b0Euc= -github.com/docker/engine v1.13.1/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY= github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o= github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA= @@ -77,6 +73,8 @@ github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM= +github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -140,8 +138,6 @@ github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3Zk github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/microsoft/go-winio v0.4.8 h1:N4SmTFXUK7/jnn/UG/gm2mrHiYu9LVGvtsvULyody/c= -github.com/microsoft/go-winio v0.4.8/go.mod h1:kcIxxtKZE55DEncT/EOvFiygPobhUWpSDqDb47poQOU= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -175,7 +171,6 @@ github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yH github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0= -github.com/portainer/portainer v0.10.1 h1:I8K345CjGWfUGsVA8c8/gqamwLCC6CIAjxZXSklAFq0= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= @@ -248,8 +243,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ= -golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -269,8 +262,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU= -gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk= gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go index 11fe22f8f..c35f3a422 100644 --- a/api/ldap/ldap.go +++ b/api/ldap/ldap.go @@ -4,10 +4,9 @@ import ( "fmt" "strings" + ldap "github.com/go-ldap/ldap/v3" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" - - "gopkg.in/ldap.v3" ) const ( From d202660bb88110bc2a8bd108414c28e6a4e42984 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 11 May 2020 11:27:09 +1200 Subject: [PATCH 008/195] feat(project): remove pulldog --- pull-dog.json | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 pull-dog.json diff --git a/pull-dog.json b/pull-dog.json deleted file mode 100644 index eaeed2e08..000000000 --- a/pull-dog.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "dockerComposeYmlFilePaths": ["docker-compose.pull-dog.yml"], - "isLazy": true -} From 4fdb0934cbfe4abf05f3ffc470a6aa2fa37c8ad4 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 11 May 2020 12:36:30 +1200 Subject: [PATCH 009/195] feat(cli): remove the --no-snapshot CLI flag (#3814) --- api/cli/cli.go | 9 --------- api/cli/defaults.go | 1 - api/cli/defaults_windows.go | 1 - api/cmd/portainer/main.go | 13 +++++-------- api/portainer.go | 2 -- app/portainer/services/stateManager.js | 1 - 6 files changed, 5 insertions(+), 22 deletions(-) diff --git a/api/cli/cli.go b/api/cli/cli.go index 91d4b3f47..88d73b411 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -50,7 +50,6 @@ 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(), SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source (deprecated)").Default(defaultSyncInterval).String(), - Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots (deprecated)").Default(defaultSnapshot).Bool(), SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(), AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(), AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), @@ -124,14 +123,6 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) { if *flags.NoAuth { log.Println("Warning: the --no-auth flag is deprecated and will likely be removed in a future version of Portainer.") } - - if !*flags.Snapshot { - log.Println("Warning: the --no-snapshot flag is deprecated and will likely be removed in a future version of Portainer.") - } - - if *flags.TemplateFile != "" { - log.Println("Warning: the --template-file flag is deprecated and will likely be removed in a future version of Portainer.") - } } func validateEndpointURL(endpointURL string) error { diff --git a/api/cli/defaults.go b/api/cli/defaults.go index f644e1ff6..40e29292f 100644 --- a/api/cli/defaults.go +++ b/api/cli/defaults.go @@ -19,6 +19,5 @@ const ( defaultSSLCertPath = "/certs/portainer.crt" defaultSSLKeyPath = "/certs/portainer.key" defaultSyncInterval = "60s" - defaultSnapshot = "true" defaultSnapshotInterval = "5m" ) diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go index b9e13d571..5fd6b10a7 100644 --- a/api/cli/defaults_windows.go +++ b/api/cli/defaults_windows.go @@ -17,6 +17,5 @@ const ( defaultSSLCertPath = "C:\\certs\\portainer.crt" defaultSSLKeyPath = "C:\\certs\\portainer.key" defaultSyncInterval = "60s" - defaultSnapshot = "true" defaultSnapshotInterval = "5m" ) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 795c2b473..10010050a 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -225,12 +225,11 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p return nil } -func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *portainer.Status { +func initStatus(endpointManagement bool, flags *portainer.CLIFlags) *portainer.Status { return &portainer.Status{ Analytics: !*flags.NoAnalytics, Authentication: !*flags.NoAuth, EndpointManagement: endpointManagement, - Snapshot: snapshot, Version: portainer.APIVersion, } } @@ -492,16 +491,14 @@ func main() { log.Fatal(err) } - if *flags.Snapshot { - err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService) - if err != nil { - log.Fatal(err) - } + err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService) + if err != nil { + log.Fatal(err) } jobScheduler.Start() - applicationStatus := initStatus(endpointManagement, *flags.Snapshot, flags) + applicationStatus := initStatus(endpointManagement, flags) err = initEndpoint(flags, store.EndpointService, snapshotter) if err != nil { diff --git a/api/portainer.go b/api/portainer.go index c1f4f9d47..ee1ff157f 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -57,7 +57,6 @@ type ( SSLCert *string SSLKey *string SyncInterval *string - Snapshot *bool SnapshotInterval *string } @@ -493,7 +492,6 @@ type ( Status struct { Authentication bool `json:"Authentication"` EndpointManagement bool `json:"EndpointManagement"` - Snapshot bool `json:"Snapshot"` Analytics bool `json:"Analytics"` Version string `json:"Version"` } diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 1fda3ae68..e65c8d224 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -80,7 +80,6 @@ angular.module('portainer.app').factory('StateManager', [ state.application.authentication = status.Authentication; state.application.analytics = status.Analytics; state.application.endpointManagement = status.EndpointManagement; - state.application.snapshot = status.Snapshot; state.application.version = status.Version; state.application.logo = settings.LogoURL; state.application.snapshotInterval = settings.SnapshotInterval; From d9665bc939c293cfaec307b28ace624347885992 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 18 May 2020 12:20:27 +1200 Subject: [PATCH 010/195] fix(api): update to template file format for Edge templates --- .../edgetemplates/edgetemplate_list.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/api/http/handler/edgetemplates/edgetemplate_list.go b/api/http/handler/edgetemplates/edgetemplate_list.go index f2ec34a4a..2afb90f6d 100644 --- a/api/http/handler/edgetemplates/edgetemplate_list.go +++ b/api/http/handler/edgetemplates/edgetemplate_list.go @@ -2,7 +2,6 @@ package edgetemplates import ( "encoding/json" - "log" "net/http" httperror "github.com/portainer/libhttp/error" @@ -11,6 +10,11 @@ import ( "github.com/portainer/portainer/api/http/client" ) +type templateFileFormat struct { + Version string `json:"version"` + Templates []portainer.Template `json:"templates"` +} + // GET request on /api/edgetemplates func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { settings, err := handler.SettingsService.Settings() @@ -24,22 +28,21 @@ func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) } var templateData []byte - templateData, err = client.Get(url, 0) + templateData, err = client.Get(url, 10) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err} } - var templates []portainer.Template + var templateFile templateFileFormat - err = json.Unmarshal(templateData, &templates) + err = json.Unmarshal(templateData, &templateFile) if err != nil { - log.Printf("[DEBUG] [http,edge,templates] [failed parsing edge templates] [body: %s]", templateData) - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external templates", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse template file", err} } - filteredTemplates := []portainer.Template{} + filteredTemplates := make([]portainer.Template, 0) - for _, template := range templates { + for _, template := range templateFile.Templates { if template.Type == portainer.EdgeStackTemplate { filteredTemplates = append(filteredTemplates, template) } From c074a714cfab4ecadb4d4a816088bc11ec477465 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 18 May 2020 11:29:37 +0300 Subject: [PATCH 011/195] feat(server): remove external endpoint feature (#3837) * fix(prettier): auto format html files (#3836) * refactor(main): remove reference to external endpoints * refactor(cli): remove parsing of external endpoints param * refactor(portainer): remove types for external endpoints * refactor(endpoints): remove warning for external endpoints * refactor(endpoints): remove endpoint management setting * refactor(endpoints): remove ref to endpoint management * fix(main): remove endpoint management --- api/cli/cli.go | 48 ---- api/cli/defaults.go | 1 - api/cli/defaults_windows.go | 1 - api/cmd/portainer/main.go | 61 +---- api/cron/job_endpoint_sync.go | 214 ------------------ api/http/handler/endpoints/endpoint_create.go | 4 - api/http/handler/endpoints/endpoint_delete.go | 4 - api/http/handler/endpoints/endpoint_update.go | 4 - api/http/handler/endpoints/handler.go | 10 +- api/http/server.go | 3 +- api/portainer.go | 17 +- api/swagger.yaml | 4 - .../endpointsDatatable.html | 9 +- .../endpoints-datatable/endpointsDatatable.js | 1 - app/portainer/models/status.js | 1 - app/portainer/services/stateManager.js | 1 - .../endpoints/edit/endpointController.js | 4 - app/portainer/views/endpoints/endpoints.html | 15 -- 18 files changed, 20 insertions(+), 382 deletions(-) delete mode 100644 api/cron/job_endpoint_sync.go diff --git a/api/cli/cli.go b/api/cli/cli.go index 88d73b411..57e874bb3 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -20,9 +20,7 @@ const ( errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://") errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe") errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file") - errInvalidSyncInterval = portainer.Error("Invalid synchronization interval") errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval") - errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints") errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file") errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file") ) @@ -38,7 +36,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), - ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints (deprecated)").String(), NoAuth: kingpin.Flag("no-auth", "Disable authentication (deprecated)").Default(defaultNoAuth).Bool(), NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(), TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), @@ -49,7 +46,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(), 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(), - SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source (deprecated)").Default(defaultSyncInterval).String(), SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(), AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(), AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), @@ -76,25 +72,11 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { displayDeprecationWarnings(flags) - if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" { - return errEndpointExcludeExternal - } - err := validateEndpointURL(*flags.EndpointURL) if err != nil { return err } - err = validateExternalEndpoints(*flags.ExternalEndpoints) - if err != nil { - return err - } - - err = validateSyncInterval(*flags.SyncInterval) - if err != nil { - return err - } - err = validateSnapshotInterval(*flags.SnapshotInterval) if err != nil { return err @@ -112,14 +94,6 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { } func displayDeprecationWarnings(flags *portainer.CLIFlags) { - if *flags.ExternalEndpoints != "" { - log.Println("Warning: the --external-endpoint flag is deprecated and will likely be removed in a future version of Portainer.") - } - - if *flags.SyncInterval != defaultSyncInterval { - log.Println("Warning: the --sync-interval flag is deprecated and will likely be removed in a future version of Portainer.") - } - if *flags.NoAuth { log.Println("Warning: the --no-auth flag is deprecated and will likely be removed in a future version of Portainer.") } @@ -145,28 +119,6 @@ func validateEndpointURL(endpointURL string) error { return nil } -func validateExternalEndpoints(externalEndpoints string) error { - if externalEndpoints != "" { - if _, err := os.Stat(externalEndpoints); err != nil { - if os.IsNotExist(err) { - return errEndpointsFileNotFound - } - return err - } - } - return nil -} - -func validateSyncInterval(syncInterval string) error { - if syncInterval != defaultSyncInterval { - _, err := time.ParseDuration(syncInterval) - if err != nil { - return errInvalidSyncInterval - } - } - return nil -} - func validateSnapshotInterval(snapshotInterval string) error { if snapshotInterval != defaultSnapshotInterval { _, err := time.ParseDuration(snapshotInterval) diff --git a/api/cli/defaults.go b/api/cli/defaults.go index 40e29292f..b79cddc5a 100644 --- a/api/cli/defaults.go +++ b/api/cli/defaults.go @@ -18,6 +18,5 @@ const ( defaultSSL = "false" defaultSSLCertPath = "/certs/portainer.crt" defaultSSLKeyPath = "/certs/portainer.key" - defaultSyncInterval = "60s" defaultSnapshotInterval = "5m" ) diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go index 5fd6b10a7..2d1f2b2ab 100644 --- a/api/cli/defaults_windows.go +++ b/api/cli/defaults_windows.go @@ -16,6 +16,5 @@ const ( defaultSSL = "false" defaultSSLCertPath = "C:\\certs\\portainer.crt" defaultSSLKeyPath = "C:\\certs\\portainer.key" - defaultSyncInterval = "60s" defaultSnapshotInterval = "5m" ) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 10010050a..829d6b0b7 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -157,45 +157,6 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter return nil } -func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, flags *portainer.CLIFlags) error { - if *flags.ExternalEndpoints == "" { - return nil - } - - log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.") - - schedules, err := scheduleService.SchedulesByJobType(portainer.EndpointSyncJobType) - if err != nil { - return err - } - - if len(schedules) != 0 { - return nil - } - - endpointSyncJob := &portainer.EndpointSyncJob{} - - endpointSyncSchedule := &portainer.Schedule{ - ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()), - Name: "system_endpointsync", - CronExpression: "@every " + *flags.SyncInterval, - Recurring: true, - JobType: portainer.EndpointSyncJobType, - EndpointSyncJob: endpointSyncJob, - Created: time.Now().Unix(), - } - - endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints) - endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endpointSyncSchedule, endpointSyncJobContext) - - err = jobScheduler.ScheduleJob(endpointSyncJobRunner) - if err != nil { - return err - } - - return scheduleService.CreateSchedule(endpointSyncSchedule) -} - func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error { schedules, err := scheduleService.Schedules() if err != nil { @@ -225,12 +186,11 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p return nil } -func initStatus(endpointManagement bool, flags *portainer.CLIFlags) *portainer.Status { +func initStatus(flags *portainer.CLIFlags) *portainer.Status { return &portainer.Status{ - Analytics: !*flags.NoAnalytics, - Authentication: !*flags.NoAuth, - EndpointManagement: endpointManagement, - Version: portainer.APIVersion, + Analytics: !*flags.NoAnalytics, + Authentication: !*flags.NoAuth, + Version: portainer.APIVersion, } } @@ -460,11 +420,6 @@ func main() { snapshotter := initSnapshotter(clientFactory) - endpointManagement := true - if *flags.ExternalEndpoints != "" { - endpointManagement = false - } - swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService) if err != nil { log.Fatal(err) @@ -486,11 +441,6 @@ func main() { log.Fatal(err) } - err = loadEndpointSyncSystemSchedule(jobScheduler, store.ScheduleService, store.EndpointService, flags) - if err != nil { - log.Fatal(err) - } - err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService) if err != nil { log.Fatal(err) @@ -498,7 +448,7 @@ func main() { jobScheduler.Start() - applicationStatus := initStatus(endpointManagement, flags) + applicationStatus := initStatus(flags) err = initEndpoint(flags, store.EndpointService, snapshotter) if err != nil { @@ -557,7 +507,6 @@ func main() { BindAddress: *flags.Addr, AssetsPath: *flags.Assets, AuthDisabled: *flags.NoAuth, - EndpointManagement: endpointManagement, RoleService: store.RoleService, UserService: store.UserService, TeamService: store.TeamService, diff --git a/api/cron/job_endpoint_sync.go b/api/cron/job_endpoint_sync.go deleted file mode 100644 index 361698649..000000000 --- a/api/cron/job_endpoint_sync.go +++ /dev/null @@ -1,214 +0,0 @@ -package cron - -import ( - "encoding/json" - "io/ioutil" - "log" - "strings" - - "github.com/portainer/portainer/api" -) - -// EndpointSyncJobRunner is used to run a EndpointSyncJob -type EndpointSyncJobRunner struct { - schedule *portainer.Schedule - context *EndpointSyncJobContext -} - -// EndpointSyncJobContext represents the context of execution of a EndpointSyncJob -type EndpointSyncJobContext struct { - endpointService portainer.EndpointService - endpointFilePath string -} - -// NewEndpointSyncJobContext returns a new context that can be used to execute a EndpointSyncJob -func NewEndpointSyncJobContext(endpointService portainer.EndpointService, endpointFilePath string) *EndpointSyncJobContext { - return &EndpointSyncJobContext{ - endpointService: endpointService, - endpointFilePath: endpointFilePath, - } -} - -// NewEndpointSyncJobRunner returns a new runner that can be scheduled -func NewEndpointSyncJobRunner(schedule *portainer.Schedule, context *EndpointSyncJobContext) *EndpointSyncJobRunner { - return &EndpointSyncJobRunner{ - schedule: schedule, - context: context, - } -} - -type synchronization struct { - endpointsToCreate []*portainer.Endpoint - endpointsToUpdate []*portainer.Endpoint - endpointsToDelete []*portainer.Endpoint -} - -type fileEndpoint struct { - Name string `json:"Name"` - URL string `json:"URL"` - TLS bool `json:"TLS,omitempty"` - TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"` - TLSCACert string `json:"TLSCACert,omitempty"` - TLSCert string `json:"TLSCert,omitempty"` - TLSKey string `json:"TLSKey,omitempty"` -} - -// GetSchedule returns the schedule associated to the runner -func (runner *EndpointSyncJobRunner) GetSchedule() *portainer.Schedule { - return runner.schedule -} - -// Run triggers the execution of the endpoint synchronization process. -func (runner *EndpointSyncJobRunner) Run() { - data, err := ioutil.ReadFile(runner.context.endpointFilePath) - if endpointSyncError(err) { - return - } - - var fileEndpoints []fileEndpoint - err = json.Unmarshal(data, &fileEndpoints) - if endpointSyncError(err) { - return - } - - if len(fileEndpoints) == 0 { - log.Println("background job error (endpoint synchronization). External endpoint source is empty") - return - } - - storedEndpoints, err := runner.context.endpointService.Endpoints() - if endpointSyncError(err) { - return - } - - convertedFileEndpoints := convertFileEndpoints(fileEndpoints) - - sync := prepareSyncData(storedEndpoints, convertedFileEndpoints) - if sync.requireSync() { - err = runner.context.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete) - if endpointSyncError(err) { - return - } - log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete)) - } -} - -func endpointSyncError(err error) bool { - if err != nil { - log.Printf("background job error (endpoint synchronization). Unable to synchronize endpoints (err=%s)\n", err) - return true - } - return false -} - -func isValidEndpoint(endpoint *portainer.Endpoint) bool { - if endpoint.Name != "" && endpoint.URL != "" { - if !strings.HasPrefix(endpoint.URL, "unix://") && !strings.HasPrefix(endpoint.URL, "tcp://") { - return false - } - return true - } - return false -} - -func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint { - convertedEndpoints := make([]portainer.Endpoint, 0) - - for _, e := range fileEndpoints { - endpoint := portainer.Endpoint{ - Name: e.Name, - URL: e.URL, - TLSConfig: portainer.TLSConfiguration{}, - } - if e.TLS { - endpoint.TLSConfig.TLS = true - endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify - endpoint.TLSConfig.TLSCACertPath = e.TLSCACert - endpoint.TLSConfig.TLSCertPath = e.TLSCert - endpoint.TLSConfig.TLSKeyPath = e.TLSKey - } - convertedEndpoints = append(convertedEndpoints, endpoint) - } - - return convertedEndpoints -} - -func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int { - for idx, v := range endpoints { - if endpoint.Name == v.Name && isValidEndpoint(&v) { - return idx - } - } - return -1 -} - -func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint { - var endpoint *portainer.Endpoint - if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS || - (updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) || - (updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) || - (updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) || - (updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) { - endpoint = original - endpoint.URL = updated.URL - if updated.TLSConfig.TLS { - endpoint.TLSConfig.TLS = true - endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify - endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath - endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath - endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath - } else { - endpoint.TLSConfig.TLS = false - endpoint.TLSConfig.TLSSkipVerify = false - endpoint.TLSConfig.TLSCACertPath = "" - endpoint.TLSConfig.TLSCertPath = "" - endpoint.TLSConfig.TLSKeyPath = "" - } - } - return endpoint -} - -func (sync synchronization) requireSync() bool { - if len(sync.endpointsToCreate) != 0 || len(sync.endpointsToUpdate) != 0 || len(sync.endpointsToDelete) != 0 { - return true - } - return false -} - -func prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization { - endpointsToCreate := make([]*portainer.Endpoint, 0) - endpointsToUpdate := make([]*portainer.Endpoint, 0) - endpointsToDelete := make([]*portainer.Endpoint, 0) - - for idx := range storedEndpoints { - fidx := endpointExists(&storedEndpoints[idx], fileEndpoints) - if fidx != -1 { - endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx]) - if endpoint != nil { - log.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL) - endpointsToUpdate = append(endpointsToUpdate, endpoint) - } - } else { - log.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL) - endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx]) - } - } - - for idx, endpoint := range fileEndpoints { - if !isValidEndpoint(&endpoint) { - log.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL) - continue - } - sidx := endpointExists(&fileEndpoints[idx], storedEndpoints) - if sidx == -1 { - log.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL) - endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx]) - } - } - - return &synchronization{ - endpointsToCreate: endpointsToCreate, - endpointsToUpdate: endpointsToUpdate, - endpointsToDelete: endpointsToDelete, - } -} diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index b7d08f320..f037c88ab 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -131,10 +131,6 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { // POST request on /api/endpoints func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - if !handler.authorizeEndpointManagement { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled} - } - payload := &endpointCreatePayload{} err := payload.Validate(r) if err != nil { diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 43b20dc78..7ef6c0a0d 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -12,10 +12,6 @@ import ( // DELETE request on /api/endpoints/:id func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - if !handler.authorizeEndpointManagement { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled} - } - endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 2cc521a47..8fe0b920c 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -35,10 +35,6 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error { // PUT request on /api/endpoints/:id func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - if !handler.authorizeEndpointManagement { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled} - } - endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index bca5dea75..f7a19d106 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -11,12 +11,6 @@ import ( "github.com/gorilla/mux" ) -const ( - // ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints - // when the server has been started with the --external-endpoints flag - ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled") -) - func hideFields(endpoint *portainer.Endpoint) { endpoint.AzureCredentials = portainer.AzureCredentials{} if len(endpoint.Snapshots) > 0 { @@ -27,7 +21,6 @@ func hideFields(endpoint *portainer.Endpoint) { // Handler is the HTTP handler used to handle endpoint operations. type Handler struct { *mux.Router - authorizeEndpointManagement bool requestBouncer *security.RequestBouncer AuthorizationService *portainer.AuthorizationService EdgeGroupService portainer.EdgeGroupService @@ -45,10 +38,9 @@ type Handler struct { } // NewHandler creates a handler to manage endpoint operations. -func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *Handler { +func NewHandler(bouncer *security.RequestBouncer) *Handler { h := &Handler{ Router: mux.NewRouter(), - authorizeEndpointManagement: authorizeEndpointManagement, requestBouncer: bouncer, } diff --git a/api/http/server.go b/api/http/server.go index 214796070..fc9d742e6 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -48,7 +48,6 @@ type Server struct { BindAddress string AssetsPath string AuthDisabled bool - EndpointManagement bool Status *portainer.Status ReverseTunnelService portainer.ReverseTunnelService ExtensionManager portainer.ExtensionManager @@ -170,7 +169,7 @@ func (server *Server) Start() error { var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer) edgeTemplatesHandler.SettingsService = server.SettingsService - var endpointHandler = endpoints.NewHandler(requestBouncer, server.EndpointManagement) + var endpointHandler = endpoints.NewHandler(requestBouncer) endpointHandler.AuthorizationService = authorizationService endpointHandler.EdgeGroupService = server.EdgeGroupService endpointHandler.EdgeStackService = server.EdgeStackService diff --git a/api/portainer.go b/api/portainer.go index ee1ff157f..2666fc24b 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -42,7 +42,6 @@ type ( Assets *string Data *string EndpointURL *string - ExternalEndpoints *string Labels *[]Pair Logo *string NoAuth *bool @@ -56,7 +55,6 @@ type ( SSL *bool SSLCert *string SSLKey *string - SyncInterval *string SnapshotInterval *string } @@ -209,6 +207,8 @@ type ( EndpointStatus int // EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file + // + // Deprecated EndpointSyncJob struct{} // EndpointType represents the type of an endpoint @@ -402,7 +402,9 @@ type ( EdgeSchedule *EdgeSchedule ScriptExecutionJob *ScriptExecutionJob SnapshotJob *SnapshotJob - EndpointSyncJob *EndpointSyncJob + + // Deprecated fields + EndpointSyncJob *EndpointSyncJob } // ScheduleID represents a schedule identifier. @@ -490,10 +492,9 @@ type ( // Status represents the application status Status struct { - Authentication bool `json:"Authentication"` - EndpointManagement bool `json:"EndpointManagement"` - Analytics bool `json:"Analytics"` - Version string `json:"Version"` + Authentication bool `json:"Authentication"` + Analytics bool `json:"Analytics"` + Version string `json:"Version"` } // Tag represents a tag that can be associated to a resource @@ -1097,7 +1098,7 @@ const ( // SnapshotJobType is a system job used to create endpoint snapshots SnapshotJobType // EndpointSyncJobType is a system job used to synchronize endpoints from - // an external definition store + // an external definition store (Deprecated) EndpointSyncJobType ) diff --git a/api/swagger.yaml b/api/swagger.yaml index 1aad1e30b..2ecd61139 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -3164,10 +3164,6 @@ definitions: type: "boolean" example: true description: "Is authentication enabled" - EndpointManagement: - type: "boolean" - example: true - description: "Is endpoint management enabled" Analytics: type: "boolean" example: true diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html index 1d57106bd..e053c0adb 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html @@ -4,7 +4,7 @@
{{ $ctrl.titleText }}
-
+
@@ -27,7 +27,7 @@ - + @@ -68,12 +68,11 @@ ng-class="{ active: item.Checked }" > - + - {{ item.Name }} - {{ item.Name }} + {{ item.Name }} diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js index d12f158a1..b717fe566 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js @@ -7,7 +7,6 @@ angular.module('portainer.app').component('endpointsDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - endpointManagement: '<', accessManagement: '<', removeAction: '<', retrievePage: '<', diff --git a/app/portainer/models/status.js b/app/portainer/models/status.js index f09b4fc58..20057e382 100644 --- a/app/portainer/models/status.js +++ b/app/portainer/models/status.js @@ -1,7 +1,6 @@ export function StatusViewModel(data) { this.Authentication = data.Authentication; this.Snapshot = data.Snapshot; - this.EndpointManagement = data.EndpointManagement; this.Analytics = data.Analytics; this.Version = data.Version; } diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index e65c8d224..fa2b1c72b 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -79,7 +79,6 @@ angular.module('portainer.app').factory('StateManager', [ function assignStateFromStatusAndSettings(status, settings) { state.application.authentication = status.Authentication; state.application.analytics = status.Analytics; - state.application.endpointManagement = status.EndpointManagement; state.application.version = status.Version; state.application.logo = settings.LogoURL; state.application.snapshotInterval = settings.SnapshotInterval; diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 0ff7590f1..c4d560645 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -19,10 +19,6 @@ angular Notifications, Authentication ) { - if (!$scope.applicationState.application.endpointManagement) { - $state.go('portainer.endpoints'); - } - $scope.state = { uploadInProgress: false, actionInProgress: false, diff --git a/app/portainer/views/endpoints/endpoints.html b/app/portainer/views/endpoints/endpoints.html index 1d6f2382f..35f0190ff 100644 --- a/app/portainer/views/endpoints/endpoints.html +++ b/app/portainer/views/endpoints/endpoints.html @@ -7,20 +7,6 @@ Endpoint management -
-
- - - - Portainer has been started using the --external-endpoints flag. Endpoint management via the UI is disabled. - You can still manage endpoint access. - - - -
-
-
Date: Tue, 19 May 2020 15:06:29 +1200 Subject: [PATCH 012/195] refactor(api): remove unused error constant --- api/cli/cli.go | 1 - 1 file changed, 1 deletion(-) diff --git a/api/cli/cli.go b/api/cli/cli.go index 57e874bb3..914f01cb4 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -19,7 +19,6 @@ type Service struct{} const ( errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://") errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe") - errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file") errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval") errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file") errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file") From 493de205406c75fea88ed0a1c5e96e544063d682 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 19 May 2020 15:08:57 +1200 Subject: [PATCH 013/195] refactor(azure): remove Azure ACI endpoint support (#3803) * feat(templates): remove template management features (#3719) * feat(api): remove template management features * feat(templates): remove template management features * refactor(azure): remove Azure ACI endpoint support --- api/cron/job_snapshot.go | 2 +- api/docker/client.go | 4 +- api/errors.go | 5 - api/go.sum | 1 + api/http/client/client.go | 52 ------ api/http/handler/endpointproxy/handler.go | 2 - api/http/handler/endpointproxy/proxy_azure.go | 43 ----- api/http/handler/endpoints/endpoint_create.go | 106 +++-------- .../handler/endpoints/endpoint_snapshot.go | 4 - .../handler/endpoints/endpoint_snapshots.go | 4 - api/http/handler/endpoints/endpoint_update.go | 48 ++--- api/http/handler/endpoints/handler.go | 1 - api/http/handler/handler.go | 2 - api/http/proxy/factory/azure.go | 20 --- api/http/proxy/factory/azure/transport.go | 80 --------- api/http/proxy/factory/factory.go | 7 - api/portainer.go | 10 +- api/swagger.yaml | 47 +---- app/__module.js | 2 - app/azure/_module.js | 51 ------ .../azure-endpoint-config.js | 8 - .../azureEndpointConfig.html | 36 ---- .../azure-sidebar-content.js | 3 - .../azureSidebarContent.html | 6 - .../containerGroupsDatatable.html | 105 ----------- .../containerGroupsDatatable.js | 13 -- app/azure/models/container_group.js | 66 ------- app/azure/models/location.js | 6 - app/azure/models/provider.js | 9 - app/azure/models/resource_group.js | 6 - app/azure/models/subscription.js | 4 - app/azure/rest/azure.js | 20 --- app/azure/rest/container_group.js | 45 ----- app/azure/rest/location.js | 18 -- app/azure/rest/provider.js | 18 -- app/azure/rest/resource_group.js | 18 -- app/azure/rest/subscription.js | 18 -- app/azure/services/azureService.js | 72 -------- app/azure/services/containerGroupService.js | 41 ----- app/azure/services/locationService.js | 29 --- app/azure/services/providerService.js | 27 --- app/azure/services/resourceGroupService.js | 29 --- app/azure/services/subscriptionService.js | 29 --- .../containerInstancesController.js | 44 ----- .../containerinstances.html | 21 --- .../createContainerInstanceController.js | 93 ---------- .../create/createcontainerinstance.html | 167 ------------------ app/azure/views/dashboard/dashboard.html | 33 ---- .../views/dashboard/dashboardController.js | 23 --- .../endpoint-item/endpointItem.html | 2 +- app/portainer/filters/filters.js | 2 - app/portainer/services/api/endpointService.js | 14 -- app/portainer/services/fileUpload.js | 16 -- app/portainer/services/stateManager.js | 8 - .../create/createEndpointController.js | 29 --- .../endpoints/create/createendpoint.html | 114 ------------ .../views/endpoints/edit/endpoint.html | 6 - .../endpoints/edit/endpointController.js | 3 - app/portainer/views/home/homeController.js | 17 +- .../views/init/endpoint/initEndpoint.html | 95 ---------- .../init/endpoint/initEndpointController.js | 26 --- app/portainer/views/sidebar/sidebar.html | 2 - assets/css/app.css | 4 - jsconfig.json | 1 - webpack/webpack.common.js | 1 - 65 files changed, 47 insertions(+), 1791 deletions(-) delete mode 100644 api/http/handler/endpointproxy/proxy_azure.go delete mode 100644 api/http/proxy/factory/azure.go delete mode 100644 api/http/proxy/factory/azure/transport.go delete mode 100644 app/azure/_module.js delete mode 100644 app/azure/components/azure-endpoint-config/azure-endpoint-config.js delete mode 100644 app/azure/components/azure-endpoint-config/azureEndpointConfig.html delete mode 100644 app/azure/components/azure-sidebar-content/azure-sidebar-content.js delete mode 100644 app/azure/components/azure-sidebar-content/azureSidebarContent.html delete mode 100644 app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html delete mode 100644 app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js delete mode 100644 app/azure/models/container_group.js delete mode 100644 app/azure/models/location.js delete mode 100644 app/azure/models/provider.js delete mode 100644 app/azure/models/resource_group.js delete mode 100644 app/azure/models/subscription.js delete mode 100644 app/azure/rest/azure.js delete mode 100644 app/azure/rest/container_group.js delete mode 100644 app/azure/rest/location.js delete mode 100644 app/azure/rest/provider.js delete mode 100644 app/azure/rest/resource_group.js delete mode 100644 app/azure/rest/subscription.js delete mode 100644 app/azure/services/azureService.js delete mode 100644 app/azure/services/containerGroupService.js delete mode 100644 app/azure/services/locationService.js delete mode 100644 app/azure/services/providerService.js delete mode 100644 app/azure/services/resourceGroupService.js delete mode 100644 app/azure/services/subscriptionService.js delete mode 100644 app/azure/views/containerinstances/containerInstancesController.js delete mode 100644 app/azure/views/containerinstances/containerinstances.html delete mode 100644 app/azure/views/containerinstances/create/createContainerInstanceController.js delete mode 100644 app/azure/views/containerinstances/create/createcontainerinstance.html delete mode 100644 app/azure/views/dashboard/dashboard.html delete mode 100644 app/azure/views/dashboard/dashboardController.js diff --git a/api/cron/job_snapshot.go b/api/cron/job_snapshot.go index 458d026c0..b3dcc9b74 100644 --- a/api/cron/job_snapshot.go +++ b/api/cron/job_snapshot.go @@ -53,7 +53,7 @@ func (runner *SnapshotJobRunner) Run() { } for _, endpoint := range endpoints { - if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentEnvironment { continue } diff --git a/api/docker/client.go b/api/docker/client.go index c1bd7a8d0..ce8d21ec1 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -35,9 +35,7 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers // a specific endpoint configuration. The nodeName parameter can be used // with an agent enabled endpoint to target a specific node in an agent cluster. func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) { - if endpoint.Type == portainer.AzureEnvironment { - return nil, unsupportedEnvironmentType - } else if endpoint.Type == portainer.AgentOnDockerEnvironment { + if endpoint.Type == portainer.AgentOnDockerEnvironment { return createAgentClient(endpoint, factory.signatureService, nodeName) } else if endpoint.Type == portainer.EdgeAgentEnvironment { return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName) diff --git a/api/errors.go b/api/errors.go index 8e09838a1..bc639d341 100644 --- a/api/errors.go +++ b/api/errors.go @@ -39,11 +39,6 @@ const ( ErrEndpointAccessDenied = Error("Access denied to endpoint") ) -// Azure environment errors -const ( - ErrAzureInvalidCredentials = Error("Invalid Azure credentials") -) - // Endpoint group errors. const ( ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group") diff --git a/api/go.sum b/api/go.sum index 621d3a831..d3eead3d3 100644 --- a/api/go.sum +++ b/api/go.sum @@ -171,6 +171,7 @@ github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yH github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0= +github.com/portainer/portainer v0.10.1 h1:I8K345CjGWfUGsVA8c8/gqamwLCC6CIAjxZXSklAFq0= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= diff --git a/api/http/client/client.go b/api/http/client/client.go index fb690105f..0e6d9d41d 100644 --- a/api/http/client/client.go +++ b/api/http/client/client.go @@ -2,12 +2,9 @@ package client import ( "crypto/tls" - "encoding/json" - "fmt" "io/ioutil" "log" "net/http" - "net/url" "strings" "time" @@ -19,55 +16,6 @@ const ( defaultHTTPTimeout = 5 ) -// HTTPClient represents a client to send HTTP requests. -type HTTPClient struct { - *http.Client -} - -// NewHTTPClient is used to build a new HTTPClient. -func NewHTTPClient() *HTTPClient { - return &HTTPClient{ - &http.Client{ - Timeout: time.Second * time.Duration(defaultHTTPTimeout), - }, - } -} - -// AzureAuthenticationResponse represents an Azure API authentication response. -type AzureAuthenticationResponse struct { - AccessToken string `json:"access_token"` - ExpiresOn string `json:"expires_on"` -} - -// ExecuteAzureAuthenticationRequest is used to execute an authentication request -// against the Azure API. It re-uses the same http.Client. -func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portainer.AzureCredentials) (*AzureAuthenticationResponse, error) { - loginURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", credentials.TenantID) - params := url.Values{ - "grant_type": {"client_credentials"}, - "client_id": {credentials.ApplicationID}, - "client_secret": {credentials.AuthenticationKey}, - "resource": {"https://management.azure.com/"}, - } - - response, err := client.PostForm(loginURL, params) - if err != nil { - return nil, err - } - - if response.StatusCode != http.StatusOK { - return nil, portainer.ErrAzureInvalidCredentials - } - - var token AzureAuthenticationResponse - err = json.NewDecoder(response.Body).Decode(&token) - if err != nil { - return nil, err - } - - return &token, nil -} - // Get executes a simple HTTP GET to the specified URL and returns // the content of the response body. Timeout can be specified via the timeout parameter, // will default to defaultHTTPTimeout if set to 0. diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index be89bb750..ed81a6527 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -24,8 +24,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), requestBouncer: bouncer, } - h.PathPrefix("/{id}/azure").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) h.PathPrefix("/{id}/docker").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) h.PathPrefix("/{id}/storidge").Handler( diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go deleted file mode 100644 index 6ffc4598a..000000000 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ /dev/null @@ -1,43 +0,0 @@ -package endpointproxy - -import ( - "strconv" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" - - "net/http" -) - -func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} - } - - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} - } - - err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) - if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} - } - - var proxy http.Handler - proxy = handler.ProxyManager.GetEndpointProxy(endpoint) - if proxy == nil { - proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err} - } - } - - id := strconv.Itoa(endpointID) - http.StripPrefix("/"+id+"/azure", proxy).ServeHTTP(w, r) - return nil -} diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index f037c88ab..cd1b84cb5 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -18,21 +18,18 @@ import ( ) type endpointCreatePayload struct { - Name string - URL string - EndpointType int - PublicURL string - GroupID int - TLS bool - TLSSkipVerify bool - TLSSkipClientVerify bool - TLSCACertFile []byte - TLSCertFile []byte - TLSKeyFile []byte - AzureApplicationID string - AzureTenantID string - AzureAuthenticationKey string - TagIDs []portainer.TagID + Name string + URL string + EndpointType int + PublicURL string + GroupID int + TLS bool + TLSSkipVerify bool + TLSSkipClientVerify bool + TLSCACertFile []byte + TLSCertFile []byte + TLSKeyFile []byte + TagIDs []portainer.TagID } func (payload *endpointCreatePayload) Validate(r *http.Request) error { @@ -44,7 +41,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false) if err != nil || endpointType == 0 { - return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)") + return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 4 (Edge Agent environment)") } payload.EndpointType = endpointType @@ -96,35 +93,14 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } } - switch portainer.EndpointType(payload.EndpointType) { - case portainer.AzureEnvironment: - azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false) - if err != nil { - return portainer.Error("Invalid Azure application ID") - } - payload.AzureApplicationID = azureApplicationID - - azureTenantID, err := request.RetrieveMultiPartFormValue(r, "AzureTenantID", false) - if err != nil { - return portainer.Error("Invalid Azure tenant ID") - } - payload.AzureTenantID = azureTenantID - - azureAuthenticationKey, err := request.RetrieveMultiPartFormValue(r, "AzureAuthenticationKey", false) - if err != nil { - return portainer.Error("Invalid Azure authentication key") - } - payload.AzureAuthenticationKey = azureAuthenticationKey - default: - url, err := request.RetrieveMultiPartFormValue(r, "URL", true) - if err != nil { - return portainer.Error("Invalid endpoint URL") - } - payload.URL = url - - publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true) - payload.PublicURL = publicURL + endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true) + if err != nil { + return portainer.Error("Invalid endpoint URL") } + payload.URL = endpointURL + + publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true) + payload.PublicURL = publicURL return nil } @@ -178,9 +154,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * } func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment { - return handler.createAzureEndpoint(payload) - } else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment { + if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment { return handler.createEdgeAgentEndpoint(payload) } @@ -190,44 +164,6 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain return handler.createUnsecuredEndpoint(payload) } -func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - credentials := portainer.AzureCredentials{ - ApplicationID: payload.AzureApplicationID, - TenantID: payload.AzureTenantID, - AuthenticationKey: payload.AzureAuthenticationKey, - } - - httpClient := client.NewHTTPClient() - _, err := httpClient.ExecuteAzureAuthenticationRequest(&credentials) - if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", err} - } - - endpointID := handler.EndpointService.GetNextIdentifier() - endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: payload.Name, - URL: "https://management.azure.com", - Type: portainer.AzureEnvironment, - GroupID: portainer.EndpointGroupID(payload.GroupID), - PublicURL: payload.PublicURL, - UserAccessPolicies: portainer.UserAccessPolicies{}, - TeamAccessPolicies: portainer.TeamAccessPolicies{}, - Extensions: []portainer.EndpointExtension{}, - AzureCredentials: credentials, - TagIDs: payload.TagIDs, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, - } - - err = handler.saveEndpointAndUpdateAuthorizations(endpoint) - if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err} - } - - return endpoint, nil -} - func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { endpointType := portainer.EdgeAgentEnvironment endpointID := handler.EndpointService.GetNextIdentifier() diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index de3eba46c..ba73674a9 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -23,10 +23,6 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - if endpoint.Type == portainer.AzureEnvironment { - return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for Azure endpoints", err} - } - snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint) latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID) diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go index e25be6f89..11a5b07ab 100644 --- a/api/http/handler/endpoints/endpoint_snapshots.go +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -17,10 +17,6 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request } for _, endpoint := range endpoints { - if endpoint.Type == portainer.AzureEnvironment { - continue - } - snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint) latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID) diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 8fe0b920c..a1c63d53c 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -9,24 +9,20 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/client" ) type endpointUpdatePayload struct { - Name *string - URL *string - PublicURL *string - GroupID *int - TLS *bool - TLSSkipVerify *bool - TLSSkipClientVerify *bool - Status *int - AzureApplicationID *string - AzureTenantID *string - AzureAuthenticationKey *string - TagIDs []portainer.TagID - UserAccessPolicies portainer.UserAccessPolicies - TeamAccessPolicies portainer.TeamAccessPolicies + Name *string + URL *string + PublicURL *string + GroupID *int + TLS *bool + TLSSkipVerify *bool + TLSSkipClientVerify *bool + Status *int + TagIDs []portainer.TagID + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies } func (payload *endpointUpdatePayload) Validate(r *http.Request) error { @@ -137,26 +133,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } - if endpoint.Type == portainer.AzureEnvironment { - credentials := endpoint.AzureCredentials - if payload.AzureApplicationID != nil { - credentials.ApplicationID = *payload.AzureApplicationID - } - if payload.AzureTenantID != nil { - credentials.TenantID = *payload.AzureTenantID - } - if payload.AzureAuthenticationKey != nil { - credentials.AuthenticationKey = *payload.AzureAuthenticationKey - } - - httpClient := client.NewHTTPClient() - _, authErr := httpClient.ExecuteAzureAuthenticationRequest(&credentials) - if authErr != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", authErr} - } - endpoint.AzureCredentials = credentials - } - if payload.TLS != nil { folder := strconv.Itoa(endpointID) @@ -201,7 +177,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } - if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment { + if payload.URL != nil || payload.TLS != nil { _, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index f7a19d106..605c81322 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -12,7 +12,6 @@ import ( ) func hideFields(endpoint *portainer.Endpoint) { - endpoint.AzureCredentials = portainer.AzureCredentials{} if len(endpoint.Snapshots) > 0 { endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{} } diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 8b167b12e..ad0fa92df 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -90,8 +90,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/storidge/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) - case strings.Contains(r.URL.Path, "/azure/"): - http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/edge/"): http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r) default: diff --git a/api/http/proxy/factory/azure.go b/api/http/proxy/factory/azure.go deleted file mode 100644 index 27b8a26f8..000000000 --- a/api/http/proxy/factory/azure.go +++ /dev/null @@ -1,20 +0,0 @@ -package factory - -import ( - "net/http" - "net/url" - - portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/proxy/factory/azure" -) - -func newAzureProxy(endpoint *portainer.Endpoint) (http.Handler, error) { - remoteURL, err := url.Parse(azureAPIBaseURL) - if err != nil { - return nil, err - } - - proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) - proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials) - return proxy, nil -} diff --git a/api/http/proxy/factory/azure/transport.go b/api/http/proxy/factory/azure/transport.go deleted file mode 100644 index 0c8505c8b..000000000 --- a/api/http/proxy/factory/azure/transport.go +++ /dev/null @@ -1,80 +0,0 @@ -package azure - -import ( - "net/http" - "strconv" - "sync" - "time" - - "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/client" -) - -type ( - azureAPIToken struct { - value string - expirationTime time.Time - } - - Transport struct { - credentials *portainer.AzureCredentials - client *client.HTTPClient - token *azureAPIToken - mutex sync.Mutex - } -) - -// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport -// interface for proxying requests to the Azure API. -func NewTransport(credentials *portainer.AzureCredentials) *Transport { - return &Transport{ - credentials: credentials, - client: client.NewHTTPClient(), - } -} - -// RoundTrip is the implementation of the the http.RoundTripper interface -func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) { - err := transport.retrieveAuthenticationToken() - if err != nil { - return nil, err - } - - request.Header.Set("Authorization", "Bearer "+transport.token.value) - return http.DefaultTransport.RoundTrip(request) -} - -func (transport *Transport) authenticate() error { - token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials) - if err != nil { - return err - } - - expiresOn, err := strconv.ParseInt(token.ExpiresOn, 10, 64) - if err != nil { - return err - } - - transport.token = &azureAPIToken{ - value: token.AccessToken, - expirationTime: time.Unix(expiresOn, 0), - } - - return nil -} - -func (transport *Transport) retrieveAuthenticationToken() error { - transport.mutex.Lock() - defer transport.mutex.Unlock() - - if transport.token == nil { - return transport.authenticate() - } - - timeLimit := time.Now().Add(-5 * time.Minute) - if timeLimit.After(transport.token.expirationTime) { - return transport.authenticate() - } - - return nil -} diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go index 207977f21..3f44b1211 100644 --- a/api/http/proxy/factory/factory.go +++ b/api/http/proxy/factory/factory.go @@ -10,8 +10,6 @@ import ( "github.com/portainer/portainer/api/docker" ) -const azureAPIBaseURL = "https://management.azure.com" - var extensionPorts = map[portainer.ExtensionID]string{ portainer.RegistryManagementExtension: "7001", portainer.OAuthAuthenticationExtension: "7002", @@ -100,11 +98,6 @@ func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (ht // NewEndpointProxy returns a new reverse proxy (filesystem based or HTTP) to an endpoint API server func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) { - switch endpoint.Type { - case portainer.AzureEnvironment: - return newAzureProxy(endpoint) - } - return factory.newDockerProxy(endpoint) } diff --git a/api/portainer.go b/api/portainer.go index 2666fc24b..a53c5355c 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -25,7 +25,7 @@ type ( Authorizations map[Authorization]bool // AzureCredentials represents the credentials used to connect to an Azure - // environment. + // environment (deprecated). AzureCredentials struct { ApplicationID string `json:"ApplicationID"` TenantID string `json:"TenantID"` @@ -140,7 +140,6 @@ type ( PublicURL string `json:"PublicURL"` TLSConfig TLSConfiguration `json:"TLSConfig"` Extensions []EndpointExtension `json:"Extensions"` - AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` TagIDs []TagID `json:"TagIds"` Status EndpointStatus `json:"Status"` Snapshots []Snapshot `json:"Snapshots"` @@ -161,6 +160,9 @@ type ( // Deprecated in DBVersion == 22 Tags []string `json:"Tags"` + + // Deprecated in DBVersion == 24 + AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` } // EndpointAuthorizations represents the authorizations associated to a set of endpoints @@ -1000,7 +1002,7 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "2.0.0-dev" // DBVersion is the version number of the Portainer database - DBVersion = 23 + DBVersion = 24 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved @@ -1074,7 +1076,7 @@ const ( DockerEnvironment // AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment AgentOnDockerEnvironment - // AzureEnvironment represents an endpoint connected to an Azure environment + // AzureEnvironment represents an endpoint connected to an Azure environment (deprecated) AzureEnvironment // EdgeAgentEnvironment represents an endpoint connected to an Edge agent EdgeAgentEnvironment diff --git a/api/swagger.yaml b/api/swagger.yaml index 2ecd61139..abb4aa686 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -254,7 +254,7 @@ paths: - name: "EndpointType" in: "formData" type: "integer" - description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge agent environment)" + description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 4 (Edge agent environment)" required: true - name: "URL" in: "formData" @@ -294,18 +294,6 @@ paths: in: "formData" type: "file" description: "TLS client key file" - - name: "AzureApplicationID" - in: "formData" - type: "string" - description: "Azure application ID. Required if endpoint type is set to 3" - - name: "AzureTenantID" - in: "formData" - type: "string" - description: "Azure tenant ID. Required if endpoint type is set to 3" - - name: "AzureAuthenticationKey" - in: "formData" - type: "string" - description: "Azure authentication key. Required if endpoint type is set to 3" responses: 200: description: "Success" @@ -3221,21 +3209,6 @@ definitions: type: "string" example: "/data/tls/key.pem" description: "Path to the TLS client key file" - AzureCredentials: - type: "object" - properties: - ApplicationID: - type: "string" - example: "eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4" - description: "Azure application ID" - TenantID: - type: "string" - example: "34ddc78d-4fel-2358-8cc1-df84c8o839f5" - description: "Azure tenant ID" - AuthenticationKey: - type: "string" - example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" - description: "Azure authentication key" LDAPSearchSettings: type: "object" properties: @@ -3507,7 +3480,7 @@ definitions: Type: type: "integer" example: 1 - description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment or 3 for an Azure environment." + description: "Endpoint environment type. 1 for a Docker environment or 2 for an agent on Docker environment" URL: type: "string" example: "docker.mydomain.tld:2375" @@ -3536,8 +3509,6 @@ definitions: description: "Team identifier" TLSConfig: $ref: "#/definitions/TLSConfiguration" - AzureCredentials: - $ref: "#/definitions/AzureCredentials" EndpointSubset: type: "object" properties: @@ -3552,7 +3523,7 @@ definitions: Type: type: "integer" example: 1 - description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment, 3 for an Azure environment." + description: "Endpoint environment type. 1 for a Docker environment or 2 for an agent on Docker environment" URL: type: "string" example: "docker.mydomain.tld:2375" @@ -3732,18 +3703,6 @@ definitions: type: "boolean" example: false description: "Skip client verification when using TLS" - ApplicationID: - type: "string" - example: "eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4" - description: "Azure application ID" - TenantID: - type: "string" - example: "34ddc78d-4fel-2358-8cc1-df84c8o839f5" - description: "Azure tenant ID" - AuthenticationKey: - type: "string" - example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" - description: "Azure authentication key" UserAccessPolicies: $ref: "#/definitions/UserAccessPolicies" TeamAccessPolicies: diff --git a/app/__module.js b/app/__module.js index 53c901aab..0c3cde285 100644 --- a/app/__module.js +++ b/app/__module.js @@ -2,7 +2,6 @@ import '../assets/css/app.css'; import angular from 'angular'; import './agent/_module'; -import './azure/_module'; import './docker/__module'; import './edge/__module'; import './portainer/__module'; @@ -28,7 +27,6 @@ angular.module('portainer', [ 'luegg.directives', 'portainer.app', 'portainer.agent', - 'portainer.azure', 'portainer.docker', 'portainer.edge', 'portainer.extensions', diff --git a/app/azure/_module.js b/app/azure/_module.js deleted file mode 100644 index a11a5aa5e..000000000 --- a/app/azure/_module.js +++ /dev/null @@ -1,51 +0,0 @@ -angular.module('portainer.azure', ['portainer.app']).config([ - '$stateRegistryProvider', - function ($stateRegistryProvider) { - 'use strict'; - - var azure = { - name: 'azure', - url: '/azure', - parent: 'root', - abstract: true, - }; - - var containerInstances = { - name: 'azure.containerinstances', - url: '/containerinstances', - views: { - 'content@': { - templateUrl: './views/containerinstances/containerinstances.html', - controller: 'AzureContainerInstancesController', - }, - }, - }; - - var containerInstanceCreation = { - name: 'azure.containerinstances.new', - url: '/new/', - views: { - 'content@': { - templateUrl: './views/containerinstances/create/createcontainerinstance.html', - controller: 'AzureCreateContainerInstanceController', - }, - }, - }; - - var dashboard = { - name: 'azure.dashboard', - url: '/dashboard', - views: { - 'content@': { - templateUrl: './views/dashboard/dashboard.html', - controller: 'AzureDashboardController', - }, - }, - }; - - $stateRegistryProvider.register(azure); - $stateRegistryProvider.register(containerInstances); - $stateRegistryProvider.register(containerInstanceCreation); - $stateRegistryProvider.register(dashboard); - }, -]); diff --git a/app/azure/components/azure-endpoint-config/azure-endpoint-config.js b/app/azure/components/azure-endpoint-config/azure-endpoint-config.js deleted file mode 100644 index ff09f0908..000000000 --- a/app/azure/components/azure-endpoint-config/azure-endpoint-config.js +++ /dev/null @@ -1,8 +0,0 @@ -angular.module('portainer.azure').component('azureEndpointConfig', { - bindings: { - applicationId: '=', - tenantId: '=', - authenticationKey: '=', - }, - templateUrl: './azureEndpointConfig.html', -}); diff --git a/app/azure/components/azure-endpoint-config/azureEndpointConfig.html b/app/azure/components/azure-endpoint-config/azureEndpointConfig.html deleted file mode 100644 index efc8bd79f..000000000 --- a/app/azure/components/azure-endpoint-config/azureEndpointConfig.html +++ /dev/null @@ -1,36 +0,0 @@ -
-
- Azure configuration -
- -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- -
diff --git a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js deleted file mode 100644 index daec3ef12..000000000 --- a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js +++ /dev/null @@ -1,3 +0,0 @@ -angular.module('portainer.azure').component('azureSidebarContent', { - templateUrl: './azureSidebarContent.html', -}); diff --git a/app/azure/components/azure-sidebar-content/azureSidebarContent.html b/app/azure/components/azure-sidebar-content/azureSidebarContent.html deleted file mode 100644 index 01986e8e7..000000000 --- a/app/azure/components/azure-sidebar-content/azureSidebarContent.html +++ /dev/null @@ -1,6 +0,0 @@ - - diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html deleted file mode 100644 index f9936d78b..000000000 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html +++ /dev/null @@ -1,105 +0,0 @@ -
- - -
-
{{ $ctrl.titleText }}
-
-
- - -
- -
- - - - - - - - - - - - - - - - - - - - - -
- - - - - - Name - - - - - - Location - - - - - Published Ports -
- - - - - {{ item.Name | truncate: 50 }} - {{ item.Location }} - - :{{ p.port }} - - - -
Loading...
No container available.
-
- -
-
-
diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js deleted file mode 100644 index 8d91518a9..000000000 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('portainer.azure').component('containergroupsDatatable', { - templateUrl: './containerGroupsDatatable.html', - controller: 'GenericDatatableController', - bindings: { - title: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - removeAction: '<', - }, -}); diff --git a/app/azure/models/container_group.js b/app/azure/models/container_group.js deleted file mode 100644 index dfc9adeef..000000000 --- a/app/azure/models/container_group.js +++ /dev/null @@ -1,66 +0,0 @@ -export function ContainerGroupDefaultModel() { - this.Location = ''; - this.OSType = 'Linux'; - this.Name = ''; - this.Image = ''; - this.AllocatePublicIP = true; - this.Ports = [ - { - container: 80, - host: 80, - protocol: 'TCP', - }, - ]; - this.CPU = 1; - this.Memory = 1; -} - -export function ContainerGroupViewModel(data) { - this.Id = data.id; - this.Name = data.name; - this.Location = data.location; - this.IPAddress = data.properties.ipAddress.ip; - this.Ports = data.properties.ipAddress.ports; -} - -export function CreateContainerGroupRequest(model) { - this.location = model.Location; - - var containerPorts = []; - var addressPorts = []; - for (var i = 0; i < model.Ports.length; i++) { - var binding = model.Ports[i]; - - containerPorts.push({ - port: binding.container, - }); - - addressPorts.push({ - port: binding.host, - protocol: binding.protocol, - }); - } - - this.properties = { - osType: model.OSType, - containers: [ - { - name: model.Name, - properties: { - image: model.Image, - ports: containerPorts, - resources: { - requests: { - cpu: model.CPU, - memoryInGB: model.Memory, - }, - }, - }, - }, - ], - ipAddress: { - type: model.AllocatePublicIP ? 'Public' : 'Private', - ports: addressPorts, - }, - }; -} diff --git a/app/azure/models/location.js b/app/azure/models/location.js deleted file mode 100644 index 6d4031331..000000000 --- a/app/azure/models/location.js +++ /dev/null @@ -1,6 +0,0 @@ -export function LocationViewModel(data) { - this.Id = data.id; - this.SubscriptionId = data.subscriptionId; - this.DisplayName = data.displayName; - this.Name = data.name; -} diff --git a/app/azure/models/provider.js b/app/azure/models/provider.js deleted file mode 100644 index d9d6c8075..000000000 --- a/app/azure/models/provider.js +++ /dev/null @@ -1,9 +0,0 @@ -import _ from 'lodash-es'; - -export function ContainerInstanceProviderViewModel(data) { - this.Id = data.id; - this.Namespace = data.namespace; - - var containerGroupType = _.find(data.resourceTypes, { resourceType: 'containerGroups' }); - this.Locations = containerGroupType.locations; -} diff --git a/app/azure/models/resource_group.js b/app/azure/models/resource_group.js deleted file mode 100644 index 894ce326d..000000000 --- a/app/azure/models/resource_group.js +++ /dev/null @@ -1,6 +0,0 @@ -export function ResourceGroupViewModel(data, subscriptionId) { - this.Id = data.id; - this.SubscriptionId = subscriptionId; - this.Name = data.name; - this.Location = data.location; -} diff --git a/app/azure/models/subscription.js b/app/azure/models/subscription.js deleted file mode 100644 index eb9bfaf52..000000000 --- a/app/azure/models/subscription.js +++ /dev/null @@ -1,4 +0,0 @@ -export function SubscriptionViewModel(data) { - this.Id = data.subscriptionId; - this.Name = data.displayName; -} diff --git a/app/azure/rest/azure.js b/app/azure/rest/azure.js deleted file mode 100644 index f463624d6..000000000 --- a/app/azure/rest/azure.js +++ /dev/null @@ -1,20 +0,0 @@ -angular.module('portainer.azure').factory('Azure', [ - '$http', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function AzureFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - - var service = {}; - - service.delete = function (id, apiVersion) { - var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/azure' + id + '?api-version=' + apiVersion; - return $http({ - method: 'DELETE', - url: url, - }); - }; - - return service; - }, -]); diff --git a/app/azure/rest/container_group.js b/app/azure/rest/container_group.js deleted file mode 100644 index 4dc7a002d..000000000 --- a/app/azure/rest/container_group.js +++ /dev/null @@ -1,45 +0,0 @@ -angular.module('portainer.azure').factory('ContainerGroup', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function ContainerGroupFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - - var resource = {}; - - var base = $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/providers/Microsoft.ContainerInstance/containerGroups', - { - endpointId: EndpointProvider.endpointID, - 'api-version': '2018-04-01', - }, - { - query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }, - } - ); - - var withResourceGroup = $resource( - API_ENDPOINT_ENDPOINTS + - '/:endpointId/azure/subscriptions/:subscriptionId/resourceGroups/:resourceGroupName/providers/Microsoft.ContainerInstance/containerGroups/:containerGroupName', - { - endpointId: EndpointProvider.endpointID, - 'api-version': '2018-04-01', - }, - { - create: { - method: 'PUT', - params: { - subscriptionId: '@subscriptionId', - resourceGroupName: '@resourceGroupName', - containerGroupName: '@containerGroupName', - }, - }, - } - ); - - resource.query = base.query; - resource.create = withResourceGroup.create; - - return resource; - }, -]); diff --git a/app/azure/rest/location.js b/app/azure/rest/location.js deleted file mode 100644 index 7503d9fc9..000000000 --- a/app/azure/rest/location.js +++ /dev/null @@ -1,18 +0,0 @@ -angular.module('portainer.azure').factory('Location', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function LocationFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/locations', - { - endpointId: EndpointProvider.endpointID, - 'api-version': '2016-06-01', - }, - { - query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }, - } - ); - }, -]); diff --git a/app/azure/rest/provider.js b/app/azure/rest/provider.js deleted file mode 100644 index b8e76d81e..000000000 --- a/app/azure/rest/provider.js +++ /dev/null @@ -1,18 +0,0 @@ -angular.module('portainer.azure').factory('Provider', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function ProviderFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/providers/:providerNamespace', - { - endpointId: EndpointProvider.endpointID, - 'api-version': '2018-02-01', - }, - { - get: { method: 'GET', params: { subscriptionId: '@subscriptionId', providerNamespace: '@providerNamespace' } }, - } - ); - }, -]); diff --git a/app/azure/rest/resource_group.js b/app/azure/rest/resource_group.js deleted file mode 100644 index 644279f3b..000000000 --- a/app/azure/rest/resource_group.js +++ /dev/null @@ -1,18 +0,0 @@ -angular.module('portainer.azure').factory('ResourceGroup', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function ResourceGroupFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/resourcegroups', - { - endpointId: EndpointProvider.endpointID, - 'api-version': '2018-02-01', - }, - { - query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }, - } - ); - }, -]); diff --git a/app/azure/rest/subscription.js b/app/azure/rest/subscription.js deleted file mode 100644 index 0711d5f92..000000000 --- a/app/azure/rest/subscription.js +++ /dev/null @@ -1,18 +0,0 @@ -angular.module('portainer.azure').factory('Subscription', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function SubscriptionFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions', - { - endpointId: EndpointProvider.endpointID, - 'api-version': '2016-06-01', - }, - { - query: { method: 'GET' }, - } - ); - }, -]); diff --git a/app/azure/services/azureService.js b/app/azure/services/azureService.js deleted file mode 100644 index b6c3ba0aa..000000000 --- a/app/azure/services/azureService.js +++ /dev/null @@ -1,72 +0,0 @@ -angular.module('portainer.azure').factory('AzureService', [ - '$q', - 'Azure', - 'SubscriptionService', - 'ResourceGroupService', - 'ContainerGroupService', - 'ProviderService', - function AzureServiceFactory($q, Azure, SubscriptionService, ResourceGroupService, ContainerGroupService, ProviderService) { - 'use strict'; - var service = {}; - - service.deleteContainerGroup = function (id) { - return Azure.delete(id, '2018-04-01'); - }; - - service.createContainerGroup = function (model, subscriptionId, resourceGroupName) { - return ContainerGroupService.create(model, subscriptionId, resourceGroupName); - }; - - service.subscriptions = function () { - return SubscriptionService.subscriptions(); - }; - - service.containerInstanceProvider = function (subscriptions) { - return retrieveResourcesForEachSubscription(subscriptions, ProviderService.containerInstanceProvider); - }; - - service.resourceGroups = function (subscriptions) { - return retrieveResourcesForEachSubscription(subscriptions, ResourceGroupService.resourceGroups); - }; - - service.containerGroups = function (subscriptions) { - return retrieveResourcesForEachSubscription(subscriptions, ContainerGroupService.containerGroups); - }; - - service.aggregate = function (resourcesBySubcription) { - var aggregatedResources = []; - Object.keys(resourcesBySubcription).forEach(function (key) { - aggregatedResources = aggregatedResources.concat(resourcesBySubcription[key]); - }); - return aggregatedResources; - }; - - function retrieveResourcesForEachSubscription(subscriptions, resourceQuery) { - var deferred = $q.defer(); - - var resources = {}; - - var resourceQueries = []; - for (var i = 0; i < subscriptions.length; i++) { - var subscription = subscriptions[i]; - resourceQueries.push(resourceQuery(subscription.Id)); - } - - $q.all(resourceQueries) - .then(function success(data) { - for (var i = 0; i < data.length; i++) { - var result = data[i]; - resources[subscriptions[i].Id] = result; - } - deferred.resolve(resources); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve resources', err: err }); - }); - - return deferred.promise; - } - - return service; - }, -]); diff --git a/app/azure/services/containerGroupService.js b/app/azure/services/containerGroupService.js deleted file mode 100644 index c99b98ada..000000000 --- a/app/azure/services/containerGroupService.js +++ /dev/null @@ -1,41 +0,0 @@ -import { ContainerGroupViewModel, CreateContainerGroupRequest } from '../models/container_group'; - -angular.module('portainer.azure').factory('ContainerGroupService', [ - '$q', - 'ContainerGroup', - function ContainerGroupServiceFactory($q, ContainerGroup) { - 'use strict'; - var service = {}; - - service.containerGroups = function (subscriptionId) { - var deferred = $q.defer(); - - ContainerGroup.query({ subscriptionId: subscriptionId }) - .$promise.then(function success(data) { - var containerGroups = data.value.map(function (item) { - return new ContainerGroupViewModel(item); - }); - deferred.resolve(containerGroups); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve container groups', err: err }); - }); - - return deferred.promise; - }; - - service.create = function (model, subscriptionId, resourceGroupName) { - var payload = new CreateContainerGroupRequest(model); - return ContainerGroup.create( - { - subscriptionId: subscriptionId, - resourceGroupName: resourceGroupName, - containerGroupName: model.Name, - }, - payload - ).$promise; - }; - - return service; - }, -]); diff --git a/app/azure/services/locationService.js b/app/azure/services/locationService.js deleted file mode 100644 index a21e7fa0a..000000000 --- a/app/azure/services/locationService.js +++ /dev/null @@ -1,29 +0,0 @@ -import { LocationViewModel } from '../models/location'; - -angular.module('portainer.azure').factory('LocationService', [ - '$q', - 'Location', - function LocationServiceFactory($q, Location) { - 'use strict'; - var service = {}; - - service.locations = function (subscriptionId) { - var deferred = $q.defer(); - - Location.query({ subscriptionId: subscriptionId }) - .$promise.then(function success(data) { - var locations = data.value.map(function (item) { - return new LocationViewModel(item); - }); - deferred.resolve(locations); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve locations', err: err }); - }); - - return deferred.promise; - }; - - return service; - }, -]); diff --git a/app/azure/services/providerService.js b/app/azure/services/providerService.js deleted file mode 100644 index edc42ae9e..000000000 --- a/app/azure/services/providerService.js +++ /dev/null @@ -1,27 +0,0 @@ -import { ContainerInstanceProviderViewModel } from '../models/provider'; - -angular.module('portainer.azure').factory('ProviderService', [ - '$q', - 'Provider', - function ProviderServiceFactory($q, Provider) { - 'use strict'; - var service = {}; - - service.containerInstanceProvider = function (subscriptionId) { - var deferred = $q.defer(); - - Provider.get({ subscriptionId: subscriptionId, providerNamespace: 'Microsoft.ContainerInstance' }) - .$promise.then(function success(data) { - var provider = new ContainerInstanceProviderViewModel(data); - deferred.resolve(provider); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve provider', err: err }); - }); - - return deferred.promise; - }; - - return service; - }, -]); diff --git a/app/azure/services/resourceGroupService.js b/app/azure/services/resourceGroupService.js deleted file mode 100644 index 4110835f4..000000000 --- a/app/azure/services/resourceGroupService.js +++ /dev/null @@ -1,29 +0,0 @@ -import { ResourceGroupViewModel } from '../models/resource_group'; - -angular.module('portainer.azure').factory('ResourceGroupService', [ - '$q', - 'ResourceGroup', - function ResourceGroupServiceFactory($q, ResourceGroup) { - 'use strict'; - var service = {}; - - service.resourceGroups = function (subscriptionId) { - var deferred = $q.defer(); - - ResourceGroup.query({ subscriptionId: subscriptionId }) - .$promise.then(function success(data) { - var resourceGroups = data.value.map(function (item) { - return new ResourceGroupViewModel(item, subscriptionId); - }); - deferred.resolve(resourceGroups); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve resource groups', err: err }); - }); - - return deferred.promise; - }; - - return service; - }, -]); diff --git a/app/azure/services/subscriptionService.js b/app/azure/services/subscriptionService.js deleted file mode 100644 index 3b22ac664..000000000 --- a/app/azure/services/subscriptionService.js +++ /dev/null @@ -1,29 +0,0 @@ -import { SubscriptionViewModel } from '../models/subscription'; - -angular.module('portainer.azure').factory('SubscriptionService', [ - '$q', - 'Subscription', - function SubscriptionServiceFactory($q, Subscription) { - 'use strict'; - var service = {}; - - service.subscriptions = function () { - var deferred = $q.defer(); - - Subscription.query({}) - .$promise.then(function success(data) { - var subscriptions = data.value.map(function (item) { - return new SubscriptionViewModel(item); - }); - deferred.resolve(subscriptions); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve subscriptions', err: err }); - }); - - return deferred.promise; - }; - - return service; - }, -]); diff --git a/app/azure/views/containerinstances/containerInstancesController.js b/app/azure/views/containerinstances/containerInstancesController.js deleted file mode 100644 index 4863d5cac..000000000 --- a/app/azure/views/containerinstances/containerInstancesController.js +++ /dev/null @@ -1,44 +0,0 @@ -angular.module('portainer.azure').controller('AzureContainerInstancesController', [ - '$scope', - '$state', - 'AzureService', - 'Notifications', - function ($scope, $state, AzureService, Notifications) { - function initView() { - AzureService.subscriptions() - .then(function success(data) { - var subscriptions = data; - return AzureService.containerGroups(subscriptions); - }) - .then(function success(data) { - $scope.containerGroups = AzureService.aggregate(data); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to load container groups'); - }); - } - - $scope.deleteAction = function (selectedItems) { - var actionCount = selectedItems.length; - angular.forEach(selectedItems, function (item) { - AzureService.deleteContainerGroup(item.Id) - .then(function success() { - Notifications.success('Container group successfully removed', item.Name); - var index = $scope.containerGroups.indexOf(item); - $scope.containerGroups.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove container group'); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } - }); - }); - }; - - initView(); - }, -]); diff --git a/app/azure/views/containerinstances/containerinstances.html b/app/azure/views/containerinstances/containerinstances.html deleted file mode 100644 index 6c0223852..000000000 --- a/app/azure/views/containerinstances/containerinstances.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - Container instances - - -
-
- -
-
diff --git a/app/azure/views/containerinstances/create/createContainerInstanceController.js b/app/azure/views/containerinstances/create/createContainerInstanceController.js deleted file mode 100644 index 7c2774946..000000000 --- a/app/azure/views/containerinstances/create/createContainerInstanceController.js +++ /dev/null @@ -1,93 +0,0 @@ -import { ContainerGroupDefaultModel } from '../../../models/container_group'; - -angular.module('portainer.azure').controller('AzureCreateContainerInstanceController', [ - '$q', - '$scope', - '$state', - 'AzureService', - 'Notifications', - function ($q, $scope, $state, AzureService, Notifications) { - var allResourceGroups = []; - var allProviders = []; - - $scope.state = { - actionInProgress: false, - selectedSubscription: null, - selectedResourceGroup: null, - }; - - $scope.changeSubscription = function () { - var selectedSubscription = $scope.state.selectedSubscription; - updateResourceGroupsAndLocations(selectedSubscription, allResourceGroups, allProviders); - }; - - $scope.addPortBinding = function () { - $scope.model.Ports.push({ host: '', container: '', protocol: 'TCP' }); - }; - - $scope.removePortBinding = function (index) { - $scope.model.Ports.splice(index, 1); - }; - - $scope.create = function () { - var model = $scope.model; - var subscriptionId = $scope.state.selectedSubscription.Id; - var resourceGroupName = $scope.state.selectedResourceGroup.Name; - - $scope.state.actionInProgress = true; - AzureService.createContainerGroup(model, subscriptionId, resourceGroupName) - .then(function success() { - Notifications.success('Container successfully created', model.Name); - $state.go('azure.containerinstances'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create container'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; - - function updateResourceGroupsAndLocations(subscription, resourceGroups, providers) { - $scope.state.selectedResourceGroup = resourceGroups[subscription.Id][0]; - $scope.resourceGroups = resourceGroups[subscription.Id]; - - var currentSubLocations = providers[subscription.Id].Locations; - $scope.model.Location = currentSubLocations[0]; - $scope.locations = currentSubLocations; - } - - function initView() { - var model = new ContainerGroupDefaultModel(); - - AzureService.subscriptions() - .then(function success(data) { - var subscriptions = data; - $scope.state.selectedSubscription = subscriptions[0]; - $scope.subscriptions = subscriptions; - - return $q.all({ - resourceGroups: AzureService.resourceGroups(subscriptions), - containerInstancesProviders: AzureService.containerInstanceProvider(subscriptions), - }); - }) - .then(function success(data) { - var resourceGroups = data.resourceGroups; - allResourceGroups = resourceGroups; - - var containerInstancesProviders = data.containerInstancesProviders; - allProviders = containerInstancesProviders; - - $scope.model = model; - - var selectedSubscription = $scope.state.selectedSubscription; - updateResourceGroupsAndLocations(selectedSubscription, resourceGroups, containerInstancesProviders); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve Azure resources'); - }); - } - - initView(); - }, -]); diff --git a/app/azure/views/containerinstances/create/createcontainerinstance.html b/app/azure/views/containerinstances/create/createcontainerinstance.html deleted file mode 100644 index 625e50bc5..000000000 --- a/app/azure/views/containerinstances/create/createcontainerinstance.html +++ /dev/null @@ -1,167 +0,0 @@ - - - Container instances > Add container - - -
-
- - -
-
- Azure settings -
- -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- -
- Container configuration -
- -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
-
- - - map additional port - -
- -
-
- -
- host - -
- - - - - -
- container - -
- - -
-
- - -
- -
- -
-
- -
- - -
-
- - -
-
- -
- Container resources -
- -
- -
- -
-
- - -
- -
- -
-
- - -
- Actions -
-
-
- -
-
- -
-
-
-
-
diff --git a/app/azure/views/dashboard/dashboard.html b/app/azure/views/dashboard/dashboard.html deleted file mode 100644 index eaa60a53e..000000000 --- a/app/azure/views/dashboard/dashboard.html +++ /dev/null @@ -1,33 +0,0 @@ - - - Dashboard - - - diff --git a/app/azure/views/dashboard/dashboardController.js b/app/azure/views/dashboard/dashboardController.js deleted file mode 100644 index 643f900a7..000000000 --- a/app/azure/views/dashboard/dashboardController.js +++ /dev/null @@ -1,23 +0,0 @@ -angular.module('portainer.azure').controller('AzureDashboardController', [ - '$scope', - 'AzureService', - 'Notifications', - function ($scope, AzureService, Notifications) { - function initView() { - AzureService.subscriptions() - .then(function success(data) { - var subscriptions = data; - $scope.subscriptions = subscriptions; - return AzureService.resourceGroups(subscriptions); - }) - .then(function success(data) { - $scope.resourceGroups = AzureService.aggregate(data); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to load dashboard data'); - }); - } - - initView(); - }, -]); diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html index 5b98bdc12..500a17ae8 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html @@ -1,6 +1,6 @@
- + diff --git a/app/portainer/filters/filters.js b/app/portainer/filters/filters.js index 034dba32c..e4c232ee5 100644 --- a/app/portainer/filters/filters.js +++ b/app/portainer/filters/filters.js @@ -128,8 +128,6 @@ angular return 'Docker'; } else if (type === 2) { return 'Agent'; - } else if (type === 3) { - return 'Azure ACI'; } else if (type === 4) { return 'Edge Agent'; } diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index becb4efbf..fd9b327b0 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -84,20 +84,6 @@ angular.module('portainer.app').factory('EndpointService', [ return deferred.promise; }; - service.createAzureEndpoint = function (name, applicationId, tenantId, authenticationKey, groupId, tagIds) { - var deferred = $q.defer(); - - FileUploadService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) - .then(function success(response) { - deferred.resolve(response.data); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to connect to Azure', err: err }); - }); - - return deferred.promise; - }; - service.executeJobFromFileUpload = function (image, jobFile, endpointId, nodeName) { return FileUploadService.executeEndpointJob(image, jobFile, endpointId, nodeName); }; diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 6a571e47a..4f024d814 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -137,22 +137,6 @@ angular.module('portainer.app').factory('FileUploadService', [ }); }; - service.createAzureEndpoint = function (name, applicationId, tenantId, authenticationKey, groupId, tagIds) { - return Upload.upload({ - url: 'api/endpoints', - data: { - Name: name, - EndpointType: 3, - GroupID: groupId, - TagIds: Upload.json(tagIds), - AzureApplicationID: applicationId, - AzureTenantID: tenantId, - AzureAuthenticationKey: authenticationKey, - }, - ignoreLoadingBar: true, - }); - }; - service.uploadLDAPTLSFiles = function (TLSCAFile, TLSCertFile, TLSKeyFile) { var queue = []; diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index fa2b1c72b..819d8c570 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -168,14 +168,6 @@ angular.module('portainer.app').factory('StateManager', [ manager.updateEndpointState = function (endpoint, extensions) { var deferred = $q.defer(); - if (endpoint.Type === 3) { - state.endpoint.name = endpoint.Name; - state.endpoint.mode = { provider: 'AZURE' }; - LocalStorage.storeEndpointState(state.endpoint); - deferred.resolve(); - return deferred.promise; - } - $q.all({ version: endpoint.Status === 1 ? SystemService.version() : $q.when(endpoint.Snapshots[0].SnapshotRaw.Version), info: endpoint.Status === 1 ? SystemService.info() : $q.when(endpoint.Snapshots[0].SnapshotRaw.Info), diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index 60f81798d..a1c6b6003 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -27,9 +27,6 @@ angular PublicURL: '', GroupId: 1, SecurityFormData: new EndpointSecurityFormData(), - AzureApplicationId: '', - AzureTenantId: '', - AzureAuthenticationKey: '', TagIds: [], }; @@ -85,17 +82,6 @@ angular addEndpoint(name, 4, URL, '', groupId, tagIds, false, false, false, null, null, null); }; - $scope.addAzureEndpoint = function () { - var name = $scope.formValues.Name; - var applicationId = $scope.formValues.AzureApplicationId; - var tenantId = $scope.formValues.AzureTenantId; - var authenticationKey = $scope.formValues.AzureAuthenticationKey; - var groupId = $scope.formValues.GroupId; - var tagIds = $scope.formValues.TagIds; - - createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds); - }; - $scope.onCreateTag = function onCreateTag(tagName) { return $async(onCreateTagAsync, tagName); }; @@ -110,21 +96,6 @@ angular } } - function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) { - $scope.state.actionInProgress = true; - EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) - .then(function success() { - Notifications.success('Endpoint created', name); - $state.go('portainer.endpoints', {}, { reload: true }); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create endpoint'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - } - function addEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { $scope.state.actionInProgress = true; EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 3c0c014c2..4a366a22e 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -44,16 +44,6 @@

Directly connect to the Docker API

-
- - -
@@ -97,29 +87,6 @@
-
-
- Information -
-
-
- -

This feature is experimental.

-

- Connect to Microsoft Azure to manage Azure Container Instances (ACI). -

-

- - Have a look at - the Azure documentation - to retrieve the credentials required below. -

-
-
-
-
Environment details
@@ -214,76 +181,6 @@
- -
- -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- - -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- - -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- -
- @@ -350,17 +247,6 @@ Add endpoint Creating endpoint... -
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index b17b0ca8c..6223629fd 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -118,12 +118,6 @@ -
Metadata diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index c4d560645..221c9282e 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -89,9 +89,6 @@ angular TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert, TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert, TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey, - AzureApplicationID: endpoint.AzureCredentials.ApplicationID, - AzureTenantID: endpoint.AzureCredentials.TenantID, - AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey, }; if ($scope.endpointType !== 'local' && endpoint.Type !== 3) { diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index af41523f7..4322c0f30 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -26,9 +26,7 @@ angular }; $scope.goToDashboard = function (endpoint) { - if (endpoint.Type === 3) { - return switchToAzureEndpoint(endpoint); - } else if (endpoint.Type === 4) { + if (endpoint.Type === 4) { return switchToEdgeEndpoint(endpoint); } @@ -89,19 +87,6 @@ angular return deferred.promise; } - function switchToAzureEndpoint(endpoint) { - EndpointProvider.setEndpointID(endpoint.Id); - EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); - EndpointProvider.setOfflineModeFromStatus(endpoint.Status); - StateManager.updateEndpointState(endpoint, []) - .then(function success() { - $state.go('azure.dashboard'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint'); - }); - } - function switchToEdgeEndpoint(endpoint) { if (!endpoint.EdgeID) { $state.go('portainer.endpoints.endpoint', { id: endpoint.Id }); diff --git a/app/portainer/views/init/endpoint/initEndpoint.html b/app/portainer/views/init/endpoint/initEndpoint.html index 0c1eec55a..717d00075 100644 --- a/app/portainer/views/init/endpoint/initEndpoint.html +++ b/app/portainer/views/init/endpoint/initEndpoint.html @@ -55,16 +55,6 @@

Connect to a Portainer agent

-
- - -
@@ -168,91 +158,6 @@ - -
-
- Information -
-
-
- -

This feature is experimental.

-

- Connect to Microsoft Azure to manage Azure Container Instances (ACI). -

-

- - Have a look at - the Azure documentation - to retrieve the credentials required below. -

-
-
-
-
- Environment -
- -
- -
- -
-
- -
- Azure credentials -
- -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
-
- -
-
- -
-
diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index fc4ff905a..3c41c60af 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -28,9 +28,6 @@ angular.module('portainer.app').controller('InitEndpointController', [ TLSCACert: null, TLSCert: null, TLSKey: null, - AzureApplicationId: '', - AzureTenantId: '', - AzureAuthenticationKey: '', }; $scope.createLocalEndpoint = function () { @@ -47,15 +44,6 @@ angular.module('portainer.app').controller('InitEndpointController', [ }); }; - $scope.createAzureEndpoint = function () { - var name = $scope.formValues.Name; - var applicationId = $scope.formValues.AzureApplicationId; - var tenantId = $scope.formValues.AzureTenantId; - var authenticationKey = $scope.formValues.AzureAuthenticationKey; - - createAzureEndpoint(name, applicationId, tenantId, authenticationKey); - }; - $scope.createAgentEndpoint = function () { var name = $scope.formValues.Name; var URL = $scope.formValues.URL; @@ -78,20 +66,6 @@ angular.module('portainer.app').controller('InitEndpointController', [ createRemoteEndpoint(name, 1, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); }; - function createAzureEndpoint(name, applicationId, tenantId, authenticationKey) { - $scope.state.actionInProgress = true; - EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, []) - .then(function success() { - $state.go('portainer.home'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Azure environment'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - } - function createRemoteEndpoint(name, type, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { $scope.state.actionInProgress = true; EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 8e1a3f4d9..d2f092095 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -13,9 +13,7 @@ Home - Date: Wed, 20 May 2020 17:23:15 +1200 Subject: [PATCH 014/195] feat(api): introduce new datastore interface (#3802) * feat(api): introduce new datastore interface * refactor(api): refactor http and main layers * refactor(api): refactor http and bolt layers --- api/authorizations.go | 69 +++--- api/bolt/datastore.go | 105 ++++++++- api/bolt/migrator/migrate_dbversion19.go | 12 +- api/bolt/migrator/migrate_dbversion20.go | 13 +- api/bolt/migrator/migrator.go | 4 +- api/chisel/service.go | 28 ++- api/chisel/tunnel.go | 2 +- api/cmd/portainer/main.go | 149 ++++++------- api/cron/job_script_execution.go | 16 +- api/cron/job_snapshot.go | 16 +- api/exec/extension.go | 18 +- api/http/handler/auth/authenticate.go | 12 +- api/http/handler/auth/authenticate_oauth.go | 10 +- api/http/handler/auth/handler.go | 23 +- .../handler/dockerhub/dockerhub_inspect.go | 2 +- .../handler/dockerhub/dockerhub_update.go | 2 +- api/http/handler/dockerhub/handler.go | 2 +- .../edgegroups/associated_endpoints.go | 4 +- .../handler/edgegroups/edgegroup_create.go | 6 +- .../handler/edgegroups/edgegroup_delete.go | 6 +- .../handler/edgegroups/edgegroup_inspect.go | 2 +- api/http/handler/edgegroups/edgegroup_list.go | 4 +- .../handler/edgegroups/edgegroup_update.go | 24 +-- api/http/handler/edgegroups/handler.go | 7 +- .../handler/edgestacks/edgestack_create.go | 24 +-- .../handler/edgestacks/edgestack_delete.go | 14 +- api/http/handler/edgestacks/edgestack_file.go | 2 +- .../handler/edgestacks/edgestack_inspect.go | 2 +- api/http/handler/edgestacks/edgestack_list.go | 2 +- .../edgestacks/edgestack_status_update.go | 6 +- .../handler/edgestacks/edgestack_update.go | 18 +- api/http/handler/edgestacks/handler.go | 12 +- .../edgetemplates/edgetemplate_list.go | 2 +- api/http/handler/edgetemplates/handler.go | 4 +- .../endpoint_edgestack_inspect.go | 4 +- api/http/handler/endpointedge/handler.go | 7 +- .../endpointgroups/endpointgroup_create.go | 10 +- .../endpointgroups/endpointgroup_delete.go | 12 +- .../endpointgroup_endpoint_add.go | 6 +- .../endpointgroup_endpoint_delete.go | 6 +- .../endpointgroups/endpointgroup_inspect.go | 2 +- .../endpointgroups/endpointgroup_list.go | 2 +- .../endpointgroups/endpointgroup_update.go | 14 +- api/http/handler/endpointgroups/endpoints.go | 10 +- api/http/handler/endpointgroups/handler.go | 9 +- api/http/handler/endpointproxy/handler.go | 3 +- .../handler/endpointproxy/proxy_docker.go | 4 +- .../handler/endpointproxy/proxy_storidge.go | 2 +- api/http/handler/endpoints/endpoint_create.go | 22 +- api/http/handler/endpoints/endpoint_delete.go | 18 +- .../endpoints/endpoint_extension_add.go | 4 +- .../endpoints/endpoint_extension_remove.go | 4 +- .../handler/endpoints/endpoint_inspect.go | 2 +- api/http/handler/endpoints/endpoint_job.go | 2 +- api/http/handler/endpoints/endpoint_list.go | 6 +- .../handler/endpoints/endpoint_snapshot.go | 6 +- .../handler/endpoints/endpoint_snapshots.go | 6 +- .../endpoints/endpoint_status_inspect.go | 10 +- api/http/handler/endpoints/endpoint_update.go | 22 +- api/http/handler/endpoints/handler.go | 26 +-- api/http/handler/extensions/data.go | 16 +- .../handler/extensions/extension_create.go | 4 +- .../handler/extensions/extension_delete.go | 4 +- .../handler/extensions/extension_inspect.go | 2 +- api/http/handler/extensions/extension_list.go | 2 +- .../handler/extensions/extension_update.go | 4 +- .../handler/extensions/extension_upload.go | 2 +- api/http/handler/extensions/handler.go | 5 +- api/http/handler/registries/handler.go | 9 +- api/http/handler/registries/proxy.go | 4 +- .../registries/proxy_management_gitlab.go | 4 +- .../handler/registries/registry_configure.go | 4 +- .../handler/registries/registry_create.go | 2 +- .../handler/registries/registry_delete.go | 4 +- .../handler/registries/registry_inspect.go | 2 +- api/http/handler/registries/registry_list.go | 2 +- .../handler/registries/registry_update.go | 6 +- api/http/handler/resourcecontrols/handler.go | 2 +- .../resourcecontrol_create.go | 4 +- .../resourcecontrol_delete.go | 4 +- .../resourcecontrol_update.go | 4 +- api/http/handler/roles/handler.go | 2 +- api/http/handler/roles/role_list.go | 2 +- api/http/handler/schedules/handler.go | 4 +- api/http/handler/schedules/schedule_create.go | 12 +- api/http/handler/schedules/schedule_delete.go | 6 +- api/http/handler/schedules/schedule_file.go | 4 +- .../handler/schedules/schedule_inspect.go | 4 +- api/http/handler/schedules/schedule_list.go | 4 +- api/http/handler/schedules/schedule_tasks.go | 6 +- api/http/handler/schedules/schedule_update.go | 10 +- api/http/handler/settings/handler.go | 5 +- api/http/handler/settings/settings_inspect.go | 2 +- api/http/handler/settings/settings_public.go | 2 +- api/http/handler/settings/settings_update.go | 10 +- .../handler/stacks/create_compose_stack.go | 24 +-- api/http/handler/stacks/create_swarm_stack.go | 24 +-- api/http/handler/stacks/handler.go | 21 +- api/http/handler/stacks/stack_create.go | 4 +- api/http/handler/stacks/stack_delete.go | 18 +- api/http/handler/stacks/stack_file.go | 6 +- api/http/handler/stacks/stack_inspect.go | 6 +- api/http/handler/stacks/stack_list.go | 8 +- api/http/handler/stacks/stack_migrate.go | 10 +- api/http/handler/stacks/stack_update.go | 8 +- api/http/handler/tags/handler.go | 7 +- api/http/handler/tags/tag_create.go | 4 +- api/http/handler/tags/tag_delete.go | 26 +-- api/http/handler/tags/tag_list.go | 2 +- api/http/handler/teammemberships/handler.go | 4 +- .../teammemberships/teammembership_create.go | 4 +- .../teammemberships/teammembership_delete.go | 4 +- .../teammemberships/teammembership_list.go | 2 +- .../teammemberships/teammembership_update.go | 4 +- api/http/handler/teams/handler.go | 5 +- api/http/handler/teams/team_create.go | 4 +- api/http/handler/teams/team_delete.go | 6 +- api/http/handler/teams/team_inspect.go | 2 +- api/http/handler/teams/team_list.go | 2 +- api/http/handler/teams/team_memberships.go | 2 +- api/http/handler/teams/team_update.go | 4 +- api/http/handler/templates/handler.go | 2 +- api/http/handler/templates/template_list.go | 2 +- api/http/handler/users/admin_check.go | 2 +- api/http/handler/users/admin_init.go | 4 +- api/http/handler/users/handler.go | 10 +- api/http/handler/users/user_create.go | 6 +- api/http/handler/users/user_delete.go | 8 +- api/http/handler/users/user_inspect.go | 2 +- api/http/handler/users/user_list.go | 2 +- api/http/handler/users/user_memberships.go | 2 +- api/http/handler/users/user_update.go | 4 +- .../handler/users/user_update_password.go | 4 +- api/http/handler/webhooks/handler.go | 3 +- api/http/handler/webhooks/webhook_create.go | 4 +- api/http/handler/webhooks/webhook_delete.go | 2 +- api/http/handler/webhooks/webhook_execute.go | 4 +- api/http/handler/webhooks/webhook_list.go | 2 +- api/http/handler/websocket/attach.go | 2 +- api/http/handler/websocket/exec.go | 2 +- api/http/handler/websocket/handler.go | 2 +- api/http/proxy/factory/docker.go | 17 +- .../proxy/factory/docker/access_control.go | 10 +- api/http/proxy/factory/docker/transport.go | 87 +++----- api/http/proxy/factory/docker_unix.go | 17 +- api/http/proxy/factory/docker_windows.go | 17 +- api/http/proxy/factory/factory.go | 47 +--- api/http/proxy/manager.go | 33 +-- api/http/security/bouncer.go | 56 ++--- api/http/server.go | 203 +++++------------- api/portainer.go | 25 ++- 151 files changed, 792 insertions(+), 1004 deletions(-) diff --git a/api/authorizations.go b/api/authorizations.go index 2309aec23..78b84b6bf 100644 --- a/api/authorizations.go +++ b/api/authorizations.go @@ -3,34 +3,13 @@ package portainer // AuthorizationService represents a service used to // update authorizations associated to a user or team. type AuthorizationService struct { - endpointService EndpointService - endpointGroupService EndpointGroupService - registryService RegistryService - roleService RoleService - teamMembershipService TeamMembershipService - userService UserService -} - -// AuthorizationServiceParameters are the required parameters -// used to create a new AuthorizationService. -type AuthorizationServiceParameters struct { - EndpointService EndpointService - EndpointGroupService EndpointGroupService - RegistryService RegistryService - RoleService RoleService - TeamMembershipService TeamMembershipService - UserService UserService + dataStore DataStore } // NewAuthorizationService returns a point to a new AuthorizationService instance. -func NewAuthorizationService(parameters *AuthorizationServiceParameters) *AuthorizationService { +func NewAuthorizationService(dataStore DataStore) *AuthorizationService { return &AuthorizationService{ - endpointService: parameters.EndpointService, - endpointGroupService: parameters.EndpointGroupService, - registryService: parameters.RegistryService, - roleService: parameters.RoleService, - teamMembershipService: parameters.TeamMembershipService, - userService: parameters.UserService, + dataStore: dataStore, } } @@ -449,7 +428,7 @@ func DefaultPortainerAuthorizations() Authorizations { // the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations // will be reset based for each role. func (service AuthorizationService) UpdateVolumeBrowsingAuthorizations(remove bool) error { - roles, err := service.roleService.Roles() + roles, err := service.dataStore.Role().Roles() if err != nil { return err } @@ -459,7 +438,7 @@ func (service AuthorizationService) UpdateVolumeBrowsingAuthorizations(remove bo if role.ID != RoleID(1) { updateRoleVolumeBrowsingAuthorizations(&role, remove) - err := service.roleService.UpdateRole(role.ID, &role) + err := service.dataStore.Role().UpdateRole(role.ID, &role) if err != nil { return err } @@ -492,7 +471,7 @@ func updateRoleVolumeBrowsingAuthorizations(role *Role, removeAuthorizations boo // RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) error { - endpoints, err := service.endpointService.Endpoints() + endpoints, err := service.dataStore.Endpoint().Endpoints() if err != nil { return err } @@ -502,7 +481,7 @@ func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) err if policyTeamID == teamID { delete(endpoint.TeamAccessPolicies, policyTeamID) - err := service.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) + err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { return err } @@ -512,7 +491,7 @@ func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) err } } - endpointGroups, err := service.endpointGroupService.EndpointGroups() + endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() if err != nil { return err } @@ -522,7 +501,7 @@ func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) err if policyTeamID == teamID { delete(endpointGroup.TeamAccessPolicies, policyTeamID) - err := service.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) if err != nil { return err } @@ -532,7 +511,7 @@ func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) err } } - registries, err := service.registryService.Registries() + registries, err := service.dataStore.Registry().Registries() if err != nil { return err } @@ -542,7 +521,7 @@ func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) err if policyTeamID == teamID { delete(registry.TeamAccessPolicies, policyTeamID) - err := service.registryService.UpdateRegistry(registry.ID, ®istry) + err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry) if err != nil { return err } @@ -557,7 +536,7 @@ func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) err // RemoveUserAccessPolicies will remove all existing access policies associated to the specified user func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) error { - endpoints, err := service.endpointService.Endpoints() + endpoints, err := service.dataStore.Endpoint().Endpoints() if err != nil { return err } @@ -567,7 +546,7 @@ func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) err if policyUserID == userID { delete(endpoint.UserAccessPolicies, policyUserID) - err := service.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) + err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { return err } @@ -577,7 +556,7 @@ func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) err } } - endpointGroups, err := service.endpointGroupService.EndpointGroups() + endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() if err != nil { return err } @@ -587,7 +566,7 @@ func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) err if policyUserID == userID { delete(endpointGroup.UserAccessPolicies, policyUserID) - err := service.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) if err != nil { return err } @@ -597,7 +576,7 @@ func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) err } } - registries, err := service.registryService.Registries() + registries, err := service.dataStore.Registry().Registries() if err != nil { return err } @@ -607,7 +586,7 @@ func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) err if policyUserID == userID { delete(registry.UserAccessPolicies, policyUserID) - err := service.registryService.UpdateRegistry(registry.ID, ®istry) + err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry) if err != nil { return err } @@ -622,7 +601,7 @@ func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) err // UpdateUsersAuthorizations will trigger an update of the authorizations for all the users. func (service *AuthorizationService) UpdateUsersAuthorizations() error { - users, err := service.userService.Users() + users, err := service.dataStore.User().Users() if err != nil { return err } @@ -638,7 +617,7 @@ func (service *AuthorizationService) UpdateUsersAuthorizations() error { } func (service *AuthorizationService) updateUserAuthorizations(userID UserID) error { - user, err := service.userService.User(userID) + user, err := service.dataStore.User().User(userID) if err != nil { return err } @@ -650,7 +629,7 @@ func (service *AuthorizationService) updateUserAuthorizations(userID UserID) err user.EndpointAuthorizations = endpointAuthorizations - return service.userService.UpdateUser(userID, user) + return service.dataStore.User().UpdateUser(userID, user) } func (service *AuthorizationService) getAuthorizations(user *User) (EndpointAuthorizations, error) { @@ -659,22 +638,22 @@ func (service *AuthorizationService) getAuthorizations(user *User) (EndpointAuth return endpointAuthorizations, nil } - userMemberships, err := service.teamMembershipService.TeamMembershipsByUserID(user.ID) + userMemberships, err := service.dataStore.TeamMembership().TeamMembershipsByUserID(user.ID) if err != nil { return endpointAuthorizations, err } - endpoints, err := service.endpointService.Endpoints() + endpoints, err := service.dataStore.Endpoint().Endpoints() if err != nil { return endpointAuthorizations, err } - endpointGroups, err := service.endpointGroupService.EndpointGroups() + endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() if err != nil { return endpointAuthorizations, err } - roles, err := service.roleService.Roles() + roles, err := service.dataStore.Role().Roles() if err != nil { return endpointAuthorizations, err } diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index c4b51ba31..476082604 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -41,7 +41,6 @@ type Store struct { db *bolt.DB isNew bool fileService portainer.FileService - RoleService *role.Service DockerHubService *dockerhub.Service EdgeGroupService *edgegroup.Service EdgeStackService *edgestack.Service @@ -51,6 +50,8 @@ type Store struct { ExtensionService *extension.Service RegistryService *registry.Service ResourceControlService *resourcecontrol.Service + RoleService *role.Service + ScheduleService *schedule.Service SettingsService *settings.Service StackService *stack.Service TagService *tag.Service @@ -60,7 +61,6 @@ type Store struct { UserService *user.Service VersionService *version.Service WebhookService *webhook.Service - ScheduleService *schedule.Service } // NewStore initializes a new Store and the associated services @@ -143,6 +143,7 @@ func (store *Store) MigrateData() error { UserService: store.UserService, VersionService: store.VersionService, FileService: store.fileService, + AuthorizationService: portainer.NewAuthorizationService(store), } migrator := migrator.NewMigrator(migratorParams) @@ -280,3 +281,103 @@ func (store *Store) initServices() error { return nil } + +// DockerHub gives access to the DockerHub data management layer +func (store *Store) DockerHub() portainer.DockerHubService { + return store.DockerHubService +} + +// EdgeGroup gives access to the EdgeGroup data management layer +func (store *Store) EdgeGroup() portainer.EdgeGroupService { + return store.EdgeGroupService +} + +// EdgeStack gives access to the EdgeStack data management layer +func (store *Store) EdgeStack() portainer.EdgeStackService { + return store.EdgeStackService +} + +// Endpoint gives access to the Endpoint data management layer +func (store *Store) Endpoint() portainer.EndpointService { + return store.EndpointService +} + +// EndpointGroup gives access to the EndpointGroup data management layer +func (store *Store) EndpointGroup() portainer.EndpointGroupService { + return store.EndpointGroupService +} + +// EndpointRelation gives access to the EndpointRelation data management layer +func (store *Store) EndpointRelation() portainer.EndpointRelationService { + return store.EndpointRelationService +} + +// Extension gives access to the Extension data management layer +func (store *Store) Extension() portainer.ExtensionService { + return store.ExtensionService +} + +// Registry gives access to the Registry data management layer +func (store *Store) Registry() portainer.RegistryService { + return store.RegistryService +} + +// ResourceControl gives access to the ResourceControl data management layer +func (store *Store) ResourceControl() portainer.ResourceControlService { + return store.ResourceControlService +} + +// Role gives access to the Role data management layer +func (store *Store) Role() portainer.RoleService { + return store.RoleService +} + +// Schedule gives access to the Schedule data management layer +func (store *Store) Schedule() portainer.ScheduleService { + return store.ScheduleService +} + +// Settings gives access to the Settings data management layer +func (store *Store) Settings() portainer.SettingsService { + return store.SettingsService +} + +// Stack gives access to the Stack data management layer +func (store *Store) Stack() portainer.StackService { + return store.StackService +} + +// Tag gives access to the Tag data management layer +func (store *Store) Tag() portainer.TagService { + return store.TagService +} + +// TeamMembership gives access to the TeamMembership data management layer +func (store *Store) TeamMembership() portainer.TeamMembershipService { + return store.TeamMembershipService +} + +// Team gives access to the Team data management layer +func (store *Store) Team() portainer.TeamService { + return store.TeamService +} + +// TunnelServer gives access to the TunnelServer data management layer +func (store *Store) TunnelServer() portainer.TunnelServerService { + return store.TunnelServerService +} + +// User gives access to the User data management layer +func (store *Store) User() portainer.UserService { + return store.UserService +} + +// Version gives access to the Version data management layer +func (store *Store) Version() portainer.VersionService { + return store.VersionService +} + +// Webhook gives access to the Webhook data management layer +func (store *Store) Webhook() portainer.WebhookService { + return store.WebhookService +} diff --git a/api/bolt/migrator/migrate_dbversion19.go b/api/bolt/migrator/migrate_dbversion19.go index 0692db5af..00a41a4e4 100644 --- a/api/bolt/migrator/migrate_dbversion19.go +++ b/api/bolt/migrator/migrate_dbversion19.go @@ -7,17 +7,7 @@ import ( ) func (m *Migrator) updateUsersToDBVersion20() error { - authorizationServiceParameters := &portainer.AuthorizationServiceParameters{ - EndpointService: m.endpointService, - EndpointGroupService: m.endpointGroupService, - RegistryService: m.registryService, - RoleService: m.roleService, - TeamMembershipService: m.teamMembershipService, - UserService: m.userService, - } - - authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters) - return authorizationService.UpdateUsersAuthorizations() + return m.authorizationService.UpdateUsersAuthorizations() } func (m *Migrator) updateSettingsToDBVersion20() error { diff --git a/api/bolt/migrator/migrate_dbversion20.go b/api/bolt/migrator/migrate_dbversion20.go index 1698f10c3..df272905a 100644 --- a/api/bolt/migrator/migrate_dbversion20.go +++ b/api/bolt/migrator/migrate_dbversion20.go @@ -74,16 +74,9 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error { readOnlyUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers) err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole) - - authorizationServiceParameters := &portainer.AuthorizationServiceParameters{ - EndpointService: m.endpointService, - EndpointGroupService: m.endpointGroupService, - RegistryService: m.registryService, - RoleService: m.roleService, - TeamMembershipService: m.teamMembershipService, - UserService: m.userService, + if err != nil { + return err } - authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters) - return authorizationService.UpdateUsersAuthorizations() + return m.authorizationService.UpdateUsersAuthorizations() } diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 1f4fa7bb0..0f72997fd 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -39,6 +39,7 @@ type ( userService *user.Service versionService *version.Service fileService portainer.FileService + authorizationService *portainer.AuthorizationService } // Parameters represents the required parameters to create a new Migrator instance. @@ -60,6 +61,7 @@ type ( UserService *user.Service VersionService *version.Service FileService portainer.FileService + AuthorizationService *portainer.AuthorizationService } ) @@ -83,12 +85,12 @@ func NewMigrator(parameters *Parameters) *Migrator { userService: parameters.UserService, versionService: parameters.VersionService, fileService: parameters.FileService, + authorizationService: parameters.AuthorizationService, } } // Migrate checks the database version and migrate the existing data to the most recent data model. func (m *Migrator) Migrate() error { - // Portainer < 1.12 if m.currentDBVersion < 1 { err := m.updateAdminUserToDBVersion1() diff --git a/api/chisel/service.go b/api/chisel/service.go index efb08db71..e007a416a 100644 --- a/api/chisel/service.go +++ b/api/chisel/service.go @@ -24,21 +24,19 @@ const ( // It is used to start a reverse tunnel server and to manage the connection status of each tunnel // connected to the tunnel server. type Service struct { - serverFingerprint string - serverPort string - tunnelDetailsMap cmap.ConcurrentMap - endpointService portainer.EndpointService - tunnelServerService portainer.TunnelServerService - snapshotter portainer.Snapshotter - chiselServer *chserver.Server + serverFingerprint string + serverPort string + tunnelDetailsMap cmap.ConcurrentMap + dataStore portainer.DataStore + snapshotter portainer.Snapshotter + chiselServer *chserver.Server } // NewService returns a pointer to a new instance of Service -func NewService(endpointService portainer.EndpointService, tunnelServerService portainer.TunnelServerService) *Service { +func NewService(dataStore portainer.DataStore) *Service { return &Service{ - tunnelDetailsMap: cmap.New(), - endpointService: endpointService, - tunnelServerService: tunnelServerService, + tunnelDetailsMap: cmap.New(), + dataStore: dataStore, } } @@ -89,7 +87,7 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotter portain func (service *Service) retrievePrivateKeySeed() (string, error) { var serverInfo *portainer.TunnelServerInfo - serverInfo, err := service.tunnelServerService.Info() + serverInfo, err := service.dataStore.TunnelServer().Info() if err == portainer.ErrObjectNotFound { keySeed := uniuri.NewLen(16) @@ -97,7 +95,7 @@ func (service *Service) retrievePrivateKeySeed() (string, error) { PrivateKeySeed: keySeed, } - err := service.tunnelServerService.UpdateInfo(serverInfo) + err := service.dataStore.TunnelServer().UpdateInfo(serverInfo) if err != nil { return "", err } @@ -173,7 +171,7 @@ func (service *Service) checkTunnels() { } func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error { - endpoint, err := service.endpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID) if err != nil { return err } @@ -187,5 +185,5 @@ func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tun endpoint.Snapshots = []portainer.Snapshot{*snapshot} endpoint.URL = endpointURL - return service.endpointService.UpdateEndpoint(endpoint.ID, endpoint) + return service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) } diff --git a/api/chisel/tunnel.go b/api/chisel/tunnel.go index e0cba1caf..ba9495419 100644 --- a/api/chisel/tunnel.go +++ b/api/chisel/tunnel.go @@ -97,7 +97,7 @@ func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointI tunnel := service.GetTunnelDetails(endpointID) if tunnel.Port == 0 { - endpoint, err := service.endpointService.Endpoint(endpointID) + endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID) if err != nil { return err } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 829d6b0b7..34eed9cef 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -25,13 +25,13 @@ import ( ) func initCLI() *portainer.CLIFlags { - var cli portainer.CLIService = &cli.Service{} - flags, err := cli.ParseFlags(portainer.APIVersion) + var cliService portainer.CLIService = &cli.Service{} + flags, err := cliService.ParseFlags(portainer.APIVersion) if err != nil { log.Fatal(err) } - err = cli.ValidateFlags(flags) + err = cliService.ValidateFlags(flags) if err != nil { log.Fatal(err) } @@ -46,7 +46,7 @@ func initFileService(dataStorePath string) portainer.FileService { return fileService } -func initStore(dataStorePath string, fileService portainer.FileService) *bolt.Store { +func initDataStore(dataStorePath string, fileService portainer.FileService) portainer.DataStore { store, err := bolt.NewStore(dataStorePath, fileService) if err != nil { log.Fatal(err) @@ -116,13 +116,13 @@ func initJobScheduler() portainer.JobScheduler { return cron.NewJobScheduler() } -func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter portainer.Snapshotter, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, settingsService portainer.SettingsService) error { - settings, err := settingsService.Settings() +func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter portainer.Snapshotter, dataStore portainer.DataStore) error { + settings, err := dataStore.Settings().Settings() if err != nil { return err } - schedules, err := scheduleService.SchedulesByJobType(portainer.SnapshotJobType) + schedules, err := dataStore.Schedule().SchedulesByJobType(portainer.SnapshotJobType) if err != nil { return err } @@ -131,7 +131,7 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter if len(schedules) == 0 { snapshotJob := &portainer.SnapshotJob{} snapshotSchedule = &portainer.Schedule{ - ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()), + ID: portainer.ScheduleID(dataStore.Schedule().GetNextIdentifier()), Name: "system_snapshot", CronExpression: "@every " + settings.SnapshotInterval, Recurring: true, @@ -143,7 +143,7 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter snapshotSchedule = &schedules[0] } - snapshotJobContext := cron.NewSnapshotJobContext(endpointService, snapshotter) + snapshotJobContext := cron.NewSnapshotJobContext(dataStore, snapshotter) snapshotJobRunner := cron.NewSnapshotJobRunner(snapshotSchedule, snapshotJobContext) err = jobScheduler.ScheduleJob(snapshotJobRunner) @@ -152,13 +152,13 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter } if len(schedules) == 0 { - return scheduleService.CreateSchedule(snapshotSchedule) + return dataStore.Schedule().CreateSchedule(snapshotSchedule) } return nil } -func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error { - schedules, err := scheduleService.Schedules() +func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, dataStore portainer.DataStore, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error { + schedules, err := dataStore.Schedule().Schedules() if err != nil { return err } @@ -166,7 +166,7 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p for _, schedule := range schedules { if schedule.JobType == portainer.ScriptExecutionJobType { - jobContext := cron.NewScriptExecutionJobContext(jobService, endpointService, fileService) + jobContext := cron.NewScriptExecutionJobContext(jobService, dataStore, fileService) jobRunner := cron.NewScriptExecutionJobRunner(&schedule, jobContext) err = jobScheduler.ScheduleJob(jobRunner) @@ -194,8 +194,8 @@ func initStatus(flags *portainer.CLIFlags) *portainer.Status { } } -func updateSettingsFromFlags(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error { - settings, err := settingsService.Settings() +func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLIFlags) error { + settings, err := dataStore.Settings().Settings() if err != nil { return err } @@ -211,7 +211,7 @@ func updateSettingsFromFlags(settingsService portainer.SettingsService, flags *p settings.BlackListedLabels = *flags.Labels } - return settingsService.UpdateSettings(settings) + return dataStore.Settings().UpdateSettings(settings) } func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error { @@ -243,7 +243,7 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D return generateAndStoreKeyPair(fileService, signatureService) } -func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error { +func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { tlsConfiguration := portainer.TLSConfiguration{ TLS: *flags.TLS, TLSSkipVerify: *flags.TLSSkipVerify, @@ -257,7 +257,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain tlsConfiguration.TLS = true } - endpointID := endpointService.GetNextIdentifier() + endpointID := dataStore.Endpoint().GetNextIdentifier() endpoint := &portainer.Endpoint{ ID: portainer.EndpointID(endpointID), Name: "primary", @@ -289,10 +289,10 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain } } - return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter) + return snapshotAndPersistEndpoint(endpoint, dataStore, snapshotter) } -func createUnsecuredEndpoint(endpointURL string, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error { +func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { if strings.HasPrefix(endpointURL, "tcp://") { _, err := client.ExecutePingOperation(endpointURL, nil) if err != nil { @@ -300,7 +300,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo } } - endpointID := endpointService.GetNextIdentifier() + endpointID := dataStore.Endpoint().GetNextIdentifier() endpoint := &portainer.Endpoint{ ID: portainer.EndpointID(endpointID), Name: "primary", @@ -316,10 +316,10 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo Snapshots: []portainer.Snapshot{}, } - return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter) + return snapshotAndPersistEndpoint(endpoint, dataStore, snapshotter) } -func snapshotAndPersistEndpoint(endpoint *portainer.Endpoint, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error { +func snapshotAndPersistEndpoint(endpoint *portainer.Endpoint, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { snapshot, err := snapshotter.CreateSnapshot(endpoint) endpoint.Status = portainer.EndpointStatusUp if err != nil { @@ -330,15 +330,15 @@ func snapshotAndPersistEndpoint(endpoint *portainer.Endpoint, endpointService po endpoint.Snapshots = []portainer.Snapshot{*snapshot} } - return endpointService.CreateEndpoint(endpoint) + return dataStore.Endpoint().CreateEndpoint(endpoint) } -func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) error { +func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { if *flags.EndpointURL == "" { return nil } - endpoints, err := endpointService.Endpoints() + endpoints, err := dataStore.Endpoint().Endpoints() if err != nil { return err } @@ -349,17 +349,17 @@ func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointS } if *flags.TLS || *flags.TLSSkipVerify { - return createTLSSecuredEndpoint(flags, endpointService, snapshotter) + return createTLSSecuredEndpoint(flags, dataStore, snapshotter) } - return createUnsecuredEndpoint(*flags.EndpointURL, endpointService, snapshotter) + return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotter) } func initJobService(dockerClientFactory *docker.ClientFactory) portainer.JobService { return docker.NewJobService(dockerClientFactory) } -func initExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) (portainer.ExtensionManager, error) { - extensionManager := exec.NewExtensionManager(fileService, extensionService) +func initExtensionManager(fileService portainer.FileService, dataStore portainer.DataStore) (portainer.ExtensionManager, error) { + extensionManager := exec.NewExtensionManager(fileService, dataStore) err := extensionManager.StartExtensions() if err != nil { @@ -369,11 +369,11 @@ func initExtensionManager(fileService portainer.FileService, extensionService po return extensionManager, nil } -func terminateIfNoAdminCreated(userService portainer.UserService) { +func terminateIfNoAdminCreated(dataStore portainer.DataStore) { timer1 := time.NewTimer(5 * time.Minute) <-timer1.C - users, err := userService.UsersByRole(portainer.AdministratorRole) + users, err := dataStore.User().UsersByRole(portainer.AdministratorRole) if err != nil { log.Fatal(err) } @@ -389,8 +389,8 @@ func main() { fileService := initFileService(*flags.Data) - store := initStore(*flags.Data, fileService) - defer store.Close() + dataStore := initDataStore(*flags.Data, fileService) + defer dataStore.Close() jwtService := initJWTService(!*flags.NoAuth) @@ -407,12 +407,12 @@ func main() { log.Fatal(err) } - extensionManager, err := initExtensionManager(fileService, store.ExtensionService) + extensionManager, err := initExtensionManager(fileService, dataStore) if err != nil { log.Fatal(err) } - reverseTunnelService := chisel.NewService(store.EndpointService, store.TunnelServerService) + reverseTunnelService := chisel.NewService(dataStore) clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService) @@ -427,8 +427,8 @@ func main() { composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService) - if store.IsNew() { - err = updateSettingsFromFlags(store.SettingsService, flags) + if dataStore.IsNew() { + err = updateSettingsFromFlags(dataStore, flags) if err != nil { log.Fatal(err) } @@ -436,12 +436,12 @@ func main() { jobScheduler := initJobScheduler() - err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService, reverseTunnelService) + err = loadSchedulesFromDatabase(jobScheduler, jobService, dataStore, fileService, reverseTunnelService) if err != nil { log.Fatal(err) } - err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService) + err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, dataStore) if err != nil { log.Fatal(err) } @@ -450,7 +450,7 @@ func main() { applicationStatus := initStatus(flags) - err = initEndpoint(flags, store.EndpointService, snapshotter) + err = initEndpoint(flags, dataStore, snapshotter) if err != nil { log.Fatal(err) } @@ -470,7 +470,7 @@ func main() { } if adminPasswordHash != "" { - users, err := store.UserService.UsersByRole(portainer.AdministratorRole) + users, err := dataStore.User().UsersByRole(portainer.AdministratorRole) if err != nil { log.Fatal(err) } @@ -483,7 +483,7 @@ func main() { Password: adminPasswordHash, PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), } - err := store.UserService.CreateUser(user) + err := dataStore.User().CreateUser(user) if err != nil { log.Fatal(err) } @@ -493,7 +493,7 @@ func main() { } if !*flags.NoAuth { - go terminateIfNoAdminCreated(store.UserService) + go terminateIfNoAdminCreated(dataStore) } err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter) @@ -502,45 +502,28 @@ func main() { } var server portainer.Server = &http.Server{ - ReverseTunnelService: reverseTunnelService, - Status: applicationStatus, - BindAddress: *flags.Addr, - AssetsPath: *flags.Assets, - AuthDisabled: *flags.NoAuth, - RoleService: store.RoleService, - UserService: store.UserService, - TeamService: store.TeamService, - TeamMembershipService: store.TeamMembershipService, - EdgeGroupService: store.EdgeGroupService, - EdgeStackService: store.EdgeStackService, - EndpointService: store.EndpointService, - EndpointGroupService: store.EndpointGroupService, - EndpointRelationService: store.EndpointRelationService, - ExtensionService: store.ExtensionService, - ResourceControlService: store.ResourceControlService, - SettingsService: store.SettingsService, - RegistryService: store.RegistryService, - DockerHubService: store.DockerHubService, - StackService: store.StackService, - ScheduleService: store.ScheduleService, - TagService: store.TagService, - WebhookService: store.WebhookService, - SwarmStackManager: swarmStackManager, - ComposeStackManager: composeStackManager, - ExtensionManager: extensionManager, - CryptoService: cryptoService, - JWTService: jwtService, - FileService: fileService, - LDAPService: ldapService, - GitService: gitService, - SignatureService: digitalSignatureService, - JobScheduler: jobScheduler, - Snapshotter: snapshotter, - SSL: *flags.SSL, - SSLCert: *flags.SSLCert, - SSLKey: *flags.SSLKey, - DockerClientFactory: clientFactory, - JobService: jobService, + ReverseTunnelService: reverseTunnelService, + Status: applicationStatus, + BindAddress: *flags.Addr, + AssetsPath: *flags.Assets, + AuthDisabled: *flags.NoAuth, + DataStore: dataStore, + SwarmStackManager: swarmStackManager, + ComposeStackManager: composeStackManager, + ExtensionManager: extensionManager, + CryptoService: cryptoService, + JWTService: jwtService, + FileService: fileService, + LDAPService: ldapService, + GitService: gitService, + SignatureService: digitalSignatureService, + JobScheduler: jobScheduler, + Snapshotter: snapshotter, + SSL: *flags.SSL, + SSLCert: *flags.SSLCert, + SSLKey: *flags.SSLKey, + DockerClientFactory: clientFactory, + JobService: jobService, } log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) diff --git a/api/cron/job_script_execution.go b/api/cron/job_script_execution.go index f9634813f..b554e6047 100644 --- a/api/cron/job_script_execution.go +++ b/api/cron/job_script_execution.go @@ -16,17 +16,17 @@ type ScriptExecutionJobRunner struct { // ScriptExecutionJobContext represents the context of execution of a ScriptExecutionJob type ScriptExecutionJobContext struct { - jobService portainer.JobService - endpointService portainer.EndpointService - fileService portainer.FileService + dataStore portainer.DataStore + jobService portainer.JobService + fileService portainer.FileService } // NewScriptExecutionJobContext returns a new context that can be used to execute a ScriptExecutionJob -func NewScriptExecutionJobContext(jobService portainer.JobService, endpointService portainer.EndpointService, fileService portainer.FileService) *ScriptExecutionJobContext { +func NewScriptExecutionJobContext(jobService portainer.JobService, dataStore portainer.DataStore, fileService portainer.FileService) *ScriptExecutionJobContext { return &ScriptExecutionJobContext{ - jobService: jobService, - endpointService: endpointService, - fileService: fileService, + jobService: jobService, + dataStore: dataStore, + fileService: fileService, } } @@ -56,7 +56,7 @@ func (runner *ScriptExecutionJobRunner) Run() { targets := make([]*portainer.Endpoint, 0) for _, endpointID := range runner.schedule.ScriptExecutionJob.Endpoints { - endpoint, err := runner.context.endpointService.Endpoint(endpointID) + endpoint, err := runner.context.dataStore.Endpoint().Endpoint(endpointID) if err != nil { log.Printf("scheduled job error (script execution). Unable to retrieve information about endpoint (id=%d) (err=%s)\n", endpointID, err) return diff --git a/api/cron/job_snapshot.go b/api/cron/job_snapshot.go index b3dcc9b74..e9c6e602b 100644 --- a/api/cron/job_snapshot.go +++ b/api/cron/job_snapshot.go @@ -14,15 +14,15 @@ type SnapshotJobRunner struct { // SnapshotJobContext represents the context of execution of a SnapshotJob type SnapshotJobContext struct { - endpointService portainer.EndpointService - snapshotter portainer.Snapshotter + dataStore portainer.DataStore + snapshotter portainer.Snapshotter } // NewSnapshotJobContext returns a new context that can be used to execute a SnapshotJob -func NewSnapshotJobContext(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) *SnapshotJobContext { +func NewSnapshotJobContext(dataStore portainer.DataStore, snapshotter portainer.Snapshotter) *SnapshotJobContext { return &SnapshotJobContext{ - endpointService: endpointService, - snapshotter: snapshotter, + dataStore: dataStore, + snapshotter: snapshotter, } } @@ -46,7 +46,7 @@ func (runner *SnapshotJobRunner) GetSchedule() *portainer.Schedule { // retrieve the latest version of the endpoint right after a snapshot. func (runner *SnapshotJobRunner) Run() { go func() { - endpoints, err := runner.context.endpointService.Endpoints() + endpoints, err := runner.context.dataStore.Endpoint().Endpoints() if err != nil { log.Printf("background schedule error (endpoint snapshot). Unable to retrieve endpoint list (err=%s)\n", err) return @@ -59,7 +59,7 @@ func (runner *SnapshotJobRunner) Run() { snapshot, snapshotError := runner.context.snapshotter.CreateSnapshot(&endpoint) - latestEndpointReference, err := runner.context.endpointService.Endpoint(endpoint.ID) + latestEndpointReference, err := runner.context.dataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) continue @@ -75,7 +75,7 @@ func (runner *SnapshotJobRunner) Run() { latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} } - err = runner.context.endpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) + err = runner.context.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { log.Printf("background schedule error (endpoint snapshot). Unable to update endpoint (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) return diff --git a/api/exec/extension.go b/api/exec/extension.go index e41cf1f49..d70cd98bb 100644 --- a/api/exec/extension.go +++ b/api/exec/extension.go @@ -34,17 +34,17 @@ var extensionBinaryMap = map[portainer.ExtensionID]string{ // ExtensionManager represents a service used to // manage extension processes. type ExtensionManager struct { - processes cmap.ConcurrentMap - fileService portainer.FileService - extensionService portainer.ExtensionService + processes cmap.ConcurrentMap + fileService portainer.FileService + dataStore portainer.DataStore } // NewExtensionManager returns a pointer to an ExtensionManager -func NewExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) *ExtensionManager { +func NewExtensionManager(fileService portainer.FileService, dataStore portainer.DataStore) *ExtensionManager { return &ExtensionManager{ - processes: cmap.New(), - fileService: fileService, - extensionService: extensionService, + processes: cmap.New(), + fileService: fileService, + dataStore: dataStore, } } @@ -188,7 +188,7 @@ func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension // The purpose of this function is to be ran at startup, as such most of the error handling won't block the program execution // and will log warning messages instead. func (manager *ExtensionManager) StartExtensions() error { - extensions, err := manager.extensionService.Extensions() + extensions, err := manager.dataStore.Extension().Extensions() if err != nil { return err } @@ -224,7 +224,7 @@ func (manager *ExtensionManager) updateAndStartExtensions(extensions []portainer } } - err := manager.extensionService.Persist(&extension) + err := manager.dataStore.Extension().Persist(&extension) if err != nil { return err } diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index 8e51f65db..1db970bb3 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -42,12 +42,12 @@ func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } - u, err := handler.UserService.UserByUsername(payload.Username) + u, err := handler.DataStore.User().UserByUsername(payload.Username) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err} } @@ -108,7 +108,7 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), } - err = handler.UserService.CreateUser(user) + err = handler.DataStore.User().CreateUser(user) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err} } @@ -146,7 +146,7 @@ func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *p } func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portainer.LDAPSettings) error { - teams, err := handler.TeamService.Teams() + teams, err := handler.DataStore.Team().Teams() if err != nil { return err } @@ -156,7 +156,7 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain return err } - userMemberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(user.ID) + userMemberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(user.ID) if err != nil { return err } @@ -174,7 +174,7 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain Role: portainer.TeamMember, } - err := handler.TeamMembershipService.CreateTeamMembership(membership) + err := handler.DataStore.TeamMembership().CreateTeamMembership(membership) if err != nil { return err } diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index de0cbd8dc..757f3fd76 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -78,7 +78,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } @@ -87,7 +87,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", portainer.Error("OAuth authentication is not enabled")} } - extension, err := handler.ExtensionService.Extension(portainer.OAuthAuthenticationExtension) + extension, err := handler.DataStore.Extension().Extension(portainer.OAuthAuthenticationExtension) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err} } else if err != nil { @@ -100,7 +100,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", portainer.ErrUnauthorized} } - user, err := handler.UserService.UserByUsername(username) + user, err := handler.DataStore.User().UserByUsername(username) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err} } @@ -116,7 +116,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), } - err = handler.UserService.CreateUser(user) + err = handler.DataStore.User().CreateUser(user) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err} } @@ -128,7 +128,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h Role: portainer.TeamMember, } - err = handler.TeamMembershipService.CreateTeamMembership(membership) + err = handler.DataStore.TeamMembership().CreateTeamMembership(membership) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err} } diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index 24a211f94..345884980 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -11,8 +11,6 @@ import ( ) const ( - // ErrInvalidCredentials is an error raised when credentials for a user are invalid - ErrInvalidCredentials = portainer.Error("Invalid credentials") // ErrAuthDisabled is an error raised when trying to access the authentication endpoints // when the server has been started with the --no-auth flag ErrAuthDisabled = portainer.Error("Authentication is disabled") @@ -21,20 +19,13 @@ const ( // Handler is the HTTP handler used to handle authentication operations. type Handler struct { *mux.Router - authDisabled bool - UserService portainer.UserService - CryptoService portainer.CryptoService - JWTService portainer.JWTService - LDAPService portainer.LDAPService - SettingsService portainer.SettingsService - TeamService portainer.TeamService - TeamMembershipService portainer.TeamMembershipService - ExtensionService portainer.ExtensionService - EndpointService portainer.EndpointService - EndpointGroupService portainer.EndpointGroupService - RoleService portainer.RoleService - ProxyManager *proxy.Manager - AuthorizationService *portainer.AuthorizationService + authDisabled bool + DataStore portainer.DataStore + CryptoService portainer.CryptoService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + ProxyManager *proxy.Manager + AuthorizationService *portainer.AuthorizationService } // NewHandler creates a handler to manage authentication operations. diff --git a/api/http/handler/dockerhub/dockerhub_inspect.go b/api/http/handler/dockerhub/dockerhub_inspect.go index b149a2a35..fd4713849 100644 --- a/api/http/handler/dockerhub/dockerhub_inspect.go +++ b/api/http/handler/dockerhub/dockerhub_inspect.go @@ -9,7 +9,7 @@ import ( // GET request on /api/dockerhub func (handler *Handler) dockerhubInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - dockerhub, err := handler.DockerHubService.DockerHub() + dockerhub, err := handler.DataStore.DockerHub().DockerHub() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err} } diff --git a/api/http/handler/dockerhub/dockerhub_update.go b/api/http/handler/dockerhub/dockerhub_update.go index 5606677fb..78787566e 100644 --- a/api/http/handler/dockerhub/dockerhub_update.go +++ b/api/http/handler/dockerhub/dockerhub_update.go @@ -43,7 +43,7 @@ func (handler *Handler) dockerhubUpdate(w http.ResponseWriter, r *http.Request) dockerhub.Password = payload.Password } - err = handler.DockerHubService.UpdateDockerHub(dockerhub) + err = handler.DataStore.DockerHub().UpdateDockerHub(dockerhub) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Dockerhub changes inside the database", err} } diff --git a/api/http/handler/dockerhub/handler.go b/api/http/handler/dockerhub/handler.go index ba4ed2c34..f1328acb8 100644 --- a/api/http/handler/dockerhub/handler.go +++ b/api/http/handler/dockerhub/handler.go @@ -16,7 +16,7 @@ func hideFields(dockerHub *portainer.DockerHub) { // Handler is the HTTP handler used to handle DockerHub operations. type Handler struct { *mux.Router - DockerHubService portainer.DockerHubService + DataStore portainer.DataStore } // NewHandler creates a handler to manage Dockerhub operations. diff --git a/api/http/handler/edgegroups/associated_endpoints.go b/api/http/handler/edgegroups/associated_endpoints.go index 8ff2f7693..8eeed5620 100644 --- a/api/http/handler/edgegroups/associated_endpoints.go +++ b/api/http/handler/edgegroups/associated_endpoints.go @@ -11,7 +11,7 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc return []portainer.EndpointID{}, nil } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return nil, err } @@ -20,7 +20,7 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc tags := []portainer.Tag{} for _, tagID := range tagIDs { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return nil, err } diff --git a/api/http/handler/edgegroups/edgegroup_create.go b/api/http/handler/edgegroups/edgegroup_create.go index 26bbd0d90..fc0d09d37 100644 --- a/api/http/handler/edgegroups/edgegroup_create.go +++ b/api/http/handler/edgegroups/edgegroup_create.go @@ -38,7 +38,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err} } @@ -62,7 +62,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) } else { endpointIDs := []portainer.EndpointID{} for _, endpointID := range payload.Endpoints { - endpoint, err := handler.EndpointService.Endpoint(endpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} } @@ -74,7 +74,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) edgeGroup.Endpoints = endpointIDs } - err = handler.EdgeGroupService.CreateEdgeGroup(edgeGroup) + err = handler.DataStore.EdgeGroup().CreateEdgeGroup(edgeGroup) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Edge group inside the database", err} } diff --git a/api/http/handler/edgegroups/edgegroup_delete.go b/api/http/handler/edgegroups/edgegroup_delete.go index 8ad9e949f..806888617 100644 --- a/api/http/handler/edgegroups/edgegroup_delete.go +++ b/api/http/handler/edgegroups/edgegroup_delete.go @@ -15,14 +15,14 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err} } - _, err = handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) + _, err = handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err} } - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err} } @@ -35,7 +35,7 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) } } - err = handler.EdgeGroupService.DeleteEdgeGroup(portainer.EdgeGroupID(edgeGroupID)) + err = handler.DataStore.EdgeGroup().DeleteEdgeGroup(portainer.EdgeGroupID(edgeGroupID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge group from the database", err} } diff --git a/api/http/handler/edgegroups/edgegroup_inspect.go b/api/http/handler/edgegroups/edgegroup_inspect.go index 5fdadf2ec..db88789c1 100644 --- a/api/http/handler/edgegroups/edgegroup_inspect.go +++ b/api/http/handler/edgegroups/edgegroup_inspect.go @@ -15,7 +15,7 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err} } - edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) + edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/edgegroups/edgegroup_list.go b/api/http/handler/edgegroups/edgegroup_list.go index f859300aa..7ba9fbdcf 100644 --- a/api/http/handler/edgegroups/edgegroup_list.go +++ b/api/http/handler/edgegroups/edgegroup_list.go @@ -14,12 +14,12 @@ type decoratedEdgeGroup struct { } func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err} } - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err} } diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index d6a72ea6d..6c227b11b 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -43,7 +43,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) + edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err} } else if err != nil { @@ -51,7 +51,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) } if payload.Name != "" { - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err} } @@ -63,12 +63,12 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) edgeGroup.Name = payload.Name } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} } - endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} } @@ -81,7 +81,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) } else { endpointIDs := []portainer.EndpointID{} for _, endpointID := range payload.Endpoints { - endpoint, err := handler.EndpointService.Endpoint(endpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} } @@ -97,7 +97,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) edgeGroup.PartialMatch = *payload.PartialMatch } - err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup) + err = handler.DataStore.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge group changes inside the database", err} } @@ -116,27 +116,27 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) } func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error { - relation, err := handler.EndpointRelationService.EndpointRelation(endpointID) + relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { return err } - endpoint, err := handler.EndpointService.Endpoint(endpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err != nil { return err } - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID) if err != nil { return err } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return err } - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return err } @@ -150,5 +150,5 @@ func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error { relation.EdgeStacks = edgeStackSet - return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, relation) + return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation) } diff --git a/api/http/handler/edgegroups/handler.go b/api/http/handler/edgegroups/handler.go index 874b13477..5f8446f8a 100644 --- a/api/http/handler/edgegroups/handler.go +++ b/api/http/handler/edgegroups/handler.go @@ -12,12 +12,7 @@ import ( // Handler is the HTTP handler used to handle endpoint group operations. type Handler struct { *mux.Router - EdgeGroupService portainer.EdgeGroupService - EdgeStackService portainer.EdgeStackService - EndpointService portainer.EndpointService - EndpointGroupService portainer.EndpointGroupService - EndpointRelationService portainer.EndpointRelationService - TagService portainer.TagService + DataStore portainer.DataStore } // NewHandler creates a handler to manage endpoint group operations. diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index e3a315cb9..27f7ce87b 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -27,17 +27,17 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Edge stack", err} } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} } - endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} } @@ -45,14 +45,14 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups) for _, endpointID := range relatedEndpoints { - relation, err := handler.EndpointRelationService.EndpointRelation(endpointID) + relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err} } relation.EdgeStacks[edgeStack.ID] = true - err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation) + err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err} } @@ -104,7 +104,7 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta return nil, err } - stackID := handler.EdgeStackService.GetNextIdentifier() + stackID := handler.DataStore.EdgeStack().GetNextIdentifier() stack := &portainer.EdgeStack{ ID: portainer.EdgeStackID(stackID), Name: payload.Name, @@ -122,7 +122,7 @@ func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*porta } stack.ProjectPath = projectPath - err = handler.EdgeStackService.CreateEdgeStack(stack) + err = handler.DataStore.EdgeStack().CreateEdgeStack(stack) if err != nil { return nil, err } @@ -172,7 +172,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por return nil, err } - stackID := handler.EdgeStackService.GetNextIdentifier() + stackID := handler.DataStore.EdgeStack().GetNextIdentifier() stack := &portainer.EdgeStack{ ID: portainer.EdgeStackID(stackID), Name: payload.Name, @@ -200,7 +200,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*por return nil, err } - err = handler.EdgeStackService.CreateEdgeStack(stack) + err = handler.DataStore.EdgeStack().CreateEdgeStack(stack) if err != nil { return nil, err } @@ -248,7 +248,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai return nil, err } - stackID := handler.EdgeStackService.GetNextIdentifier() + stackID := handler.DataStore.EdgeStack().GetNextIdentifier() stack := &portainer.EdgeStack{ ID: portainer.EdgeStackID(stackID), Name: payload.Name, @@ -266,7 +266,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai } stack.ProjectPath = projectPath - err = handler.EdgeStackService.CreateEdgeStack(stack) + err = handler.DataStore.EdgeStack().CreateEdgeStack(stack) if err != nil { return nil, err } @@ -275,7 +275,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portai } func (handler *Handler) validateUniqueName(name string) error { - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return err } diff --git a/api/http/handler/edgestacks/edgestack_delete.go b/api/http/handler/edgestacks/edgestack_delete.go index ae5d1b476..cec2a3c82 100644 --- a/api/http/handler/edgestacks/edgestack_delete.go +++ b/api/http/handler/edgestacks/edgestack_delete.go @@ -15,29 +15,29 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err} } - edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID)) + edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err} } - err = handler.EdgeStackService.DeleteEdgeStack(portainer.EdgeStackID(edgeStackID)) + err = handler.DataStore.EdgeStack().DeleteEdgeStack(portainer.EdgeStackID(edgeStackID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the edge stack from the database", err} } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} } - endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} } @@ -45,14 +45,14 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups) for _, endpointID := range relatedEndpoints { - relation, err := handler.EndpointRelationService.EndpointRelation(endpointID) + relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err} } delete(relation.EdgeStacks, edgeStack.ID) - err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation) + err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err} } diff --git a/api/http/handler/edgestacks/edgestack_file.go b/api/http/handler/edgestacks/edgestack_file.go index c82348b8d..bcb20f626 100644 --- a/api/http/handler/edgestacks/edgestack_file.go +++ b/api/http/handler/edgestacks/edgestack_file.go @@ -21,7 +21,7 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err} } - stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID)) + stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/edgestacks/edgestack_inspect.go b/api/http/handler/edgestacks/edgestack_inspect.go index 66a591633..ae417e603 100644 --- a/api/http/handler/edgestacks/edgestack_inspect.go +++ b/api/http/handler/edgestacks/edgestack_inspect.go @@ -15,7 +15,7 @@ func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err} } - edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID)) + edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/edgestacks/edgestack_list.go b/api/http/handler/edgestacks/edgestack_list.go index dd15e58c1..1db0159c6 100644 --- a/api/http/handler/edgestacks/edgestack_list.go +++ b/api/http/handler/edgestacks/edgestack_list.go @@ -8,7 +8,7 @@ import ( ) func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err} } diff --git a/api/http/handler/edgestacks/edgestack_status_update.go b/api/http/handler/edgestacks/edgestack_status_update.go index bcf0a639b..5d5c3ebda 100644 --- a/api/http/handler/edgestacks/edgestack_status_update.go +++ b/api/http/handler/edgestacks/edgestack_status_update.go @@ -35,7 +35,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} } - stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID)) + stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { @@ -48,7 +48,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(*payload.EndpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(*payload.EndpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -66,7 +66,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req EndpointID: *payload.EndpointID, } - err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack) + err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err} } diff --git a/api/http/handler/edgestacks/edgestack_update.go b/api/http/handler/edgestacks/edgestack_update.go index 404ff1d92..3d7ac1ddc 100644 --- a/api/http/handler/edgestacks/edgestack_update.go +++ b/api/http/handler/edgestacks/edgestack_update.go @@ -34,7 +34,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} } - stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID)) + stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { @@ -48,17 +48,17 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) } if payload.EdgeGroups != nil { - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err} } - endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} } @@ -84,14 +84,14 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) } for endpointID := range endpointsToRemove { - relation, err := handler.EndpointRelationService.EndpointRelation(endpointID) + relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err} } delete(relation.EdgeStacks, stack.ID) - err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation) + err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err} } @@ -105,14 +105,14 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) } for endpointID := range endpointsToAdd { - relation, err := handler.EndpointRelationService.EndpointRelation(endpointID) + relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err} } relation.EdgeStacks[stack.ID] = true - err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation) + err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpointID, relation) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err} } @@ -137,7 +137,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) stack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{} } - err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack) + err = handler.DataStore.EdgeStack().UpdateEdgeStack(stack.ID, stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err} } diff --git a/api/http/handler/edgestacks/handler.go b/api/http/handler/edgestacks/handler.go index 45c823e5c..3c75f837e 100644 --- a/api/http/handler/edgestacks/handler.go +++ b/api/http/handler/edgestacks/handler.go @@ -12,14 +12,10 @@ import ( // Handler is the HTTP handler used to handle endpoint group operations. type Handler struct { *mux.Router - requestBouncer *security.RequestBouncer - EdgeGroupService portainer.EdgeGroupService - EdgeStackService portainer.EdgeStackService - EndpointService portainer.EndpointService - EndpointGroupService portainer.EndpointGroupService - EndpointRelationService portainer.EndpointRelationService - FileService portainer.FileService - GitService portainer.GitService + requestBouncer *security.RequestBouncer + DataStore portainer.DataStore + FileService portainer.FileService + GitService portainer.GitService } // NewHandler creates a handler to manage endpoint group operations. diff --git a/api/http/handler/edgetemplates/edgetemplate_list.go b/api/http/handler/edgetemplates/edgetemplate_list.go index 2afb90f6d..00f271dbb 100644 --- a/api/http/handler/edgetemplates/edgetemplate_list.go +++ b/api/http/handler/edgetemplates/edgetemplate_list.go @@ -17,7 +17,7 @@ type templateFileFormat struct { // GET request on /api/edgetemplates func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } diff --git a/api/http/handler/edgetemplates/handler.go b/api/http/handler/edgetemplates/handler.go index 75473c49a..963ddb931 100644 --- a/api/http/handler/edgetemplates/handler.go +++ b/api/http/handler/edgetemplates/handler.go @@ -13,8 +13,8 @@ import ( // Handler is the HTTP handler used to handle edge endpoint operations. type Handler struct { *mux.Router - requestBouncer *security.RequestBouncer - SettingsService portainer.SettingsService + requestBouncer *security.RequestBouncer + DataStore portainer.DataStore } // NewHandler creates a handler to manage endpoint operations. diff --git a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go index 3853e3bba..54af3f3ef 100644 --- a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go +++ b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go @@ -23,7 +23,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -40,7 +40,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err} } - edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID)) + edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/endpointedge/handler.go b/api/http/handler/endpointedge/handler.go index e8dfc2995..14aff281e 100644 --- a/api/http/handler/endpointedge/handler.go +++ b/api/http/handler/endpointedge/handler.go @@ -13,10 +13,9 @@ import ( // Handler is the HTTP handler used to handle edge endpoint operations. type Handler struct { *mux.Router - requestBouncer *security.RequestBouncer - EndpointService portainer.EndpointService - EdgeStackService portainer.EdgeStackService - FileService portainer.FileService + requestBouncer *security.RequestBouncer + DataStore portainer.DataStore + FileService portainer.FileService } // NewHandler creates a handler to manage endpoint operations. diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index f296fee64..d77464744 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -43,12 +43,12 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque TagIDs: payload.TagIDs, } - err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup) + err = handler.DataStore.EndpointGroup().CreateEndpointGroup(endpointGroup) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the endpoint group inside the database", err} } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} } @@ -58,7 +58,7 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque if endpoint.ID == id { endpoint.GroupID = endpointGroup.ID - err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} } @@ -74,14 +74,14 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque } for _, tagID := range endpointGroup.TagIDs { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tag from the database", err} } tag.EndpointGroups[endpointGroup.ID] = true - err = handler.TagService.UpdateTag(tagID, tag) + err = handler.DataStore.Tag().UpdateTag(tagID, tag) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err} } diff --git a/api/http/handler/endpointgroups/endpointgroup_delete.go b/api/http/handler/endpointgroups/endpointgroup_delete.go index 76cd4ce86..0dba168eb 100644 --- a/api/http/handler/endpointgroups/endpointgroup_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_delete.go @@ -20,19 +20,19 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", portainer.ErrCannotRemoveDefaultGroup} } - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} } - err = handler.EndpointGroupService.DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID)) + err = handler.DataStore.EndpointGroup().DeleteEndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the endpoint group from the database", err} } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} } @@ -42,7 +42,7 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) { updateAuthorizations = true endpoint.GroupID = portainer.EndpointGroupID(1) - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} } @@ -62,14 +62,14 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque } for _, tagID := range endpointGroup.TagIDs { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tag from the database", err} } delete(tag.EndpointGroups, endpointGroup.ID) - err = handler.TagService.UpdateTag(tagID, tag) + err = handler.DataStore.Tag().UpdateTag(tagID, tag) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err} } diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go index f2435ab33..710aa7df5 100644 --- a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go @@ -21,14 +21,14 @@ func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -37,7 +37,7 @@ func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http. endpoint.GroupID = endpointGroup.ID - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} } diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go index 0e4a21611..f235b3ab9 100644 --- a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go @@ -21,14 +21,14 @@ func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *ht return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - _, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) + _, err = handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -37,7 +37,7 @@ func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *ht endpoint.GroupID = portainer.EndpointGroupID(1) - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} } diff --git a/api/http/handler/endpointgroups/endpointgroup_inspect.go b/api/http/handler/endpointgroups/endpointgroup_inspect.go index 3ddc464e7..b931ba82b 100644 --- a/api/http/handler/endpointgroups/endpointgroup_inspect.go +++ b/api/http/handler/endpointgroups/endpointgroup_inspect.go @@ -16,7 +16,7 @@ func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Requ return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} } - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/endpointgroups/endpointgroup_list.go b/api/http/handler/endpointgroups/endpointgroup_list.go index 7ea73d2d5..e4e1bb2da 100644 --- a/api/http/handler/endpointgroups/endpointgroup_list.go +++ b/api/http/handler/endpointgroups/endpointgroup_list.go @@ -10,7 +10,7 @@ import ( // GET request on /api/endpoint_groups func (handler *Handler) endpointGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err} } diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 362b8b697..e7d00d41c 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -35,7 +35,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { @@ -62,12 +62,12 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque removeTags := portainer.TagDifference(endpointGroupTagSet, payloadTagSet) for tagID := range removeTags { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err} } delete(tag.EndpointGroups, endpointGroup.ID) - err = handler.TagService.UpdateTag(tag.ID, tag) + err = handler.DataStore.Tag().UpdateTag(tag.ID, tag) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err} } @@ -75,14 +75,14 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque endpointGroup.TagIDs = payload.TagIDs for _, tagID := range payload.TagIDs { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err} } tag.EndpointGroups[endpointGroup.ID] = true - err = handler.TagService.UpdateTag(tag.ID, tag) + err = handler.DataStore.Tag().UpdateTag(tag.ID, tag) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err} } @@ -101,7 +101,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque updateAuthorizations = true } - err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup) + err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err} } @@ -114,7 +114,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque } if tagsChanged { - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} diff --git a/api/http/handler/endpointgroups/endpoints.go b/api/http/handler/endpointgroups/endpoints.go index 11e760e15..3ff3096cb 100644 --- a/api/http/handler/endpointgroups/endpoints.go +++ b/api/http/handler/endpointgroups/endpoints.go @@ -8,7 +8,7 @@ func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, en } if endpointGroup == nil { - unassignedGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(1)) + unassignedGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(1)) if err != nil { return err } @@ -16,17 +16,17 @@ func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, en endpointGroup = unassignedGroup } - endpointRelation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID) + endpointRelation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID) if err != nil { return err } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return err } - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return err } @@ -38,5 +38,5 @@ func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, en } endpointRelation.EdgeStacks = stacksSet - return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, endpointRelation) + return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation) } diff --git a/api/http/handler/endpointgroups/handler.go b/api/http/handler/endpointgroups/handler.go index a738a2dc1..9730e875d 100644 --- a/api/http/handler/endpointgroups/handler.go +++ b/api/http/handler/endpointgroups/handler.go @@ -12,13 +12,8 @@ import ( // Handler is the HTTP handler used to handle endpoint group operations. type Handler struct { *mux.Router - AuthorizationService *portainer.AuthorizationService - EdgeGroupService portainer.EdgeGroupService - EdgeStackService portainer.EdgeStackService - EndpointService portainer.EndpointService - EndpointGroupService portainer.EndpointGroupService - EndpointRelationService portainer.EndpointRelationService - TagService portainer.TagService + DataStore portainer.DataStore + AuthorizationService *portainer.AuthorizationService } // NewHandler creates a handler to manage endpoint group operations. diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index ed81a6527..394fa0b54 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -11,9 +11,8 @@ import ( // Handler is the HTTP handler used to proxy requests to external APIs. type Handler struct { *mux.Router + DataStore portainer.DataStore requestBouncer *security.RequestBouncer - EndpointService portainer.EndpointService - SettingsService portainer.SettingsService ProxyManager *proxy.Manager ReverseTunnelService portainer.ReverseTunnelService } diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index d84aacc62..8a228fcf4 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -18,7 +18,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -44,7 +44,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err} } - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } diff --git a/api/http/handler/endpointproxy/proxy_storidge.go b/api/http/handler/endpointproxy/proxy_storidge.go index f2d2aacb1..70a019a83 100644 --- a/api/http/handler/endpointproxy/proxy_storidge.go +++ b/api/http/handler/endpointproxy/proxy_storidge.go @@ -18,7 +18,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index cd1b84cb5..e53d7f96d 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -118,17 +118,17 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * return endpointCreationError } - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group inside the database", err} } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err} } - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err} } @@ -145,7 +145,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * } } - err = handler.EndpointRelationService.CreateEndpointRelation(relationObject) + err = handler.DataStore.EndpointRelation().CreateEndpointRelation(relationObject) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the relation object inside the database", err} } @@ -166,7 +166,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { endpointType := portainer.EdgeAgentEnvironment - endpointID := handler.EndpointService.GetNextIdentifier() + endpointID := handler.DataStore.Endpoint().GetNextIdentifier() portainerURL, err := url.Parse(payload.URL) if err != nil { @@ -228,7 +228,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) } } - endpointID := handler.EndpointService.GetNextIdentifier() + endpointID := handler.DataStore.Endpoint().GetNextIdentifier() endpoint := &portainer.Endpoint{ ID: portainer.EndpointID(endpointID), Name: payload.Name, @@ -271,7 +271,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) endpointType = portainer.AgentOnDockerEnvironment } - endpointID := handler.EndpointService.GetNextIdentifier() + endpointID := handler.DataStore.Endpoint().GetNextIdentifier() endpoint := &portainer.Endpoint{ ID: portainer.EndpointID(endpointID), Name: payload.Name, @@ -327,12 +327,12 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) } func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error { - err := handler.EndpointService.CreateEndpoint(endpoint) + err := handler.DataStore.Endpoint().CreateEndpoint(endpoint) if err != nil { return err } - group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID) + group, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID) if err != nil { return err } @@ -342,14 +342,14 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer. } for _, tagID := range endpoint.TagIDs { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return err } tag.Endpoints[endpoint.ID] = true - err = handler.TagService.UpdateTag(tagID, tag) + err = handler.DataStore.Tag().UpdateTag(tagID, tag) if err != nil { return err } diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 7ef6c0a0d..7a280d703 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -17,7 +17,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -32,7 +32,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * } } - err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID)) + err = handler.DataStore.Endpoint().DeleteEndpoint(portainer.EndpointID(endpointID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err} } @@ -46,26 +46,26 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * } } - err = handler.EndpointRelationService.DeleteEndpointRelation(endpoint.ID) + err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint relation from the database", err} } for _, tagID := range endpoint.TagIDs { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return &httperror.HandlerError{http.StatusNotFound, "Unable to find tag inside the database", err} } delete(tag.Endpoints, endpoint.ID) - err = handler.TagService.UpdateTag(tagID, tag) + err = handler.DataStore.Tag().UpdateTag(tagID, tag) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag relation inside the database", err} } } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err} } @@ -75,14 +75,14 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * endpointIdx := findEndpointIndex(edgeGroup.Endpoints, endpoint.ID) if endpointIdx != -1 { edgeGroup.Endpoints = removeElement(edgeGroup.Endpoints, endpointIdx) - err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup) + err = handler.DataStore.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge group", err} } } } - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err} } @@ -91,7 +91,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * edgeStack := &edgeStacks[idx] if _, ok := edgeStack.Status[endpoint.ID]; ok { delete(edgeStack.Status, endpoint.ID) - err = handler.EdgeStackService.UpdateEdgeStack(edgeStack.ID, edgeStack) + err = handler.DataStore.EdgeStack().UpdateEdgeStack(edgeStack.ID, edgeStack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge stack", err} } diff --git a/api/http/handler/endpoints/endpoint_extension_add.go b/api/http/handler/endpoints/endpoint_extension_add.go index f91a714c3..4472913dc 100644 --- a/api/http/handler/endpoints/endpoint_extension_add.go +++ b/api/http/handler/endpoints/endpoint_extension_add.go @@ -34,7 +34,7 @@ func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Requ return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -66,7 +66,7 @@ func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Requ endpoint.Extensions = append(endpoint.Extensions, *extension) } - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} } diff --git a/api/http/handler/endpoints/endpoint_extension_remove.go b/api/http/handler/endpoints/endpoint_extension_remove.go index b426071e0..6d81da363 100644 --- a/api/http/handler/endpoints/endpoint_extension_remove.go +++ b/api/http/handler/endpoints/endpoint_extension_remove.go @@ -18,7 +18,7 @@ func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.R return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -36,7 +36,7 @@ func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.R } } - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} } diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index 10cf34ed9..01abe2c3d 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -16,7 +16,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/endpoints/endpoint_job.go b/api/http/handler/endpoints/endpoint_job.go index 78d00bc9c..77f727ea3 100644 --- a/api/http/handler/endpoints/endpoint_job.go +++ b/api/http/handler/endpoints/endpoint_job.go @@ -63,7 +63,7 @@ func (handler *Handler) endpointJob(w http.ResponseWriter, r *http.Request) *htt nodeName, _ := request.RetrieveQueryParameter(r, "nodeName", true) - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 57ffacbce..fe7c489ae 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -38,12 +38,12 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht var endpointIDs []portainer.EndpointID request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true) - endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err} } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} } @@ -64,7 +64,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht } if search != "" { - tags, err := handler.TagService.Tags() + tags, err := handler.DataStore.Tag().Tags() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err} } diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index ba73674a9..21d6b8b0a 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -16,7 +16,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -25,7 +25,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint) - latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID) + latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } @@ -39,7 +39,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} } - err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) + err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} } diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go index 11a5b07ab..092dc5df1 100644 --- a/api/http/handler/endpoints/endpoint_snapshots.go +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -11,7 +11,7 @@ import ( // POST request on /api/endpoints/snapshot func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} } @@ -19,7 +19,7 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request for _, endpoint := range endpoints { snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint) - latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID) + latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) continue @@ -35,7 +35,7 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} } - err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) + err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} } diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index 13ae819a0..b8b1c4d63 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -30,7 +30,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -47,13 +47,13 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req endpoint.EdgeID = edgeIdentifier - err := handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err} } } - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } @@ -72,14 +72,14 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID) } - relation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID) + relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve relation object from the database", err} } edgeStacksStatus := []stackStatusResponse{} for stackID := range relation.EdgeStacks { - stack, err := handler.EdgeStackService.EdgeStack(stackID) + stack, err := handler.DataStore.EdgeStack().EdgeStack(stackID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack from the database", err} } diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index a1c63d53c..6ba76c992 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -42,7 +42,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -80,13 +80,13 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * removeTags := portainer.TagDifference(endpointTagSet, payloadTagSet) for tagID := range removeTags { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err} } delete(tag.Endpoints, endpoint.ID) - err = handler.TagService.UpdateTag(tag.ID, tag) + err = handler.DataStore.Tag().UpdateTag(tag.ID, tag) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err} } @@ -94,14 +94,14 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * endpoint.TagIDs = payload.TagIDs for _, tagID := range payload.TagIDs { - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err} } tag.Endpoints[endpoint.ID] = true - err = handler.TagService.UpdateTag(tag.ID, tag) + err = handler.DataStore.Tag().UpdateTag(tag.ID, tag) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err} } @@ -184,7 +184,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} } @@ -197,22 +197,22 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } if endpoint.Type == portainer.EdgeAgentEnvironment && (groupIDChanged || tagsChanged) { - relation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID) + relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation inside the database", err} } - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint group inside the database", err} } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err} } - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err} } @@ -226,7 +226,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * relation.EdgeStacks = edgeStackSet - err = handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, relation) + err = handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation changes inside the database", err} } diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 605c81322..15e8a55c0 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -20,27 +20,21 @@ func hideFields(endpoint *portainer.Endpoint) { // Handler is the HTTP handler used to handle endpoint operations. type Handler struct { *mux.Router - requestBouncer *security.RequestBouncer - AuthorizationService *portainer.AuthorizationService - EdgeGroupService portainer.EdgeGroupService - EdgeStackService portainer.EdgeStackService - EndpointService portainer.EndpointService - EndpointGroupService portainer.EndpointGroupService - EndpointRelationService portainer.EndpointRelationService - FileService portainer.FileService - JobService portainer.JobService - ProxyManager *proxy.Manager - ReverseTunnelService portainer.ReverseTunnelService - SettingsService portainer.SettingsService - Snapshotter portainer.Snapshotter - TagService portainer.TagService + requestBouncer *security.RequestBouncer + DataStore portainer.DataStore + AuthorizationService *portainer.AuthorizationService + FileService portainer.FileService + JobService portainer.JobService + ProxyManager *proxy.Manager + ReverseTunnelService portainer.ReverseTunnelService + Snapshotter portainer.Snapshotter } // NewHandler creates a handler to manage endpoint operations. func NewHandler(bouncer *security.RequestBouncer) *Handler { h := &Handler{ - Router: mux.NewRouter(), - requestBouncer: bouncer, + Router: mux.NewRouter(), + requestBouncer: bouncer, } h.Handle("/endpoints", diff --git a/api/http/handler/extensions/data.go b/api/http/handler/extensions/data.go index 8c950e608..37dcd62fc 100644 --- a/api/http/handler/extensions/data.go +++ b/api/http/handler/extensions/data.go @@ -17,7 +17,7 @@ func updateTeamAccessPolicyToReadOnlyRole(policies portainer.TeamAccessPolicies, } func (handler *Handler) upgradeRBACData() error { - endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { return err } @@ -31,13 +31,13 @@ func (handler *Handler) upgradeRBACData() error { updateTeamAccessPolicyToReadOnlyRole(endpointGroup.TeamAccessPolicies, key) } - err := handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + err := handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) if err != nil { return err } } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return err } @@ -51,7 +51,7 @@ func (handler *Handler) upgradeRBACData() error { updateTeamAccessPolicyToReadOnlyRole(endpoint.TeamAccessPolicies, key) } - err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { return err } @@ -73,7 +73,7 @@ func updateTeamAccessPolicyToNoRole(policies portainer.TeamAccessPolicies, key p } func (handler *Handler) downgradeRBACData() error { - endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups() if err != nil { return err } @@ -87,13 +87,13 @@ func (handler *Handler) downgradeRBACData() error { updateTeamAccessPolicyToNoRole(endpointGroup.TeamAccessPolicies, key) } - err := handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + err := handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) if err != nil { return err } } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return err } @@ -107,7 +107,7 @@ func (handler *Handler) downgradeRBACData() error { updateTeamAccessPolicyToNoRole(endpoint.TeamAccessPolicies, key) } - err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) if err != nil { return err } diff --git a/api/http/handler/extensions/extension_create.go b/api/http/handler/extensions/extension_create.go index 7e41c9595..dd602f3b0 100644 --- a/api/http/handler/extensions/extension_create.go +++ b/api/http/handler/extensions/extension_create.go @@ -36,7 +36,7 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) } extensionID := portainer.ExtensionID(extensionIdentifier) - extensions, err := handler.ExtensionService.Extensions() + extensions, err := handler.DataStore.Extension().Extensions() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err} } @@ -77,7 +77,7 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) } } - err = handler.ExtensionService.Persist(extension) + err = handler.DataStore.Extension().Persist(extension) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err} } diff --git a/api/http/handler/extensions/extension_delete.go b/api/http/handler/extensions/extension_delete.go index 3f9853016..789fa84fd 100644 --- a/api/http/handler/extensions/extension_delete.go +++ b/api/http/handler/extensions/extension_delete.go @@ -17,7 +17,7 @@ func (handler *Handler) extensionDelete(w http.ResponseWriter, r *http.Request) } extensionID := portainer.ExtensionID(extensionIdentifier) - extension, err := handler.ExtensionService.Extension(extensionID) + extension, err := handler.DataStore.Extension().Extension(extensionID) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err} } else if err != nil { @@ -36,7 +36,7 @@ func (handler *Handler) extensionDelete(w http.ResponseWriter, r *http.Request) } } - err = handler.ExtensionService.DeleteExtension(extensionID) + err = handler.DataStore.Extension().DeleteExtension(extensionID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the extension from the database", err} } diff --git a/api/http/handler/extensions/extension_inspect.go b/api/http/handler/extensions/extension_inspect.go index 6af8b1940..94c8292fe 100644 --- a/api/http/handler/extensions/extension_inspect.go +++ b/api/http/handler/extensions/extension_inspect.go @@ -25,7 +25,7 @@ func (handler *Handler) extensionInspect(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err} } - localExtension, err := handler.ExtensionService.Extension(extensionID) + localExtension, err := handler.DataStore.Extension().Extension(extensionID) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension information from the database", err} } diff --git a/api/http/handler/extensions/extension_list.go b/api/http/handler/extensions/extension_list.go index e96e103c8..661f7b7df 100644 --- a/api/http/handler/extensions/extension_list.go +++ b/api/http/handler/extensions/extension_list.go @@ -12,7 +12,7 @@ import ( func (handler *Handler) extensionList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { fetchManifestInformation, _ := request.RetrieveBooleanQueryParameter(r, "store", true) - extensions, err := handler.ExtensionService.Extensions() + extensions, err := handler.DataStore.Extension().Extensions() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions from the database", err} } diff --git a/api/http/handler/extensions/extension_update.go b/api/http/handler/extensions/extension_update.go index b51bf93ba..38487a2f7 100644 --- a/api/http/handler/extensions/extension_update.go +++ b/api/http/handler/extensions/extension_update.go @@ -35,7 +35,7 @@ func (handler *Handler) extensionUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - extension, err := handler.ExtensionService.Extension(extensionID) + extension, err := handler.DataStore.Extension().Extension(extensionID) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err} } else if err != nil { @@ -47,7 +47,7 @@ func (handler *Handler) extensionUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update extension", err} } - err = handler.ExtensionService.Persist(extension) + err = handler.DataStore.Extension().Persist(extension) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err} } diff --git a/api/http/handler/extensions/extension_upload.go b/api/http/handler/extensions/extension_upload.go index 46d403fc6..9469726be 100644 --- a/api/http/handler/extensions/extension_upload.go +++ b/api/http/handler/extensions/extension_upload.go @@ -66,7 +66,7 @@ func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request) } } - err = handler.ExtensionService.Persist(extension) + err = handler.DataStore.Extension().Persist(extension) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err} } diff --git a/api/http/handler/extensions/handler.go b/api/http/handler/extensions/handler.go index 15df2ce17..cba6b354c 100644 --- a/api/http/handler/extensions/handler.go +++ b/api/http/handler/extensions/handler.go @@ -14,11 +14,8 @@ import ( // Handler is the HTTP handler used to handle extension operations. type Handler struct { *mux.Router - ExtensionService portainer.ExtensionService + DataStore portainer.DataStore ExtensionManager portainer.ExtensionManager - EndpointGroupService portainer.EndpointGroupService - EndpointService portainer.EndpointService - RegistryService portainer.RegistryService AuthorizationService *portainer.AuthorizationService } diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go index 3b2646dcb..74a933586 100644 --- a/api/http/handler/registries/handler.go +++ b/api/http/handler/registries/handler.go @@ -18,11 +18,10 @@ func hideFields(registry *portainer.Registry) { // Handler is the HTTP handler used to handle registry operations. type Handler struct { *mux.Router - requestBouncer *security.RequestBouncer - RegistryService portainer.RegistryService - ExtensionService portainer.ExtensionService - FileService portainer.FileService - ProxyManager *proxy.Manager + requestBouncer *security.RequestBouncer + DataStore portainer.DataStore + FileService portainer.FileService + ProxyManager *proxy.Manager } // NewHandler creates a handler to manage registry operations. diff --git a/api/http/handler/registries/proxy.go b/api/http/handler/registries/proxy.go index 3f94bed4a..0bc996d95 100644 --- a/api/http/handler/registries/proxy.go +++ b/api/http/handler/registries/proxy.go @@ -17,7 +17,7 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err} } - registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { @@ -29,7 +29,7 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied} } - extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension) + extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err} } else if err != nil { diff --git a/api/http/handler/registries/proxy_management_gitlab.go b/api/http/handler/registries/proxy_management_gitlab.go index 28f1ead12..c9dcbfac0 100644 --- a/api/http/handler/registries/proxy_management_gitlab.go +++ b/api/http/handler/registries/proxy_management_gitlab.go @@ -17,7 +17,7 @@ func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWrit return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err} } - registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { @@ -29,7 +29,7 @@ func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWrit return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied} } - extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension) + extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err} } else if err != nil { diff --git a/api/http/handler/registries/registry_configure.go b/api/http/handler/registries/registry_configure.go index c967b6996..21d2f92f8 100644 --- a/api/http/handler/registries/registry_configure.go +++ b/api/http/handler/registries/registry_configure.go @@ -78,7 +78,7 @@ func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { @@ -128,7 +128,7 @@ func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request } } - err = handler.RegistryService.UpdateRegistry(registry.ID, registry) + err = handler.DataStore.Registry().UpdateRegistry(registry.ID, registry) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err} } diff --git a/api/http/handler/registries/registry_create.go b/api/http/handler/registries/registry_create.go index 09f6d0a2e..aac899138 100644 --- a/api/http/handler/registries/registry_create.go +++ b/api/http/handler/registries/registry_create.go @@ -55,7 +55,7 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) * Gitlab: payload.Gitlab, } - err = handler.RegistryService.CreateRegistry(registry) + err = handler.DataStore.Registry().CreateRegistry(registry) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the registry inside the database", err} } diff --git a/api/http/handler/registries/registry_delete.go b/api/http/handler/registries/registry_delete.go index ebb833ab4..62e0c663f 100644 --- a/api/http/handler/registries/registry_delete.go +++ b/api/http/handler/registries/registry_delete.go @@ -16,14 +16,14 @@ func (handler *Handler) registryDelete(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err} } - _, err = handler.RegistryService.Registry(portainer.RegistryID(registryID)) + _, err = handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} } - err = handler.RegistryService.DeleteRegistry(portainer.RegistryID(registryID)) + err = handler.DataStore.Registry().DeleteRegistry(portainer.RegistryID(registryID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the registry from the database", err} } diff --git a/api/http/handler/registries/registry_inspect.go b/api/http/handler/registries/registry_inspect.go index d40ef693a..586f198db 100644 --- a/api/http/handler/registries/registry_inspect.go +++ b/api/http/handler/registries/registry_inspect.go @@ -16,7 +16,7 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err} } - registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/registries/registry_list.go b/api/http/handler/registries/registry_list.go index b78763375..1acd380f4 100644 --- a/api/http/handler/registries/registry_list.go +++ b/api/http/handler/registries/registry_list.go @@ -10,7 +10,7 @@ import ( // GET request on /api/registries func (handler *Handler) registryList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - registries, err := handler.RegistryService.Registries() + registries, err := handler.DataStore.Registry().Registries() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err} } diff --git a/api/http/handler/registries/registry_update.go b/api/http/handler/registries/registry_update.go index 15dd2d9dc..cd4255140 100644 --- a/api/http/handler/registries/registry_update.go +++ b/api/http/handler/registries/registry_update.go @@ -36,7 +36,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID)) + registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { @@ -48,7 +48,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) * } if payload.URL != nil { - registries, err := handler.RegistryService.Registries() + registries, err := handler.DataStore.Registry().Registries() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err} } @@ -88,7 +88,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) * registry.TeamAccessPolicies = payload.TeamAccessPolicies } - err = handler.RegistryService.UpdateRegistry(registry.ID, registry) + err = handler.DataStore.Registry().UpdateRegistry(registry.ID, registry) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err} } diff --git a/api/http/handler/resourcecontrols/handler.go b/api/http/handler/resourcecontrols/handler.go index e6851c649..d0dc65b19 100644 --- a/api/http/handler/resourcecontrols/handler.go +++ b/api/http/handler/resourcecontrols/handler.go @@ -12,7 +12,7 @@ import ( // Handler is the HTTP handler used to handle resource control operations. type Handler struct { *mux.Router - ResourceControlService portainer.ResourceControlService + DataStore portainer.DataStore } // NewHandler creates a handler to manage resource control operations. diff --git a/api/http/handler/resourcecontrols/resourcecontrol_create.go b/api/http/handler/resourcecontrols/resourcecontrol_create.go index 0a6696bd9..5f56daa19 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_create.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_create.go @@ -68,7 +68,7 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", portainer.ErrInvalidResourceControlType} } - rc, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType) + rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err} } @@ -104,7 +104,7 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req TeamAccesses: teamAccesses, } - err = handler.ResourceControlService.CreateResourceControl(&resourceControl) + err = handler.DataStore.ResourceControl().CreateResourceControl(&resourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the resource control inside the database", err} } diff --git a/api/http/handler/resourcecontrols/resourcecontrol_delete.go b/api/http/handler/resourcecontrols/resourcecontrol_delete.go index 76794e423..076e86519 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_delete.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_delete.go @@ -16,14 +16,14 @@ func (handler *Handler) resourceControlDelete(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err} } - _, err = handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID)) + _, err = handler.DataStore.ResourceControl().ResourceControl(portainer.ResourceControlID(resourceControlID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err} } - err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID)) + err = handler.DataStore.ResourceControl().DeleteResourceControl(portainer.ResourceControlID(resourceControlID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err} } diff --git a/api/http/handler/resourcecontrols/resourcecontrol_update.go b/api/http/handler/resourcecontrols/resourcecontrol_update.go index fc170f1bd..9e5f52752 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_update.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_update.go @@ -42,7 +42,7 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID)) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControl(portainer.ResourceControlID(resourceControlID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err} } else if err != nil { @@ -85,7 +85,7 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied} } - err = handler.ResourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl) + err = handler.DataStore.ResourceControl().UpdateResourceControl(resourceControl.ID, resourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control changes inside the database", err} } diff --git a/api/http/handler/roles/handler.go b/api/http/handler/roles/handler.go index 89ec52452..a4a709562 100644 --- a/api/http/handler/roles/handler.go +++ b/api/http/handler/roles/handler.go @@ -12,7 +12,7 @@ import ( // Handler is the HTTP handler used to handle role operations. type Handler struct { *mux.Router - RoleService portainer.RoleService + DataStore portainer.DataStore } // NewHandler creates a handler to manage role operations. diff --git a/api/http/handler/roles/role_list.go b/api/http/handler/roles/role_list.go index e39e38595..11817c2f3 100644 --- a/api/http/handler/roles/role_list.go +++ b/api/http/handler/roles/role_list.go @@ -9,7 +9,7 @@ import ( // GET request on /api/Role func (handler *Handler) roleList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - roles, err := handler.RoleService.Roles() + roles, err := handler.DataStore.Role().Roles() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve authorization sets from the database", err} } diff --git a/api/http/handler/schedules/handler.go b/api/http/handler/schedules/handler.go index cc7d3dbf2..2d4382bf3 100644 --- a/api/http/handler/schedules/handler.go +++ b/api/http/handler/schedules/handler.go @@ -12,9 +12,7 @@ import ( // Handler is the HTTP handler used to handle schedule operations. type Handler struct { *mux.Router - ScheduleService portainer.ScheduleService - EndpointService portainer.EndpointService - SettingsService portainer.SettingsService + DataStore portainer.DataStore FileService portainer.FileService JobService portainer.JobService JobScheduler portainer.JobScheduler diff --git a/api/http/handler/schedules/schedule_create.go b/api/http/handler/schedules/schedule_create.go index 196913a33..9e54bbcab 100644 --- a/api/http/handler/schedules/schedule_create.go +++ b/api/http/handler/schedules/schedule_create.go @@ -117,7 +117,7 @@ func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) e // POST /api/schedules?method=file|string func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} } @@ -175,7 +175,7 @@ func (handler *Handler) createScheduleFromFile(w http.ResponseWriter, r *http.Re } func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCreateFromFilePayload) *portainer.Schedule { - scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier()) + scheduleIdentifier := portainer.ScheduleID(handler.DataStore.Schedule().GetNextIdentifier()) job := &portainer.ScriptExecutionJob{ Endpoints: payload.Endpoints, @@ -198,7 +198,7 @@ func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCre } func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *scheduleCreateFromFileContentPayload) *portainer.Schedule { - scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier()) + scheduleIdentifier := portainer.ScheduleID(handler.DataStore.Schedule().GetNextIdentifier()) job := &portainer.ScriptExecutionJob{ Endpoints: payload.Endpoints, @@ -231,7 +231,7 @@ func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file for _, ID := range schedule.ScriptExecutionJob.Endpoints { - endpoint, err := handler.EndpointService.Endpoint(ID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(ID) if err != nil { return err } @@ -268,7 +268,7 @@ func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file schedule.ScriptExecutionJob.ScriptPath = scriptPath - jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService) + jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.DataStore, handler.FileService) jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext) err = handler.JobScheduler.ScheduleJob(jobRunner) @@ -276,5 +276,5 @@ func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file return err } - return handler.ScheduleService.CreateSchedule(schedule) + return handler.DataStore.Schedule().CreateSchedule(schedule) } diff --git a/api/http/handler/schedules/schedule_delete.go b/api/http/handler/schedules/schedule_delete.go index c30b01696..8970e35bf 100644 --- a/api/http/handler/schedules/schedule_delete.go +++ b/api/http/handler/schedules/schedule_delete.go @@ -12,7 +12,7 @@ import ( ) func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} } @@ -25,7 +25,7 @@ func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} } - schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID)) + schedule, err := handler.DataStore.Schedule().Schedule(portainer.ScheduleID(scheduleID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err} } else if err != nil { @@ -46,7 +46,7 @@ func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) * handler.JobScheduler.UnscheduleJob(schedule.ID) - err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID)) + err = handler.DataStore.Schedule().DeleteSchedule(portainer.ScheduleID(scheduleID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the schedule from the database", err} } diff --git a/api/http/handler/schedules/schedule_file.go b/api/http/handler/schedules/schedule_file.go index c10b698dd..263ff6eb2 100644 --- a/api/http/handler/schedules/schedule_file.go +++ b/api/http/handler/schedules/schedule_file.go @@ -16,7 +16,7 @@ type scheduleFileResponse struct { // GET request on /api/schedules/:id/file func (handler *Handler) scheduleFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} } @@ -29,7 +29,7 @@ func (handler *Handler) scheduleFile(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} } - schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID)) + schedule, err := handler.DataStore.Schedule().Schedule(portainer.ScheduleID(scheduleID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/schedules/schedule_inspect.go b/api/http/handler/schedules/schedule_inspect.go index 594c29b1a..9067c81f7 100644 --- a/api/http/handler/schedules/schedule_inspect.go +++ b/api/http/handler/schedules/schedule_inspect.go @@ -11,7 +11,7 @@ import ( ) func (handler *Handler) scheduleInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} } @@ -24,7 +24,7 @@ func (handler *Handler) scheduleInspect(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} } - schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID)) + schedule, err := handler.DataStore.Schedule().Schedule(portainer.ScheduleID(scheduleID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/schedules/schedule_list.go b/api/http/handler/schedules/schedule_list.go index 55662dc0e..399e7e1bb 100644 --- a/api/http/handler/schedules/schedule_list.go +++ b/api/http/handler/schedules/schedule_list.go @@ -10,7 +10,7 @@ import ( // GET request on /api/schedules func (handler *Handler) scheduleList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} } @@ -18,7 +18,7 @@ func (handler *Handler) scheduleList(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled} } - schedules, err := handler.ScheduleService.Schedules() + schedules, err := handler.DataStore.Schedule().Schedules() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve schedules from the database", err} } diff --git a/api/http/handler/schedules/schedule_tasks.go b/api/http/handler/schedules/schedule_tasks.go index a4993e6cd..48dfc35aa 100644 --- a/api/http/handler/schedules/schedule_tasks.go +++ b/api/http/handler/schedules/schedule_tasks.go @@ -24,7 +24,7 @@ type taskContainer struct { // GET request on /api/schedules/:id/tasks func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} } @@ -37,7 +37,7 @@ func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err} } - schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID)) + schedule, err := handler.DataStore.Schedule().Schedule(portainer.ScheduleID(scheduleID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err} } else if err != nil { @@ -51,7 +51,7 @@ func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *h tasks := make([]taskContainer, 0) for _, endpointID := range schedule.ScriptExecutionJob.Endpoints { - endpoint, err := handler.EndpointService.Endpoint(endpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err == portainer.ErrObjectNotFound { continue } else if err != nil { diff --git a/api/http/handler/schedules/schedule_update.go b/api/http/handler/schedules/schedule_update.go index f68e77126..2bf2faf39 100644 --- a/api/http/handler/schedules/schedule_update.go +++ b/api/http/handler/schedules/schedule_update.go @@ -33,7 +33,7 @@ func (payload *scheduleUpdatePayload) Validate(r *http.Request) error { } func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} } @@ -52,7 +52,7 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID)) + schedule, err := handler.DataStore.Schedule().Schedule(portainer.ScheduleID(scheduleID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err} } else if err != nil { @@ -78,7 +78,7 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) * } if updateJobSchedule { - jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService) + jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.DataStore, handler.FileService) jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext) err := handler.JobScheduler.UpdateJobSchedule(jobRunner) if err != nil { @@ -86,7 +86,7 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) * } } - err = handler.ScheduleService.UpdateSchedule(portainer.ScheduleID(scheduleID), schedule) + err = handler.DataStore.Schedule().UpdateSchedule(portainer.ScheduleID(scheduleID), schedule) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist schedule changes inside the database", err} } @@ -104,7 +104,7 @@ func (handler *Handler) updateEdgeSchedule(schedule *portainer.Schedule, payload edgeEndpointIDs := make([]portainer.EndpointID, 0) for _, ID := range payload.Endpoints { - endpoint, err := handler.EndpointService.Endpoint(ID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(ID) if err != nil { return err } diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index 1f688f343..349d05228 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -17,13 +17,10 @@ func hideFields(settings *portainer.Settings) { // Handler is the HTTP handler used to handle settings operations. type Handler struct { *mux.Router - SettingsService portainer.SettingsService + DataStore portainer.DataStore LDAPService portainer.LDAPService FileService portainer.FileService JobScheduler portainer.JobScheduler - ScheduleService portainer.ScheduleService - RoleService portainer.RoleService - ExtensionService portainer.ExtensionService AuthorizationService *portainer.AuthorizationService } diff --git a/api/http/handler/settings/settings_inspect.go b/api/http/handler/settings/settings_inspect.go index a28b1ef09..0e732ee43 100644 --- a/api/http/handler/settings/settings_inspect.go +++ b/api/http/handler/settings/settings_inspect.go @@ -9,7 +9,7 @@ import ( // GET request on /api/settings func (handler *Handler) settingsInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err} } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index a5cd0ac57..5af534d45 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -22,7 +22,7 @@ type publicSettingsResponse struct { // GET request on /api/settings/public func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err} } diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index cef3bdf0f..86d26bdaa 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -48,7 +48,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err} } @@ -130,7 +130,7 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * return tlsError } - err = handler.SettingsService.UpdateSettings(settings) + err = handler.DataStore.Settings().UpdateSettings(settings) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err} } @@ -151,7 +151,7 @@ func (handler *Handler) updateVolumeBrowserSetting(settings *portainer.Settings) return err } - extension, err := handler.ExtensionService.Extension(portainer.RBACExtension) + extension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) if err != nil && err != portainer.ErrObjectNotFound { return err } @@ -169,7 +169,7 @@ func (handler *Handler) updateVolumeBrowserSetting(settings *portainer.Settings) func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error { settings.SnapshotInterval = snapshotInterval - schedules, err := handler.ScheduleService.SchedulesByJobType(portainer.SnapshotJobType) + schedules, err := handler.DataStore.Schedule().SchedulesByJobType(portainer.SnapshotJobType) if err != nil { return err } @@ -183,7 +183,7 @@ func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, sna return err } - err = handler.ScheduleService.UpdateSchedule(snapshotSchedule.ID, &snapshotSchedule) + err = handler.DataStore.Schedule().UpdateSchedule(snapshotSchedule.ID, &snapshotSchedule) if err != nil { return err } diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index ebdcea252..051999980 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -47,7 +47,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.StackService.Stacks() + stacks, err := handler.DataStore.Stack().Stacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} } @@ -58,7 +58,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, } } - stackID := handler.StackService.GetNextIdentifier() + stackID := handler.DataStore.Stack().GetNextIdentifier() stack := &portainer.Stack{ ID: portainer.StackID(stackID), Name: payload.Name, @@ -88,7 +88,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} } - err = handler.StackService.CreateStack(stack) + err = handler.DataStore.Stack().CreateStack(stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} } @@ -132,7 +132,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.StackService.Stacks() + stacks, err := handler.DataStore.Stack().Stacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} } @@ -143,7 +143,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite } } - stackID := handler.StackService.GetNextIdentifier() + stackID := handler.DataStore.Stack().GetNextIdentifier() stack := &portainer.Stack{ ID: portainer.StackID(stackID), Name: payload.Name, @@ -183,7 +183,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} } - err = handler.StackService.CreateStack(stack) + err = handler.DataStore.Stack().CreateStack(stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} } @@ -227,7 +227,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.StackService.Stacks() + stacks, err := handler.DataStore.Stack().Stacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} } @@ -238,7 +238,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, } } - stackID := handler.StackService.GetNextIdentifier() + stackID := handler.DataStore.Stack().GetNextIdentifier() stack := &portainer.Stack{ ID: portainer.StackID(stackID), Name: payload.Name, @@ -268,7 +268,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} } - err = handler.StackService.CreateStack(stack) + err = handler.DataStore.Stack().CreateStack(stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} } @@ -291,12 +291,12 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} } - dockerhub, err := handler.DockerHubService.DockerHub() + dockerhub, err := handler.DataStore.DockerHub().DockerHub() if err != nil { return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err} } - registries, err := handler.RegistryService.Registries() + registries, err := handler.DataStore.Registry().Registries() if err != nil { return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err} } @@ -319,7 +319,7 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai // clean it. Hence the use of the mutex. // We should contribute to libcompose to support authentication without using the config.json file. func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return err } diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 143292ea9..09e743e44 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -42,7 +42,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.StackService.Stacks() + stacks, err := handler.DataStore.Stack().Stacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} } @@ -53,7 +53,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r } } - stackID := handler.StackService.GetNextIdentifier() + stackID := handler.DataStore.Stack().GetNextIdentifier() stack := &portainer.Stack{ ID: portainer.StackID(stackID), Name: payload.Name, @@ -84,7 +84,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} } - err = handler.StackService.CreateStack(stack) + err = handler.DataStore.Stack().CreateStack(stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} } @@ -131,7 +131,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.StackService.Stacks() + stacks, err := handler.DataStore.Stack().Stacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} } @@ -142,7 +142,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, } } - stackID := handler.StackService.GetNextIdentifier() + stackID := handler.DataStore.Stack().GetNextIdentifier() stack := &portainer.Stack{ ID: portainer.StackID(stackID), Name: payload.Name, @@ -183,7 +183,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} } - err = handler.StackService.CreateStack(stack) + err = handler.DataStore.Stack().CreateStack(stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} } @@ -234,7 +234,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stacks, err := handler.StackService.Stacks() + stacks, err := handler.DataStore.Stack().Stacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} } @@ -245,7 +245,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r } } - stackID := handler.StackService.GetNextIdentifier() + stackID := handler.DataStore.Stack().GetNextIdentifier() stack := &portainer.Stack{ ID: portainer.StackID(stackID), Name: payload.Name, @@ -276,7 +276,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} } - err = handler.StackService.CreateStack(stack) + err = handler.DataStore.Stack().CreateStack(stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err} } @@ -300,12 +300,12 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} } - dockerhub, err := handler.DockerHubService.DockerHub() + dockerhub, err := handler.DataStore.DockerHub().DockerHub() if err != nil { return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve DockerHub details from the database", err} } - registries, err := handler.RegistryService.Registries() + registries, err := handler.DataStore.Registry().Registries() if err != nil { return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err} } @@ -324,7 +324,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine } func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return err } diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index d0a6b4ea5..6722feaa8 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -16,18 +16,11 @@ type Handler struct { stackDeletionMutex *sync.Mutex requestBouncer *security.RequestBouncer *mux.Router - FileService portainer.FileService - GitService portainer.GitService - StackService portainer.StackService - EndpointService portainer.EndpointService - ResourceControlService portainer.ResourceControlService - RegistryService portainer.RegistryService - DockerHubService portainer.DockerHubService - SwarmStackManager portainer.SwarmStackManager - ComposeStackManager portainer.ComposeStackManager - SettingsService portainer.SettingsService - UserService portainer.UserService - ExtensionService portainer.ExtensionService + DataStore portainer.DataStore + FileService portainer.FileService + GitService portainer.GitService + SwarmStackManager portainer.SwarmStackManager + ComposeStackManager portainer.ComposeStackManager } // NewHandler creates a handler to manage stack operations. @@ -69,14 +62,14 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR return true, nil } - _, err := handler.ExtensionService.Extension(portainer.RBACExtension) + _, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) if err == portainer.ErrObjectNotFound { return false, nil } else if err != nil && err != portainer.ErrObjectNotFound { return false, err } - user, err := handler.UserService.User(securityContext.UserID) + user, err := handler.DataStore.User().User(securityContext.UserID) if err != nil { return false, err } diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index dc374c4b1..7f3244520 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -43,7 +43,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -135,7 +135,7 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError { resourceControl := portainer.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID) - err := handler.ResourceControlService.CreateResourceControl(resourceControl) + err := handler.DataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err} } diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index da7e82751..cac9b21fe 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -36,7 +36,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} } - stack, err := handler.StackService.Stack(portainer.StackID(id)) + stack, err := handler.DataStore.Stack().Stack(portainer.StackID(id)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { @@ -56,7 +56,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt endpointIdentifier = portainer.EndpointID(endpointID) } - endpoint, err := handler.EndpointService.Endpoint(endpointIdentifier) + endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointIdentifier) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { @@ -68,7 +68,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } @@ -86,13 +86,13 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} } - err = handler.StackService.DeleteStack(portainer.StackID(id)) + err = handler.DataStore.Stack().DeleteStack(portainer.StackID(id)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err} } if resourceControl != nil { - err = handler.ResourceControlService.DeleteResourceControl(resourceControl.ID) + err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err} } @@ -112,12 +112,12 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} } - user, err := handler.UserService.User(securityContext.UserID) + user, err := handler.DataStore.User().User(securityContext.UserID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err} } - rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension) + rbacExtension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify if RBAC extension is loaded", err} } @@ -138,7 +138,7 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit } } - stack, err := handler.StackService.StackByName(stackName) + stack, err := handler.DataStore.Stack().StackByName(stackName) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err} } @@ -146,7 +146,7 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", portainer.ErrStackNotExternal} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index 7c966a08e..7c601a263 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -22,14 +22,14 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} } - stack, err := handler.StackService.Stack(portainer.StackID(stackID)) + stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } - endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -41,7 +41,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index 42d0ad9c5..eb41dc793 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -17,14 +17,14 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} } - stack, err := handler.StackService.Stack(portainer.StackID(stackID)) + stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } - endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -41,7 +41,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err} } - resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } diff --git a/api/http/handler/stacks/stack_list.go b/api/http/handler/stacks/stack_list.go index 2c87cb40b..eab7d7ca1 100644 --- a/api/http/handler/stacks/stack_list.go +++ b/api/http/handler/stacks/stack_list.go @@ -23,13 +23,13 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err} } - stacks, err := handler.StackService.Stacks() + stacks, err := handler.DataStore.Stack().Stacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve stacks from the database", err} } stacks = filterStacks(stacks, &filters) - resourceControls, err := handler.ResourceControlService.ResourceControls() + resourceControls, err := handler.DataStore.ResourceControl().ResourceControls() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err} } @@ -43,14 +43,14 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe if !securityContext.IsAdmin { rbacExtensionEnabled := true - _, err := handler.ExtensionService.Extension(portainer.RBACExtension) + _, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) if err == portainer.ErrObjectNotFound { rbacExtensionEnabled = false } else if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check if RBAC extension is enabled", err} } - user, err := handler.UserService.User(securityContext.UserID) + user, err := handler.DataStore.User().User(securityContext.UserID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err} } diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index 690622a7a..1c0f98497 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -36,14 +36,14 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - stack, err := handler.StackService.Stack(portainer.StackID(stackID)) + stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } - endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -55,7 +55,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } @@ -84,7 +84,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht stack.EndpointID = portainer.EndpointID(endpointID) } - targetEndpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(payload.EndpointID)) + targetEndpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(payload.EndpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { @@ -112,7 +112,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} } - err = handler.StackService.UpdateStack(stack.ID, stack) + err = handler.DataStore.Stack().UpdateStack(stack.ID, stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err} } diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 1e4e08fdf..3781f9a78 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -45,7 +45,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} } - stack, err := handler.StackService.Stack(portainer.StackID(stackID)) + stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { @@ -63,7 +63,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt stack.EndpointID = portainer.EndpointID(endpointID) } - endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { @@ -75,7 +75,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} } @@ -98,7 +98,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt return updateError } - err = handler.StackService.UpdateStack(stack.ID, stack) + err = handler.DataStore.Stack().UpdateStack(stack.ID, stack) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err} } diff --git a/api/http/handler/tags/handler.go b/api/http/handler/tags/handler.go index 21ca61acb..f7b6fd75d 100644 --- a/api/http/handler/tags/handler.go +++ b/api/http/handler/tags/handler.go @@ -12,12 +12,7 @@ import ( // Handler is the HTTP handler used to handle tag operations. type Handler struct { *mux.Router - TagService portainer.TagService - EdgeGroupService portainer.EdgeGroupService - EdgeStackService portainer.EdgeStackService - EndpointService portainer.EndpointService - EndpointGroupService portainer.EndpointGroupService - EndpointRelationService portainer.EndpointRelationService + DataStore portainer.DataStore } // NewHandler creates a handler to manage tag operations. diff --git a/api/http/handler/tags/tag_create.go b/api/http/handler/tags/tag_create.go index 846d256ee..d1857bcf5 100644 --- a/api/http/handler/tags/tag_create.go +++ b/api/http/handler/tags/tag_create.go @@ -29,7 +29,7 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - tags, err := handler.TagService.Tags() + tags, err := handler.DataStore.Tag().Tags() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err} } @@ -46,7 +46,7 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe Endpoints: map[portainer.EndpointID]bool{}, } - err = handler.TagService.CreateTag(tag) + err = handler.DataStore.Tag().CreateTag(tag) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the tag inside the database", err} } diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index c2cafe43e..52dac6a9c 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -17,7 +17,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe } tagID := portainer.TagID(id) - tag, err := handler.TagService.Tag(tagID) + tag, err := handler.DataStore.Tag().Tag(tagID) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a tag with the specified identifier inside the database", err} } else if err != nil { @@ -25,7 +25,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe } for endpointID := range tag.Endpoints { - endpoint, err := handler.EndpointService.Endpoint(endpointID) + endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} } @@ -33,7 +33,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe tagIdx := findTagIndex(endpoint.TagIDs, tagID) if tagIdx != -1 { endpoint.TagIDs = removeElement(endpoint.TagIDs, tagIdx) - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} } @@ -41,7 +41,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe } for endpointGroupID := range tag.EndpointGroups { - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpointGroupID) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpointGroupID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint group from the database", err} } @@ -49,24 +49,24 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe tagIdx := findTagIndex(endpointGroup.TagIDs, tagID) if tagIdx != -1 { endpointGroup.TagIDs = removeElement(endpointGroup.TagIDs, tagIdx) - err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup) + err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err} } } } - endpoints, err := handler.EndpointService.Endpoints() + endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} } - edgeGroups, err := handler.EdgeGroupService.EdgeGroups() + edgeGroups, err := handler.DataStore.EdgeGroup().EdgeGroups() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err} } - edgeStacks, err := handler.EdgeStackService.EdgeStacks() + edgeStacks, err := handler.DataStore.EdgeStack().EdgeStacks() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err} } @@ -85,14 +85,14 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe tagIdx := findTagIndex(edgeGroup.TagIDs, tagID) if tagIdx != -1 { edgeGroup.TagIDs = removeElement(edgeGroup.TagIDs, tagIdx) - err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup) + err = handler.DataStore.EdgeGroup().UpdateEdgeGroup(edgeGroup.ID, edgeGroup) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err} } } } - err = handler.TagService.DeleteTag(tagID) + err = handler.DataStore.Tag().DeleteTag(tagID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the tag from the database", err} } @@ -101,12 +101,12 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe } func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error { - endpointRelation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID) + endpointRelation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID) if err != nil { return err } - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID) + endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID) if err != nil { return err } @@ -118,7 +118,7 @@ func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edg } endpointRelation.EdgeStacks = stacksSet - return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, endpointRelation) + return handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, endpointRelation) } func findTagIndex(tags []portainer.TagID, searchTagID portainer.TagID) int { diff --git a/api/http/handler/tags/tag_list.go b/api/http/handler/tags/tag_list.go index a19aa48e7..e4d7f2afa 100644 --- a/api/http/handler/tags/tag_list.go +++ b/api/http/handler/tags/tag_list.go @@ -9,7 +9,7 @@ import ( // GET request on /api/tags func (handler *Handler) tagList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - tags, err := handler.TagService.Tags() + tags, err := handler.DataStore.Tag().Tags() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err} } diff --git a/api/http/handler/teammemberships/handler.go b/api/http/handler/teammemberships/handler.go index 0428241ec..e9faca62d 100644 --- a/api/http/handler/teammemberships/handler.go +++ b/api/http/handler/teammemberships/handler.go @@ -13,8 +13,8 @@ import ( // Handler is the HTTP handler used to handle team membership operations. type Handler struct { *mux.Router - TeamMembershipService portainer.TeamMembershipService - AuthorizationService *portainer.AuthorizationService + DataStore portainer.DataStore + AuthorizationService *portainer.AuthorizationService } // NewHandler creates a handler to manage team membership operations. diff --git a/api/http/handler/teammemberships/teammembership_create.go b/api/http/handler/teammemberships/teammembership_create.go index 245b1fe67..21f5cd74d 100644 --- a/api/http/handler/teammemberships/teammembership_create.go +++ b/api/http/handler/teammemberships/teammembership_create.go @@ -46,7 +46,7 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ return &httperror.HandlerError{http.StatusForbidden, "Permission denied to manage team memberships", portainer.ErrResourceAccessDenied} } - memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(portainer.UserID(payload.UserID)) + memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(portainer.UserID(payload.UserID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve team memberships from the database", err} } @@ -65,7 +65,7 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ Role: portainer.MembershipRole(payload.Role), } - err = handler.TeamMembershipService.CreateTeamMembership(membership) + err = handler.DataStore.TeamMembership().CreateTeamMembership(membership) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team memberships inside the database", err} } diff --git a/api/http/handler/teammemberships/teammembership_delete.go b/api/http/handler/teammemberships/teammembership_delete.go index 179bbe800..775ba4d1a 100644 --- a/api/http/handler/teammemberships/teammembership_delete.go +++ b/api/http/handler/teammemberships/teammembership_delete.go @@ -17,7 +17,7 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ return &httperror.HandlerError{http.StatusBadRequest, "Invalid membership identifier route variable", err} } - membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID)) + membership, err := handler.DataStore.TeamMembership().TeamMembership(portainer.TeamMembershipID(membershipID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", err} } else if err != nil { @@ -33,7 +33,7 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the membership", portainer.ErrResourceAccessDenied} } - err = handler.TeamMembershipService.DeleteTeamMembership(portainer.TeamMembershipID(membershipID)) + err = handler.DataStore.TeamMembership().DeleteTeamMembership(portainer.TeamMembershipID(membershipID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the team membership from the database", err} } diff --git a/api/http/handler/teammemberships/teammembership_list.go b/api/http/handler/teammemberships/teammembership_list.go index bb0196458..20e42b599 100644 --- a/api/http/handler/teammemberships/teammembership_list.go +++ b/api/http/handler/teammemberships/teammembership_list.go @@ -20,7 +20,7 @@ func (handler *Handler) teamMembershipList(w http.ResponseWriter, r *http.Reques return &httperror.HandlerError{http.StatusForbidden, "Permission denied to list team memberships", portainer.ErrResourceAccessDenied} } - memberships, err := handler.TeamMembershipService.TeamMemberships() + memberships, err := handler.DataStore.TeamMembership().TeamMemberships() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve team memberships from the database", err} } diff --git a/api/http/handler/teammemberships/teammembership_update.go b/api/http/handler/teammemberships/teammembership_update.go index 84ab44323..d98929a75 100644 --- a/api/http/handler/teammemberships/teammembership_update.go +++ b/api/http/handler/teammemberships/teammembership_update.go @@ -51,7 +51,7 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the membership", portainer.ErrResourceAccessDenied} } - membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID)) + membership, err := handler.DataStore.TeamMembership().TeamMembership(portainer.TeamMembershipID(membershipID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", err} } else if err != nil { @@ -66,7 +66,7 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ membership.TeamID = portainer.TeamID(payload.TeamID) membership.Role = portainer.MembershipRole(payload.Role) - err = handler.TeamMembershipService.UpdateTeamMembership(membership.ID, membership) + err = handler.DataStore.TeamMembership().UpdateTeamMembership(membership.ID, membership) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist membership changes inside the database", err} } diff --git a/api/http/handler/teams/handler.go b/api/http/handler/teams/handler.go index e5eea77fc..ce87e0513 100644 --- a/api/http/handler/teams/handler.go +++ b/api/http/handler/teams/handler.go @@ -12,9 +12,8 @@ import ( // Handler is the HTTP handler used to handle team operations. type Handler struct { *mux.Router - TeamService portainer.TeamService - TeamMembershipService portainer.TeamMembershipService - AuthorizationService *portainer.AuthorizationService + DataStore portainer.DataStore + AuthorizationService *portainer.AuthorizationService } // NewHandler creates a handler to manage team operations. diff --git a/api/http/handler/teams/team_create.go b/api/http/handler/teams/team_create.go index 3cfad7acb..9012b0b88 100644 --- a/api/http/handler/teams/team_create.go +++ b/api/http/handler/teams/team_create.go @@ -28,7 +28,7 @@ func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - team, err := handler.TeamService.TeamByName(payload.Name) + team, err := handler.DataStore.Team().TeamByName(payload.Name) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve teams from the database", err} } @@ -40,7 +40,7 @@ func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *http Name: payload.Name, } - err = handler.TeamService.CreateTeam(team) + err = handler.DataStore.Team().CreateTeam(team) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the team inside the database", err} } diff --git a/api/http/handler/teams/team_delete.go b/api/http/handler/teams/team_delete.go index 2b96e9351..d1629347e 100644 --- a/api/http/handler/teams/team_delete.go +++ b/api/http/handler/teams/team_delete.go @@ -16,19 +16,19 @@ func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusBadRequest, "Invalid team identifier route variable", err} } - _, err = handler.TeamService.Team(portainer.TeamID(teamID)) + _, err = handler.DataStore.Team().Team(portainer.TeamID(teamID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team with the specified identifier inside the database", err} } - err = handler.TeamService.DeleteTeam(portainer.TeamID(teamID)) + err = handler.DataStore.Team().DeleteTeam(portainer.TeamID(teamID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the team from the database", err} } - err = handler.TeamMembershipService.DeleteTeamMembershipByTeamID(portainer.TeamID(teamID)) + err = handler.DataStore.TeamMembership().DeleteTeamMembershipByTeamID(portainer.TeamID(teamID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete associated team memberships from the database", err} } diff --git a/api/http/handler/teams/team_inspect.go b/api/http/handler/teams/team_inspect.go index 0eeb89e8f..f543a8f37 100644 --- a/api/http/handler/teams/team_inspect.go +++ b/api/http/handler/teams/team_inspect.go @@ -26,7 +26,7 @@ func (handler *Handler) teamInspect(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusForbidden, "Access denied to team", portainer.ErrResourceAccessDenied} } - team, err := handler.TeamService.Team(portainer.TeamID(teamID)) + team, err := handler.DataStore.Team().Team(portainer.TeamID(teamID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/teams/team_list.go b/api/http/handler/teams/team_list.go index da67b696b..5056a69b6 100644 --- a/api/http/handler/teams/team_list.go +++ b/api/http/handler/teams/team_list.go @@ -10,7 +10,7 @@ import ( // GET request on /api/teams func (handler *Handler) teamList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - teams, err := handler.TeamService.Teams() + teams, err := handler.DataStore.Team().Teams() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve teams from the database", err} } diff --git a/api/http/handler/teams/team_memberships.go b/api/http/handler/teams/team_memberships.go index e7c26fff6..1d61e1a01 100644 --- a/api/http/handler/teams/team_memberships.go +++ b/api/http/handler/teams/team_memberships.go @@ -26,7 +26,7 @@ func (handler *Handler) teamMemberships(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusForbidden, "Access denied to team", portainer.ErrResourceAccessDenied} } - memberships, err := handler.TeamMembershipService.TeamMembershipsByTeamID(portainer.TeamID(teamID)) + memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByTeamID(portainer.TeamID(teamID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve associated team memberships from the database", err} } diff --git a/api/http/handler/teams/team_update.go b/api/http/handler/teams/team_update.go index 740e4fc61..06e1ef9d0 100644 --- a/api/http/handler/teams/team_update.go +++ b/api/http/handler/teams/team_update.go @@ -30,7 +30,7 @@ func (handler *Handler) teamUpdate(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - team, err := handler.TeamService.Team(portainer.TeamID(teamID)) + team, err := handler.DataStore.Team().Team(portainer.TeamID(teamID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err} } else if err != nil { @@ -41,7 +41,7 @@ func (handler *Handler) teamUpdate(w http.ResponseWriter, r *http.Request) *http team.Name = payload.Name } - err = handler.TeamService.UpdateTeam(team.ID, team) + err = handler.DataStore.Team().UpdateTeam(team.ID, team) if err != nil { return &httperror.HandlerError{http.StatusNotFound, "Unable to persist team changes inside the database", err} } diff --git a/api/http/handler/templates/handler.go b/api/http/handler/templates/handler.go index 994d27306..7360d5252 100644 --- a/api/http/handler/templates/handler.go +++ b/api/http/handler/templates/handler.go @@ -12,7 +12,7 @@ import ( // Handler represents an HTTP API handler for managing templates. type Handler struct { *mux.Router - SettingsService portainer.SettingsService + DataStore portainer.DataStore } // NewHandler returns a new instance of Handler. diff --git a/api/http/handler/templates/template_list.go b/api/http/handler/templates/template_list.go index 5d86941dd..236b5cfdd 100644 --- a/api/http/handler/templates/template_list.go +++ b/api/http/handler/templates/template_list.go @@ -9,7 +9,7 @@ import ( // GET request on /api/templates func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } diff --git a/api/http/handler/users/admin_check.go b/api/http/handler/users/admin_check.go index 0f53e6693..7c1a54f33 100644 --- a/api/http/handler/users/admin_check.go +++ b/api/http/handler/users/admin_check.go @@ -10,7 +10,7 @@ import ( // GET request on /api/users/admin/check func (handler *Handler) adminCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - users, err := handler.UserService.UsersByRole(portainer.AdministratorRole) + users, err := handler.DataStore.User().UsersByRole(portainer.AdministratorRole) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err} } diff --git a/api/http/handler/users/admin_init.go b/api/http/handler/users/admin_init.go index a57e95f62..67010b660 100644 --- a/api/http/handler/users/admin_init.go +++ b/api/http/handler/users/admin_init.go @@ -33,7 +33,7 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - users, err := handler.UserService.UsersByRole(portainer.AdministratorRole) + users, err := handler.DataStore.User().UsersByRole(portainer.AdministratorRole) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err} } @@ -53,7 +53,7 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure} } - err = handler.UserService.CreateUser(user) + err = handler.DataStore.User().CreateUser(user) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err} } diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index 646bf8ae5..cbdc9eaf7 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -17,13 +17,9 @@ func hideFields(user *portainer.User) { // Handler is the HTTP handler used to handle user operations. type Handler struct { *mux.Router - UserService portainer.UserService - TeamService portainer.TeamService - TeamMembershipService portainer.TeamMembershipService - ResourceControlService portainer.ResourceControlService - CryptoService portainer.CryptoService - SettingsService portainer.SettingsService - AuthorizationService *portainer.AuthorizationService + DataStore portainer.DataStore + CryptoService portainer.CryptoService + AuthorizationService *portainer.AuthorizationService } // NewHandler creates a handler to manage user operations. diff --git a/api/http/handler/users/user_create.go b/api/http/handler/users/user_create.go index 582ca084d..43bcab0e7 100644 --- a/api/http/handler/users/user_create.go +++ b/api/http/handler/users/user_create.go @@ -49,7 +49,7 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create administrator user", portainer.ErrResourceAccessDenied} } - user, err := handler.UserService.UserByUsername(payload.Username) + user, err := handler.DataStore.User().UserByUsername(payload.Username) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err} } @@ -63,7 +63,7 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), } - settings, err := handler.SettingsService.Settings() + settings, err := handler.DataStore.Settings().Settings() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} } @@ -75,7 +75,7 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http } } - err = handler.UserService.CreateUser(user) + err = handler.DataStore.User().CreateUser(user) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err} } diff --git a/api/http/handler/users/user_delete.go b/api/http/handler/users/user_delete.go index 203a3be47..df72fe673 100644 --- a/api/http/handler/users/user_delete.go +++ b/api/http/handler/users/user_delete.go @@ -26,7 +26,7 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusForbidden, "Cannot remove your own user account. Contact another administrator", portainer.ErrAdminCannotRemoveSelf} } - user, err := handler.UserService.User(portainer.UserID(userID)) + user, err := handler.DataStore.User().User(portainer.UserID(userID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} } else if err != nil { @@ -45,7 +45,7 @@ func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.U return handler.deleteUser(w, user) } - users, err := handler.UserService.Users() + users, err := handler.DataStore.User().Users() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err} } @@ -65,12 +65,12 @@ func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.U } func (handler *Handler) deleteUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError { - err := handler.UserService.DeleteUser(user.ID) + err := handler.DataStore.User().DeleteUser(user.ID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user from the database", err} } - err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(user.ID) + err = handler.DataStore.TeamMembership().DeleteTeamMembershipByUserID(user.ID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user memberships from the database", err} } diff --git a/api/http/handler/users/user_inspect.go b/api/http/handler/users/user_inspect.go index 4c5954283..b35d67d3a 100644 --- a/api/http/handler/users/user_inspect.go +++ b/api/http/handler/users/user_inspect.go @@ -27,7 +27,7 @@ func (handler *Handler) userInspect(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusForbidden, "Permission denied inspect user", portainer.ErrResourceAccessDenied} } - user, err := handler.UserService.User(portainer.UserID(userID)) + user, err := handler.DataStore.User().User(portainer.UserID(userID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/users/user_list.go b/api/http/handler/users/user_list.go index 5792689a0..f79b65bb3 100644 --- a/api/http/handler/users/user_list.go +++ b/api/http/handler/users/user_list.go @@ -10,7 +10,7 @@ import ( // GET request on /api/users func (handler *Handler) userList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - users, err := handler.UserService.Users() + users, err := handler.DataStore.User().Users() if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err} } diff --git a/api/http/handler/users/user_memberships.go b/api/http/handler/users/user_memberships.go index 090880e6b..3d4655bbb 100644 --- a/api/http/handler/users/user_memberships.go +++ b/api/http/handler/users/user_memberships.go @@ -26,7 +26,7 @@ func (handler *Handler) userMemberships(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user memberships", portainer.ErrUnauthorized} } - memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(portainer.UserID(userID)) + memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(portainer.UserID(userID)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist membership changes inside the database", err} } diff --git a/api/http/handler/users/user_update.go b/api/http/handler/users/user_update.go index 62ba24f46..e61a62085 100644 --- a/api/http/handler/users/user_update.go +++ b/api/http/handler/users/user_update.go @@ -48,7 +48,7 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user to administrator role", portainer.ErrResourceAccessDenied} } - user, err := handler.UserService.User(portainer.UserID(userID)) + user, err := handler.DataStore.User().User(portainer.UserID(userID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} } else if err != nil { @@ -66,7 +66,7 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http user.Role = portainer.UserRole(payload.Role) } - err = handler.UserService.UpdateUser(user.ID, user) + err = handler.DataStore.User().UpdateUser(user.ID, user) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user changes inside the database", err} } diff --git a/api/http/handler/users/user_update_password.go b/api/http/handler/users/user_update_password.go index 77ce99b49..8905a0462 100644 --- a/api/http/handler/users/user_update_password.go +++ b/api/http/handler/users/user_update_password.go @@ -48,7 +48,7 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - user, err := handler.UserService.User(portainer.UserID(userID)) + user, err := handler.DataStore.User().User(portainer.UserID(userID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} } else if err != nil { @@ -65,7 +65,7 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure} } - err = handler.UserService.UpdateUser(user.ID, user) + err = handler.DataStore.User().UpdateUser(user.ID, user) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user changes inside the database", err} } diff --git a/api/http/handler/webhooks/handler.go b/api/http/handler/webhooks/handler.go index 2e342114e..aa5046cfe 100644 --- a/api/http/handler/webhooks/handler.go +++ b/api/http/handler/webhooks/handler.go @@ -13,8 +13,7 @@ import ( // Handler is the HTTP handler used to handle webhook operations. type Handler struct { *mux.Router - WebhookService portainer.WebhookService - EndpointService portainer.EndpointService + DataStore portainer.DataStore DockerClientFactory *docker.ClientFactory } diff --git a/api/http/handler/webhooks/webhook_create.go b/api/http/handler/webhooks/webhook_create.go index 883521608..5b960ec2d 100644 --- a/api/http/handler/webhooks/webhook_create.go +++ b/api/http/handler/webhooks/webhook_create.go @@ -37,7 +37,7 @@ func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } - webhook, err := handler.WebhookService.WebhookByResourceID(payload.ResourceID) + webhook, err := handler.DataStore.Webhook().WebhookByResourceID(payload.ResourceID) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "An error occurred retrieving webhooks from the database", err} } @@ -57,7 +57,7 @@ func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *h WebhookType: portainer.WebhookType(payload.WebhookType), } - err = handler.WebhookService.CreateWebhook(webhook) + err = handler.DataStore.Webhook().CreateWebhook(webhook) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the webhook inside the database", err} } diff --git a/api/http/handler/webhooks/webhook_delete.go b/api/http/handler/webhooks/webhook_delete.go index ee36f822c..305dbca6b 100644 --- a/api/http/handler/webhooks/webhook_delete.go +++ b/api/http/handler/webhooks/webhook_delete.go @@ -16,7 +16,7 @@ func (handler *Handler) webhookDelete(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusBadRequest, "Invalid webhook id", err} } - err = handler.WebhookService.DeleteWebhook(portainer.WebhookID(id)) + err = handler.DataStore.Webhook().DeleteWebhook(portainer.WebhookID(id)) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the webhook from the database", err} } diff --git a/api/http/handler/webhooks/webhook_execute.go b/api/http/handler/webhooks/webhook_execute.go index 2e82f0e1a..11e62fd99 100644 --- a/api/http/handler/webhooks/webhook_execute.go +++ b/api/http/handler/webhooks/webhook_execute.go @@ -21,7 +21,7 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) * return &httperror.HandlerError{http.StatusInternalServerError, "Invalid service id parameter", err} } - webhook, err := handler.WebhookService.WebhookByToken(webhookToken) + webhook, err := handler.DataStore.Webhook().WebhookByToken(webhookToken) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a webhook with this token", err} @@ -33,7 +33,7 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) * endpointID := webhook.EndpointID webhookType := webhook.WebhookType - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { diff --git a/api/http/handler/webhooks/webhook_list.go b/api/http/handler/webhooks/webhook_list.go index a45051884..df9f5550c 100644 --- a/api/http/handler/webhooks/webhook_list.go +++ b/api/http/handler/webhooks/webhook_list.go @@ -22,7 +22,7 @@ func (handler *Handler) webhookList(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err} } - webhooks, err := handler.WebhookService.Webhooks() + webhooks, err := handler.DataStore.Webhook().Webhooks() webhooks = filterWebhooks(webhooks, &filters) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve webhooks from the database", err} diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index 5de214eca..2a498f82e 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -32,7 +32,7 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index afe670a56..8c3318a83 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -39,7 +39,7 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} } - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { diff --git a/api/http/handler/websocket/handler.go b/api/http/handler/websocket/handler.go index cc0165eb0..eb86de0a9 100644 --- a/api/http/handler/websocket/handler.go +++ b/api/http/handler/websocket/handler.go @@ -11,7 +11,7 @@ import ( // Handler is the HTTP handler used to handle websocket operations. type Handler struct { *mux.Router - EndpointService portainer.EndpointService + DataStore portainer.DataStore SignatureService portainer.DigitalSignatureService ReverseTunnelService portainer.ReverseTunnelService requestBouncer *security.RequestBouncer diff --git a/api/http/proxy/factory/docker.go b/api/http/proxy/factory/docker.go index acf0731bd..149c488d9 100644 --- a/api/http/proxy/factory/docker.go +++ b/api/http/proxy/factory/docker.go @@ -56,18 +56,11 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h } transportParameters := &docker.TransportParameters{ - Endpoint: endpoint, - ResourceControlService: factory.resourceControlService, - UserService: factory.userService, - TeamService: factory.teamService, - TeamMembershipService: factory.teamMembershipService, - RegistryService: factory.registryService, - DockerHubService: factory.dockerHubService, - SettingsService: factory.settingsService, - ReverseTunnelService: factory.reverseTunnelService, - ExtensionService: factory.extensionService, - SignatureService: factory.signatureService, - DockerClientFactory: factory.dockerClientFactory, + Endpoint: endpoint, + DataStore: factory.dataStore, + ReverseTunnelService: factory.reverseTunnelService, + SignatureService: factory.signatureService, + DockerClientFactory: factory.dockerClientFactory, } dockerTransport, err := docker.NewTransport(transportParameters, httpTransport) diff --git a/api/http/proxy/factory/docker/access_control.go b/api/http/proxy/factory/docker/access_control.go index 2b8e183f9..d3429f3b6 100644 --- a/api/http/proxy/factory/docker/access_control.go +++ b/api/http/proxy/factory/docker/access_control.go @@ -32,7 +32,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil { resourceControl := portainer.NewPublicResourceControl(resourceID, resourceType) - err := transport.resourceControlService.CreateResourceControl(resourceControl) + err := transport.dataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { return nil, err } @@ -57,7 +57,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m userIDs := make([]portainer.UserID, 0) for _, name := range teamNames { - team, err := transport.teamService.TeamByName(name) + team, err := transport.dataStore.Team().TeamByName(name) if err != nil { log.Printf("[WARN] [http,proxy,docker] [message: unknown team name in access control label, ignoring access control rule for this team] [name: %s] [resource_id: %s]", name, resourceID) continue @@ -67,7 +67,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m } for _, name := range userNames { - user, err := transport.userService.UserByUsername(name) + user, err := transport.dataStore.User().UserByUsername(name) if err != nil { log.Printf("[WARN] [http,proxy,docker] [message: unknown user name in access control label, ignoring access control rule for this user] [name: %s] [resource_id: %s]", name, resourceID) continue @@ -78,7 +78,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m resourceControl := portainer.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs) - err := transport.resourceControlService.CreateResourceControl(resourceControl) + err := transport.dataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { return nil, err } @@ -92,7 +92,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m func (transport *Transport) createPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) (*portainer.ResourceControl, error) { resourceControl := portainer.NewPrivateResourceControl(resourceIdentifier, resourceType, userID) - err := transport.resourceControlService.CreateResourceControl(resourceControl) + err := transport.dataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { log.Printf("[ERROR] [http,proxy,docker,transport] [message: unable to persist resource control] [resource: %s] [err: %s]", resourceIdentifier, err) return nil, err diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index c511948fa..2456cc860 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -24,36 +24,22 @@ type ( // Transport is a custom transport for Docker API reverse proxy. It allows // interception of requests and rewriting of responses. Transport struct { - HTTPTransport *http.Transport - endpoint *portainer.Endpoint - resourceControlService portainer.ResourceControlService - userService portainer.UserService - teamService portainer.TeamService - teamMembershipService portainer.TeamMembershipService - registryService portainer.RegistryService - dockerHubService portainer.DockerHubService - settingsService portainer.SettingsService - signatureService portainer.DigitalSignatureService - reverseTunnelService portainer.ReverseTunnelService - extensionService portainer.ExtensionService - dockerClient *client.Client - dockerClientFactory *docker.ClientFactory + HTTPTransport *http.Transport + endpoint *portainer.Endpoint + dataStore portainer.DataStore + signatureService portainer.DigitalSignatureService + reverseTunnelService portainer.ReverseTunnelService + dockerClient *client.Client + dockerClientFactory *docker.ClientFactory } // TransportParameters is used to create a new Transport TransportParameters struct { - Endpoint *portainer.Endpoint - ResourceControlService portainer.ResourceControlService - UserService portainer.UserService - TeamService portainer.TeamService - TeamMembershipService portainer.TeamMembershipService - RegistryService portainer.RegistryService - DockerHubService portainer.DockerHubService - SettingsService portainer.SettingsService - SignatureService portainer.DigitalSignatureService - ReverseTunnelService portainer.ReverseTunnelService - ExtensionService portainer.ExtensionService - DockerClientFactory *docker.ClientFactory + Endpoint *portainer.Endpoint + DataStore portainer.DataStore + SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService + DockerClientFactory *docker.ClientFactory } restrictedDockerOperationContext struct { @@ -80,20 +66,13 @@ func NewTransport(parameters *TransportParameters, httpTransport *http.Transport } transport := &Transport{ - endpoint: parameters.Endpoint, - resourceControlService: parameters.ResourceControlService, - userService: parameters.UserService, - teamService: parameters.TeamService, - teamMembershipService: parameters.TeamMembershipService, - registryService: parameters.RegistryService, - dockerHubService: parameters.DockerHubService, - settingsService: parameters.SettingsService, - signatureService: parameters.SignatureService, - reverseTunnelService: parameters.ReverseTunnelService, - extensionService: parameters.ExtensionService, - dockerClientFactory: parameters.DockerClientFactory, - HTTPTransport: httpTransport, - dockerClient: dockerClient, + endpoint: parameters.Endpoint, + dataStore: parameters.DataStore, + signatureService: parameters.SignatureService, + reverseTunnelService: parameters.ReverseTunnelService, + dockerClientFactory: parameters.DockerClientFactory, + HTTPTransport: httpTransport, + dockerClient: dockerClient, } return transport, nil @@ -429,18 +408,18 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r } if tokenData.Role != portainer.AdministratorRole { - rbacExtension, err := transport.extensionService.Extension(portainer.RBACExtension) + rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension) if err != nil && err != portainer.ErrObjectNotFound { return nil, err } - user, err := transport.userService.User(tokenData.ID) + user, err := transport.dataStore.User().User(tokenData.ID) if err != nil { return nil, err } if volumeBrowseRestrictionCheck { - settings, err := transport.settingsService.Settings() + settings, err := transport.dataStore.Settings().Settings() if err != nil { return nil, err } @@ -468,7 +447,7 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r return transport.executeDockerRequest(request) } - teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID) + teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID) if err != nil { return nil, err } @@ -478,7 +457,7 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r userTeamIDs = append(userTeamIDs, membership.TeamID) } - resourceControls, err := transport.resourceControlService.ResourceControls() + resourceControls, err := transport.dataStore.ResourceControl().ResourceControls() if err != nil { return nil, err } @@ -516,7 +495,7 @@ func (transport *Transport) rewriteOperationWithLabelFiltering(request *http.Req return nil, err } - settings, err := transport.settingsService.Settings() + settings, err := transport.dataStore.Settings().Settings() if err != nil { return nil, err } @@ -610,13 +589,13 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt return response, err } - resourceControl, err := transport.resourceControlService.ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType) + resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType) if err != nil { return response, err } if resourceControl != nil { - err = transport.resourceControlService.DeleteResourceControl(resourceControl.ID) + err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID) if err != nil { return response, err } @@ -661,13 +640,13 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) ( userID: tokenData.ID, } - hub, err := transport.dockerHubService.DockerHub() + hub, err := transport.dataStore.DockerHub().DockerHub() if err != nil { return nil, err } accessContext.dockerHub = hub - registries, err := transport.registryService.Registries() + registries, err := transport.dataStore.Registry().Registries() if err != nil { return nil, err } @@ -676,7 +655,7 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) ( if tokenData.Role != portainer.AdministratorRole { accessContext.isAdmin = false - teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID) + teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID) if err != nil { return nil, err } @@ -694,7 +673,7 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest return nil, err } - resourceControls, err := transport.resourceControlService.ResourceControls() + resourceControls, err := transport.dataStore.ResourceControl().ResourceControls() if err != nil { return nil, err } @@ -709,7 +688,7 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest if tokenData.Role != portainer.AdministratorRole { operationContext.isAdmin = false - user, err := transport.userService.User(operationContext.userID) + user, err := transport.dataStore.User().User(operationContext.userID) if err != nil { return nil, err } @@ -719,7 +698,7 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest operationContext.endpointResourceAccess = true } - teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID) + teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID) if err != nil { return nil, err } diff --git a/api/http/proxy/factory/docker_unix.go b/api/http/proxy/factory/docker_unix.go index 214b50747..32a572d30 100644 --- a/api/http/proxy/factory/docker_unix.go +++ b/api/http/proxy/factory/docker_unix.go @@ -12,18 +12,11 @@ import ( func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portainer.Endpoint) (http.Handler, error) { transportParameters := &docker.TransportParameters{ - Endpoint: endpoint, - ResourceControlService: factory.resourceControlService, - UserService: factory.userService, - TeamService: factory.teamService, - TeamMembershipService: factory.teamMembershipService, - RegistryService: factory.registryService, - DockerHubService: factory.dockerHubService, - SettingsService: factory.settingsService, - ReverseTunnelService: factory.reverseTunnelService, - ExtensionService: factory.extensionService, - SignatureService: factory.signatureService, - DockerClientFactory: factory.dockerClientFactory, + Endpoint: endpoint, + DataStore: factory.dataStore, + ReverseTunnelService: factory.reverseTunnelService, + SignatureService: factory.signatureService, + DockerClientFactory: factory.dockerClientFactory, } proxy := &dockerLocalProxy{} diff --git a/api/http/proxy/factory/docker_windows.go b/api/http/proxy/factory/docker_windows.go index 50ba768f7..fb71b91d1 100644 --- a/api/http/proxy/factory/docker_windows.go +++ b/api/http/proxy/factory/docker_windows.go @@ -13,18 +13,11 @@ import ( func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portainer.Endpoint) (http.Handler, error) { transportParameters := &docker.TransportParameters{ - Endpoint: endpoint, - ResourceControlService: factory.resourceControlService, - UserService: factory.userService, - TeamService: factory.teamService, - TeamMembershipService: factory.teamMembershipService, - RegistryService: factory.registryService, - DockerHubService: factory.dockerHubService, - SettingsService: factory.settingsService, - ReverseTunnelService: factory.reverseTunnelService, - ExtensionService: factory.extensionService, - SignatureService: factory.signatureService, - DockerClientFactory: factory.dockerClientFactory, + Endpoint: endpoint, + DataStore: factory.dataStore, + ReverseTunnelService: factory.reverseTunnelService, + SignatureService: factory.signatureService, + DockerClientFactory: factory.dockerClientFactory, } proxy := &dockerLocalProxy{} diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go index 3f44b1211..b81291ab9 100644 --- a/api/http/proxy/factory/factory.go +++ b/api/http/proxy/factory/factory.go @@ -19,49 +19,20 @@ var extensionPorts = map[portainer.ExtensionID]string{ type ( // ProxyFactory is a factory to create reverse proxies to Docker endpoints and extensions ProxyFactory struct { - resourceControlService portainer.ResourceControlService - userService portainer.UserService - teamService portainer.TeamService - teamMembershipService portainer.TeamMembershipService - settingsService portainer.SettingsService - registryService portainer.RegistryService - dockerHubService portainer.DockerHubService - signatureService portainer.DigitalSignatureService - reverseTunnelService portainer.ReverseTunnelService - extensionService portainer.ExtensionService - dockerClientFactory *docker.ClientFactory - } - - // ProxyFactoryParameters is used to create a new ProxyFactory - ProxyFactoryParameters struct { - ResourceControlService portainer.ResourceControlService - UserService portainer.UserService - TeamService portainer.TeamService - TeamMembershipService portainer.TeamMembershipService - SettingsService portainer.SettingsService - RegistryService portainer.RegistryService - DockerHubService portainer.DockerHubService - SignatureService portainer.DigitalSignatureService - ReverseTunnelService portainer.ReverseTunnelService - ExtensionService portainer.ExtensionService - DockerClientFactory *docker.ClientFactory + dataStore portainer.DataStore + signatureService portainer.DigitalSignatureService + reverseTunnelService portainer.ReverseTunnelService + dockerClientFactory *docker.ClientFactory } ) // NewProxyFactory returns a pointer to a new instance of a ProxyFactory -func NewProxyFactory(parameters *ProxyFactoryParameters) *ProxyFactory { +func NewProxyFactory(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory) *ProxyFactory { return &ProxyFactory{ - resourceControlService: parameters.ResourceControlService, - userService: parameters.UserService, - teamService: parameters.TeamService, - teamMembershipService: parameters.TeamMembershipService, - settingsService: parameters.SettingsService, - registryService: parameters.RegistryService, - dockerHubService: parameters.DockerHubService, - signatureService: parameters.SignatureService, - reverseTunnelService: parameters.ReverseTunnelService, - extensionService: parameters.ExtensionService, - dockerClientFactory: parameters.DockerClientFactory, + dataStore: dataStore, + signatureService: signatureService, + reverseTunnelService: tunnelService, + dockerClientFactory: clientFactory, } } diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index fa27f6399..336481f9c 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -20,44 +20,15 @@ type ( extensionProxies cmap.ConcurrentMap legacyExtensionProxies cmap.ConcurrentMap } - - // ManagerParams represents the required parameters to create a new Manager instance. - ManagerParams struct { - ResourceControlService portainer.ResourceControlService - UserService portainer.UserService - TeamService portainer.TeamService - TeamMembershipService portainer.TeamMembershipService - SettingsService portainer.SettingsService - RegistryService portainer.RegistryService - DockerHubService portainer.DockerHubService - SignatureService portainer.DigitalSignatureService - ReverseTunnelService portainer.ReverseTunnelService - ExtensionService portainer.ExtensionService - DockerClientFactory *docker.ClientFactory - } ) // NewManager initializes a new proxy Service -func NewManager(parameters *ManagerParams) *Manager { - proxyFactoryParameters := &factory.ProxyFactoryParameters{ - ResourceControlService: parameters.ResourceControlService, - UserService: parameters.UserService, - TeamService: parameters.TeamService, - TeamMembershipService: parameters.TeamMembershipService, - SettingsService: parameters.SettingsService, - RegistryService: parameters.RegistryService, - DockerHubService: parameters.DockerHubService, - SignatureService: parameters.SignatureService, - ReverseTunnelService: parameters.ReverseTunnelService, - ExtensionService: parameters.ExtensionService, - DockerClientFactory: parameters.DockerClientFactory, - } - +func NewManager(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory) *Manager { return &Manager{ endpointProxies: cmap.New(), extensionProxies: cmap.New(), legacyExtensionProxies: cmap.New(), - proxyFactory: factory.NewProxyFactory(proxyFactoryParameters), + proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory), } } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 5bcfe0c72..d6ded7f62 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -13,26 +13,10 @@ import ( type ( // RequestBouncer represents an entity that manages API request accesses RequestBouncer struct { - jwtService portainer.JWTService - userService portainer.UserService - teamMembershipService portainer.TeamMembershipService - endpointService portainer.EndpointService - endpointGroupService portainer.EndpointGroupService - extensionService portainer.ExtensionService - rbacExtensionClient *rbacExtensionClient - authDisabled bool - } - - // RequestBouncerParams represents the required parameters to create a new RequestBouncer instance. - RequestBouncerParams struct { - JWTService portainer.JWTService - UserService portainer.UserService - TeamMembershipService portainer.TeamMembershipService - EndpointService portainer.EndpointService - EndpointGroupService portainer.EndpointGroupService - ExtensionService portainer.ExtensionService - RBACExtensionURL string - AuthDisabled bool + dataStore portainer.DataStore + jwtService portainer.JWTService + rbacExtensionClient *rbacExtensionClient + authDisabled bool } // RestrictedRequestContext is a data structure containing information @@ -46,16 +30,12 @@ type ( ) // NewRequestBouncer initializes a new RequestBouncer -func NewRequestBouncer(parameters *RequestBouncerParams) *RequestBouncer { +func NewRequestBouncer(dataStore portainer.DataStore, jwtService portainer.JWTService, authenticationDisabled bool, rbacExtensionURL string) *RequestBouncer { return &RequestBouncer{ - jwtService: parameters.JWTService, - userService: parameters.UserService, - teamMembershipService: parameters.TeamMembershipService, - endpointService: parameters.EndpointService, - endpointGroupService: parameters.EndpointGroupService, - extensionService: parameters.ExtensionService, - rbacExtensionClient: newRBACExtensionClient(parameters.RBACExtensionURL), - authDisabled: parameters.AuthDisabled, + dataStore: dataStore, + jwtService: jwtService, + rbacExtensionClient: newRBACExtensionClient(rbacExtensionURL), + authDisabled: authenticationDisabled, } } @@ -121,12 +101,12 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp return nil } - memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(tokenData.ID) + memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID) if err != nil { return err } - group, err := bouncer.endpointGroupService.EndpointGroup(endpoint.GroupID) + group, err := bouncer.dataStore.EndpointGroup().EndpointGroup(endpoint.GroupID) if err != nil { return err } @@ -173,14 +153,14 @@ func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Reque return nil } - extension, err := bouncer.extensionService.Extension(portainer.RBACExtension) + extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension) if err == portainer.ErrObjectNotFound { return nil } else if err != nil { return err } - user, err := bouncer.userService.User(tokenData.ID) + user, err := bouncer.dataStore.User().User(tokenData.ID) if err != nil { return err } @@ -208,7 +188,7 @@ func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portain return nil } - memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(tokenData.ID) + memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID) if err != nil { return err } @@ -244,7 +224,7 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, return } - extension, err := bouncer.extensionService.Extension(portainer.RBACExtension) + extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension) if err == portainer.ErrObjectNotFound { if administratorOnly { httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrUnauthorized) @@ -258,7 +238,7 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, return } - user, err := bouncer.userService.User(tokenData.ID) + user, err := bouncer.dataStore.User().User(tokenData.ID) if err != nil && err == portainer.ErrObjectNotFound { httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) return @@ -335,7 +315,7 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han return } - _, err = bouncer.userService.User(tokenData.ID) + _, err = bouncer.dataStore.User().User(tokenData.ID) if err != nil && err == portainer.ErrObjectNotFound { httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) return @@ -372,7 +352,7 @@ func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.User if userRole != portainer.AdministratorRole { requestContext.IsAdmin = false - memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(userID) + memberships, err := bouncer.dataStore.TeamMembership().TeamMembershipsByUserID(userID) if err != nil { return nil, err } diff --git a/api/http/server.go b/api/http/server.go index fc9d742e6..40e2a880f 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -45,163 +45,87 @@ import ( // Server implements the portainer.Server interface type Server struct { - BindAddress string - AssetsPath string - AuthDisabled bool - Status *portainer.Status - ReverseTunnelService portainer.ReverseTunnelService - ExtensionManager portainer.ExtensionManager - ComposeStackManager portainer.ComposeStackManager - CryptoService portainer.CryptoService - SignatureService portainer.DigitalSignatureService - JobScheduler portainer.JobScheduler - Snapshotter portainer.Snapshotter - RoleService portainer.RoleService - DockerHubService portainer.DockerHubService - EdgeGroupService portainer.EdgeGroupService - EdgeStackService portainer.EdgeStackService - EndpointService portainer.EndpointService - EndpointGroupService portainer.EndpointGroupService - EndpointRelationService portainer.EndpointRelationService - FileService portainer.FileService - GitService portainer.GitService - JWTService portainer.JWTService - LDAPService portainer.LDAPService - ExtensionService portainer.ExtensionService - RegistryService portainer.RegistryService - ResourceControlService portainer.ResourceControlService - ScheduleService portainer.ScheduleService - SettingsService portainer.SettingsService - StackService portainer.StackService - SwarmStackManager portainer.SwarmStackManager - TagService portainer.TagService - TeamService portainer.TeamService - TeamMembershipService portainer.TeamMembershipService - UserService portainer.UserService - WebhookService portainer.WebhookService - Handler *handler.Handler - SSL bool - SSLCert string - SSLKey string - DockerClientFactory *docker.ClientFactory - JobService portainer.JobService + BindAddress string + AssetsPath string + AuthDisabled bool + Status *portainer.Status + ReverseTunnelService portainer.ReverseTunnelService + ExtensionManager portainer.ExtensionManager + ComposeStackManager portainer.ComposeStackManager + CryptoService portainer.CryptoService + SignatureService portainer.DigitalSignatureService + JobScheduler portainer.JobScheduler + Snapshotter portainer.Snapshotter + FileService portainer.FileService + DataStore portainer.DataStore + GitService portainer.GitService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + SwarmStackManager portainer.SwarmStackManager + Handler *handler.Handler + SSL bool + SSLCert string + SSLKey string + DockerClientFactory *docker.ClientFactory + JobService portainer.JobService } // Start starts the HTTP server func (server *Server) Start() error { - proxyManagerParameters := &proxy.ManagerParams{ - ResourceControlService: server.ResourceControlService, - UserService: server.UserService, - TeamService: server.TeamService, - TeamMembershipService: server.TeamMembershipService, - SettingsService: server.SettingsService, - RegistryService: server.RegistryService, - DockerHubService: server.DockerHubService, - SignatureService: server.SignatureService, - ReverseTunnelService: server.ReverseTunnelService, - ExtensionService: server.ExtensionService, - DockerClientFactory: server.DockerClientFactory, - } - proxyManager := proxy.NewManager(proxyManagerParameters) + proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory) - authorizationServiceParameters := &portainer.AuthorizationServiceParameters{ - EndpointService: server.EndpointService, - EndpointGroupService: server.EndpointGroupService, - RegistryService: server.RegistryService, - RoleService: server.RoleService, - TeamMembershipService: server.TeamMembershipService, - UserService: server.UserService, - } - authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters) + authorizationService := portainer.NewAuthorizationService(server.DataStore) - requestBouncerParameters := &security.RequestBouncerParams{ - JWTService: server.JWTService, - UserService: server.UserService, - TeamMembershipService: server.TeamMembershipService, - EndpointService: server.EndpointService, - EndpointGroupService: server.EndpointGroupService, - ExtensionService: server.ExtensionService, - RBACExtensionURL: proxyManager.GetExtensionURL(portainer.RBACExtension), - AuthDisabled: server.AuthDisabled, - } - requestBouncer := security.NewRequestBouncer(requestBouncerParameters) + rbacExtensionURL := proxyManager.GetExtensionURL(portainer.RBACExtension) + requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService, server.AuthDisabled, rbacExtensionURL) rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour) var authHandler = auth.NewHandler(requestBouncer, rateLimiter, server.AuthDisabled) - authHandler.UserService = server.UserService + authHandler.DataStore = server.DataStore authHandler.CryptoService = server.CryptoService authHandler.JWTService = server.JWTService authHandler.LDAPService = server.LDAPService - authHandler.SettingsService = server.SettingsService - authHandler.TeamService = server.TeamService - authHandler.TeamMembershipService = server.TeamMembershipService - authHandler.ExtensionService = server.ExtensionService - authHandler.EndpointService = server.EndpointService - authHandler.EndpointGroupService = server.EndpointGroupService - authHandler.RoleService = server.RoleService authHandler.ProxyManager = proxyManager authHandler.AuthorizationService = authorizationService var roleHandler = roles.NewHandler(requestBouncer) - roleHandler.RoleService = server.RoleService + roleHandler.DataStore = server.DataStore var dockerHubHandler = dockerhub.NewHandler(requestBouncer) - dockerHubHandler.DockerHubService = server.DockerHubService + dockerHubHandler.DataStore = server.DataStore var edgeGroupsHandler = edgegroups.NewHandler(requestBouncer) - edgeGroupsHandler.EdgeGroupService = server.EdgeGroupService - edgeGroupsHandler.EdgeStackService = server.EdgeStackService - edgeGroupsHandler.EndpointService = server.EndpointService - edgeGroupsHandler.EndpointGroupService = server.EndpointGroupService - edgeGroupsHandler.EndpointRelationService = server.EndpointRelationService - edgeGroupsHandler.TagService = server.TagService + edgeGroupsHandler.DataStore = server.DataStore var edgeStacksHandler = edgestacks.NewHandler(requestBouncer) - edgeStacksHandler.EdgeGroupService = server.EdgeGroupService - edgeStacksHandler.EdgeStackService = server.EdgeStackService - edgeStacksHandler.EndpointService = server.EndpointService - edgeStacksHandler.EndpointGroupService = server.EndpointGroupService - edgeStacksHandler.EndpointRelationService = server.EndpointRelationService + edgeStacksHandler.DataStore = server.DataStore edgeStacksHandler.FileService = server.FileService edgeStacksHandler.GitService = server.GitService var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer) - edgeTemplatesHandler.SettingsService = server.SettingsService + edgeTemplatesHandler.DataStore = server.DataStore var endpointHandler = endpoints.NewHandler(requestBouncer) + endpointHandler.DataStore = server.DataStore endpointHandler.AuthorizationService = authorizationService - endpointHandler.EdgeGroupService = server.EdgeGroupService - endpointHandler.EdgeStackService = server.EdgeStackService - endpointHandler.EndpointService = server.EndpointService - endpointHandler.EndpointGroupService = server.EndpointGroupService - endpointHandler.EndpointRelationService = server.EndpointRelationService endpointHandler.FileService = server.FileService endpointHandler.JobService = server.JobService endpointHandler.ProxyManager = proxyManager endpointHandler.ReverseTunnelService = server.ReverseTunnelService - endpointHandler.SettingsService = server.SettingsService endpointHandler.Snapshotter = server.Snapshotter - endpointHandler.TagService = server.TagService var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer) - endpointEdgeHandler.EdgeStackService = server.EdgeStackService - endpointEdgeHandler.EndpointService = server.EndpointService + endpointEdgeHandler.DataStore = server.DataStore endpointEdgeHandler.FileService = server.FileService var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer) + endpointGroupHandler.DataStore = server.DataStore endpointGroupHandler.AuthorizationService = authorizationService - endpointGroupHandler.EdgeGroupService = server.EdgeGroupService - endpointGroupHandler.EdgeStackService = server.EdgeStackService - endpointGroupHandler.EndpointService = server.EndpointService - endpointGroupHandler.EndpointGroupService = server.EndpointGroupService - endpointGroupHandler.EndpointRelationService = server.EndpointRelationService - endpointGroupHandler.TagService = server.TagService var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer) - endpointProxyHandler.EndpointService = server.EndpointService + endpointProxyHandler.DataStore = server.DataStore endpointProxyHandler.ProxyManager = proxyManager - endpointProxyHandler.SettingsService = server.SettingsService endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) @@ -209,70 +133,48 @@ func (server *Server) Start() error { var motdHandler = motd.NewHandler(requestBouncer) var extensionHandler = extensions.NewHandler(requestBouncer) - extensionHandler.ExtensionService = server.ExtensionService + extensionHandler.DataStore = server.DataStore extensionHandler.ExtensionManager = server.ExtensionManager - extensionHandler.EndpointGroupService = server.EndpointGroupService - extensionHandler.EndpointService = server.EndpointService - extensionHandler.RegistryService = server.RegistryService extensionHandler.AuthorizationService = authorizationService var registryHandler = registries.NewHandler(requestBouncer) - registryHandler.RegistryService = server.RegistryService - registryHandler.ExtensionService = server.ExtensionService + registryHandler.DataStore = server.DataStore registryHandler.FileService = server.FileService registryHandler.ProxyManager = proxyManager var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer) - resourceControlHandler.ResourceControlService = server.ResourceControlService + resourceControlHandler.DataStore = server.DataStore var schedulesHandler = schedules.NewHandler(requestBouncer) - schedulesHandler.ScheduleService = server.ScheduleService - schedulesHandler.EndpointService = server.EndpointService + schedulesHandler.DataStore = server.DataStore schedulesHandler.FileService = server.FileService schedulesHandler.JobService = server.JobService schedulesHandler.JobScheduler = server.JobScheduler - schedulesHandler.SettingsService = server.SettingsService schedulesHandler.ReverseTunnelService = server.ReverseTunnelService var settingsHandler = settings.NewHandler(requestBouncer) - settingsHandler.SettingsService = server.SettingsService + settingsHandler.DataStore = server.DataStore settingsHandler.LDAPService = server.LDAPService settingsHandler.FileService = server.FileService settingsHandler.JobScheduler = server.JobScheduler - settingsHandler.ScheduleService = server.ScheduleService - settingsHandler.RoleService = server.RoleService - settingsHandler.ExtensionService = server.ExtensionService settingsHandler.AuthorizationService = authorizationService var stackHandler = stacks.NewHandler(requestBouncer) + stackHandler.DataStore = server.DataStore stackHandler.FileService = server.FileService - stackHandler.StackService = server.StackService - stackHandler.EndpointService = server.EndpointService - stackHandler.ResourceControlService = server.ResourceControlService stackHandler.SwarmStackManager = server.SwarmStackManager stackHandler.ComposeStackManager = server.ComposeStackManager stackHandler.GitService = server.GitService - stackHandler.RegistryService = server.RegistryService - stackHandler.DockerHubService = server.DockerHubService - stackHandler.SettingsService = server.SettingsService - stackHandler.UserService = server.UserService - stackHandler.ExtensionService = server.ExtensionService var tagHandler = tags.NewHandler(requestBouncer) - tagHandler.EdgeGroupService = server.EdgeGroupService - tagHandler.EdgeStackService = server.EdgeStackService - tagHandler.EndpointService = server.EndpointService - tagHandler.EndpointGroupService = server.EndpointGroupService - tagHandler.EndpointRelationService = server.EndpointRelationService - tagHandler.TagService = server.TagService + tagHandler.DataStore = server.DataStore var teamHandler = teams.NewHandler(requestBouncer) - teamHandler.TeamService = server.TeamService - teamHandler.TeamMembershipService = server.TeamMembershipService + teamHandler.DataStore = server.DataStore teamHandler.AuthorizationService = authorizationService var teamMembershipHandler = teammemberships.NewHandler(requestBouncer) - teamMembershipHandler.TeamMembershipService = server.TeamMembershipService + teamMembershipHandler.DataStore = server.DataStore teamMembershipHandler.AuthorizationService = authorizationService var statusHandler = status.NewHandler(requestBouncer, server.Status) @@ -280,28 +182,23 @@ func (server *Server) Start() error { var supportHandler = support.NewHandler(requestBouncer) var templatesHandler = templates.NewHandler(requestBouncer) - templatesHandler.SettingsService = server.SettingsService + templatesHandler.DataStore = server.DataStore var uploadHandler = upload.NewHandler(requestBouncer) uploadHandler.FileService = server.FileService var userHandler = users.NewHandler(requestBouncer, rateLimiter) - userHandler.UserService = server.UserService - userHandler.TeamService = server.TeamService - userHandler.TeamMembershipService = server.TeamMembershipService + userHandler.DataStore = server.DataStore userHandler.CryptoService = server.CryptoService - userHandler.ResourceControlService = server.ResourceControlService - userHandler.SettingsService = server.SettingsService userHandler.AuthorizationService = authorizationService var websocketHandler = websocket.NewHandler(requestBouncer) - websocketHandler.EndpointService = server.EndpointService + websocketHandler.DataStore = server.DataStore websocketHandler.SignatureService = server.SignatureService websocketHandler.ReverseTunnelService = server.ReverseTunnelService var webhookHandler = webhooks.NewHandler(requestBouncer) - webhookHandler.WebhookService = server.WebhookService - webhookHandler.EndpointService = server.EndpointService + webhookHandler.DataStore = server.DataStore webhookHandler.DockerClientFactory = server.DockerClientFactory server.Handler = &handler.Handler{ diff --git a/api/portainer.go b/api/portainer.go index a53c5355c..ae675174a 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1,6 +1,8 @@ package portainer -import "time" +import ( + "time" +) type ( // AccessPolicy represent a policy that can be associated to a user or team @@ -71,6 +73,27 @@ type ( Close() error IsNew() bool MigrateData() error + + DockerHub() DockerHubService + EdgeGroup() EdgeGroupService + EdgeStack() EdgeStackService + Endpoint() EndpointService + EndpointGroup() EndpointGroupService + EndpointRelation() EndpointRelationService + Extension() ExtensionService + Registry() RegistryService + ResourceControl() ResourceControlService + Role() RoleService + Schedule() ScheduleService + Settings() SettingsService + Stack() StackService + Tag() TagService + TeamMembership() TeamMembershipService + Team() TeamService + TunnelServer() TunnelServerService + User() UserService + Version() VersionService + Webhook() WebhookService } // DockerHub represents all the required information to connect and use the From c5f78f663a233ba1b30b55b4c5e06d296e23c8c9 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 21 May 2020 12:20:36 +1200 Subject: [PATCH 015/195] feat(settings): update templates documentation link --- app/portainer/views/settings/settings.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 0de6b5fea..3401411e5 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -50,7 +50,7 @@
You can specify the URL to your own template definitions file here. See - Portainer documentation for more details. + Portainer documentation for more details.
@@ -58,7 +58,7 @@ URL
- +
From 01d8c90348a1a1a980353c2483b871f3ab2d6db4 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 21 May 2020 11:30:37 +0300 Subject: [PATCH 016/195] fix(sidebar): show docker sidebar when needed (#3852) --- app/portainer/views/sidebar/sidebar.html | 1 + 1 file changed, 1 insertion(+) diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index d2f092095..a52d88684 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -14,6 +14,7 @@ Date: Mon, 25 May 2020 18:10:02 +1200 Subject: [PATCH 017/195] fix(bolt): migrate empty templates URL settings (#3856) --- api/bolt/migrator/migrate_dbversion23.go | 18 ++++++++++++++++++ api/bolt/migrator/migrator.go | 8 ++++++++ 2 files changed, 26 insertions(+) create mode 100644 api/bolt/migrator/migrate_dbversion23.go diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go new file mode 100644 index 000000000..2688eecd5 --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -0,0 +1,18 @@ +package migrator + +import portainer "github.com/portainer/portainer/api" + +func (m *Migrator) updateSettingsToDB24() error { + legacySettings, err := m.settingsService.Settings() + if err != nil { + return err + } + + if legacySettings.TemplatesURL == "" { + legacySettings.TemplatesURL = portainer.DefaultTemplatesURL + + return m.settingsService.UpdateSettings(legacySettings) + } + + return nil +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 0f72997fd..ed84b52e5 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -320,5 +320,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 2.0 + if m.currentDBVersion < 24 { + err := m.updateSettingsToDB24() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } From 38066ece33841e61669c3d1144a269ac64066ef1 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 3 Jun 2020 11:50:39 +1200 Subject: [PATCH 018/195] feat(project): re-introduce pull-dog --- pull-dog.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pull-dog.json diff --git a/pull-dog.json b/pull-dog.json new file mode 100644 index 000000000..fdaf375e0 --- /dev/null +++ b/pull-dog.json @@ -0,0 +1,4 @@ +{ + "dockerComposeYmlFilePaths": ["docker-compose.pull-dog.yml"], + "isLazy": true +} \ No newline at end of file From 766ced7cb1c0872de1e11ddf4770f823dff2fc6d Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 4 Jun 2020 08:01:31 +0300 Subject: [PATCH 019/195] chore(project): add angular components code snippets (#3649) * chore(project): add angular components code snippets * chore(project): add plopjs templates * feat(project): use class in controller template * chore(client): rename generators * chore(vscode): fix controller snippets * chore(git): ignore only specific files in .vscode * chore(plop): move generators to app * chore(plop): fix portainer module * fix(git): fix gitignore vscode * chore(vscode): remove symling to code-snippets * refactor(build): move plop templates to root * feat(build): add readme for plop --- .gitignore | 6 +- .vscode/portainer.code-snippets | 162 ++++++ package.json | 1 + plop-templates/README.md | 16 + plop-templates/component-controller.js.hbs | 6 + plop-templates/component.html.hbs | 1 + plop-templates/component.js.hbs | 6 + plopfile.js | 46 ++ yarn.lock | 593 +++++++++++++++++++-- 9 files changed, 804 insertions(+), 33 deletions(-) create mode 100644 .vscode/portainer.code-snippets create mode 100644 plop-templates/README.md create mode 100644 plop-templates/component-controller.js.hbs create mode 100644 plop-templates/component.html.hbs create mode 100644 plop-templates/component.js.hbs create mode 100644 plopfile.js diff --git a/.gitignore b/.gitignore index d9c64e8bc..d0ac052cc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,7 @@ dist portainer-checksum.txt api/cmd/portainer/portainer* .tmp -.vscode -.eslintcache \ No newline at end of file +**/.vscode/settings.json +**/.vscode/tasks.json + +.eslintcache diff --git a/.vscode/portainer.code-snippets b/.vscode/portainer.code-snippets new file mode 100644 index 000000000..6f622dc6b --- /dev/null +++ b/.vscode/portainer.code-snippets @@ -0,0 +1,162 @@ +{ + // Place your portainer workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Component": { + "scope": "javascript", + "prefix": "mycomponent", + "description": "Dummy Angularjs Component", + "body": [ + "import angular from 'angular';", + "import ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller from './${TM_FILENAME_BASE}Controller'", + "", + "angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').component('$TM_FILENAME_BASE', {", + " templateUrl: './$TM_FILENAME_BASE.html',", + " controller: ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/}Controller,", + "});", + "" + ] + }, + "Controller": { + "scope": "javascript", + "prefix": "mycontroller", + "body": [ + "class ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/} {", + "\t/* @ngInject */", + "\tconstructor($0) {", + "\t}", + "}", + "", + "export default ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/};" + ], + "description": "Dummy ES6+ controller" + }, + "Model": { + "scope": "javascript", + "prefix": "mymodel", + "description": "Dummy ES6+ model", + "body": [ + "/**", + " * $1 Model", + " */", + "const _$1 = Object.freeze({", + " $0", + "});", + "", + "export class $1 {", + " constructor() {", + " Object.assign(this, JSON.parse(JSON.stringify(_$1)));", + " }", + "}" + ] + }, + "Service": { + "scope": "javascript", + "prefix": "myservice", + "description": "Dummy ES6+ service", + "body": [ + "import angular from 'angular';", + "import PortainerError from 'Portainer/error';", + "", + "class $1 {", + " /* @ngInject */", + " constructor(\\$async, $0) {", + " this.\\$async = \\$async;", + "", + " this.getAsync = this.getAsync.bind(this);", + " this.getAllAsync = this.getAllAsync.bind(this);", + " this.createAsync = this.createAsync.bind(this);", + " this.updateAsync = this.updateAsync.bind(this);", + " this.deleteAsync = this.deleteAsync.bind(this);", + " }", + "", + " /**", + " * GET", + " */", + " async getAsync() {", + " try {", + "", + " } catch (err) {", + " throw new PortainerError('', err);", + " }", + " }", + "", + " async getAllAsync() {", + " try {", + "", + " } catch (err) {", + " throw new PortainerError('', err);", + " }", + " }", + "", + " get() {", + " if () {", + " return this.\\$async(this.getAsync);", + " }", + " return this.\\$async(this.getAllAsync);", + " }", + "", + " /**", + " * CREATE", + " */", + " async createAsync() {", + " try {", + "", + " } catch (err) {", + " throw new PortainerError('', err);", + " }", + " }", + "", + " create() {", + " return this.\\$async(this.createAsync);", + " }", + "", + " /**", + " * UPDATE", + " */", + " async updateAsync() {", + " try {", + "", + " } catch (err) {", + " throw new PortainerError('', err);", + " }", + " }", + "", + " update() {", + " return this.\\$async(this.updateAsync);", + " }", + "", + " /**", + " * DELETE", + " */", + " async deleteAsync() {", + " try {", + "", + " } catch (err) {", + " throw new PortainerError('', err);", + " }", + " }", + "", + " delete() {", + " return this.\\$async(this.deleteAsync);", + " }", + "}", + "", + "export default $1;", + "angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').service('$1', $1);" + ] + } +} diff --git a/package.json b/package.json index 53cb12546..3546c290a 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "lodash-webpack-plugin": "^0.11.5", "mini-css-extract-plugin": "^0.4.4", "ngtemplate-loader": "^2.0.1", + "plop": "^2.6.0", "postcss-loader": "^3.0.0", "prettier": "^2.0.2", "speed-measure-webpack-plugin": "^1.2.3", diff --git a/plop-templates/README.md b/plop-templates/README.md new file mode 100644 index 000000000..ca9d28216 --- /dev/null +++ b/plop-templates/README.md @@ -0,0 +1,16 @@ +# Plop generator + +We use [plop.js](https://plopjs.com/) to generate angular components in our app (in the future we might use it for other things). +in order to create a component with the name `exampleComponent`, go in your terminal to the folder in which you want to create the component (for example, if I want to create it in the portainer module components, I'll go to `./app/portainer/components`). then execute the following line: + +``` +yarn plop exampleComponent +``` + +this will create the following files and folders: + +``` +example-component/index.js - the component file +example-component/exampleComponent.html - the template file +example-component/exampleComponentController.js - the component controller file +``` diff --git a/plop-templates/component-controller.js.hbs b/plop-templates/component-controller.js.hbs new file mode 100644 index 000000000..29f87870b --- /dev/null +++ b/plop-templates/component-controller.js.hbs @@ -0,0 +1,6 @@ +class {{properCase name}}Controller { + /* @ngInject */ + constructor() {} +} + +export default {{properCase name}}Controller; \ No newline at end of file diff --git a/plop-templates/component.html.hbs b/plop-templates/component.html.hbs new file mode 100644 index 000000000..104a7cfb3 --- /dev/null +++ b/plop-templates/component.html.hbs @@ -0,0 +1 @@ +{{name}} \ No newline at end of file diff --git a/plop-templates/component.js.hbs b/plop-templates/component.js.hbs new file mode 100644 index 000000000..f4fb2adbd --- /dev/null +++ b/plop-templates/component.js.hbs @@ -0,0 +1,6 @@ +import {{properCase name}}Controller from './{{dashCase name}}/{{camelCase name}}Controller.js' + +angular.module('portainer.{{module}}').component('{{camelCase name}}', { + templateUrl: './{{camelCase name}}.html', + controller: {{properCase name}}Controller, +}); diff --git a/plopfile.js b/plopfile.js new file mode 100644 index 000000000..a5c23a347 --- /dev/null +++ b/plopfile.js @@ -0,0 +1,46 @@ +module.exports = function (plop) { + // use of INIT_CWD instead of process.cwd() because yarn changes the cwd + const cwd = process.env.INIT_CWD; + plop.addHelper('cwd', () => cwd); + plop.setGenerator('component', { + prompts: [ + { + type: 'input', + name: 'name', + message: 'component name please', + }, + { + type: 'input', + name: 'module', + message: 'module name please', + default: `${getCurrentPortainerModule(cwd)}`, + // when: false + }, + ], // array of inquirer prompts + actions: [ + { + type: 'add', + path: `{{cwd}}/{{dashCase name}}/index.js`, + templateFile: './plop-templates/component.js.hbs', + }, + { + type: 'add', + path: `{{cwd}}/{{dashCase name}}/{{camelCase name}}Controller.js`, + templateFile: './plop-templates/component-controller.js.hbs', + }, + { + type: 'add', + path: `{{cwd}}/{{dashCase name}}/{{camelCase name}}.html`, + templateFile: './plop-templates/component.html.hbs', + }, + ], // array of actions + }); +}; + +function getCurrentPortainerModule(cwd) { + const match = cwd.match(/\/app\/([^\/]*)(\/.*)?$/); + if (!match || !match.length || match[1] === 'portainer') { + return 'app'; + } + return match[1]; +} diff --git a/yarn.lock b/yarn.lock index 77bd0b6ed..4774787ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -755,6 +755,27 @@ resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9" integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg== +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -786,6 +807,28 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/globby@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@types/globby/-/globby-9.1.0.tgz#08e2cf99c64f8e45c6cfbe05e9d8ac763aca6482" + integrity sha512-9du/HCA71EBz7syHRnM4Q/u4Fbx3SyN/Uu+4Of9lyPX4A6Xi+A8VMxvx8j5/CMTfrae2Zwdwg0fAaKvKXfRbAw== + dependencies: + globby "*" + +"@types/handlebars@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.1.0.tgz#3fcce9bf88f85fe73dc932240ab3fb682c624850" + integrity sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA== + dependencies: + handlebars "*" + +"@types/inquirer@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.0.1.tgz#ea9b483a81f16f4f0d27d5c8d9d081dfa36c4ee9" + integrity sha512-O9rEHE9iBvYaFAGS0fAlDzqY/3CsOrRKzni4zwnAEce2JrHUEbXAce2Pwwe8ZGzmQkucwSXn1tSiKig37INgfA== + dependencies: + "@types/through" "*" + rxjs ">=6.4.0" + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -806,6 +849,13 @@ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@uirouter/angularjs@1.0.11": version "1.0.11" resolved "https://registry.yarnpkg.com/@uirouter/angularjs/-/angularjs-1.0.11.tgz#ced1ec8bea68a5db7dcfd77a43b7b8b9a2953540" @@ -1040,6 +1090,14 @@ active-x-obfuscator@0.0.1: dependencies: zeparser "0.0.5" +aggregate-error@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" + integrity sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv-errors@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" @@ -1226,6 +1284,13 @@ ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + dependencies: + type-fest "^0.11.0" + ansi-gray@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" @@ -1253,6 +1318,11 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -1398,6 +1468,11 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + array-uniq@^1.0.1, array-uniq@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -2140,7 +2215,7 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camel-case@3.0.x: +camel-case@3.0.x, camel-case@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= @@ -2237,6 +2312,30 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +change-case@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.1.0.tgz#0e611b7edc9952df2e8513b27b42de72647dd17e" + integrity sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw== + dependencies: + camel-case "^3.0.0" + constant-case "^2.0.0" + dot-case "^2.1.0" + header-case "^1.0.0" + is-lower-case "^1.1.0" + is-upper-case "^1.1.0" + lower-case "^1.1.1" + lower-case-first "^1.0.0" + no-case "^2.3.2" + param-case "^2.1.0" + pascal-case "^2.0.0" + path-case "^2.1.0" + sentence-case "^2.1.0" + snake-case "^2.1.0" + swap-case "^1.1.0" + title-case "^2.1.0" + upper-case "^1.1.1" + upper-case-first "^1.1.0" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -2358,6 +2457,11 @@ clean-css@~3.0.4: commander "2.5.x" source-map ">=0.1.43 <0.2" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + clean-terminal-webpack-plugin@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/clean-terminal-webpack-plugin/-/clean-terminal-webpack-plugin-1.1.0.tgz#5c345af8ae52c276d7a8076382e79df8850896a8" @@ -2384,6 +2488,18 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" + integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== + cli-truncate@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" @@ -2546,12 +2662,12 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: +color-name@1.1.3, color-name@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -2623,7 +2739,7 @@ commander@2.5.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.5.1.tgz#23c61f6e47be143cc02e7ad4bb1c47f5cd5a2883" integrity sha1-I8Yfbke+FDzALnrUuxxH9c1aKIM= -commander@^2.20.0: +commander@^2.20.0, commander@~2.20.3: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -2801,6 +2917,14 @@ console-stream@^0.1.1: resolved "https://registry.yarnpkg.com/console-stream/-/console-stream-0.1.1.tgz#a095fe07b20465955f2fafd28b5d72bccd949d44" integrity sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ= +constant-case@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-2.0.0.tgz#4175764d389d3fa9c8ecd29186ed6005243b6a46" + integrity sha1-QXV2TTidP6nI7NKRhu1gBSQ7akY= + dependencies: + snake-case "^2.1.0" + upper-case "^1.1.1" + constants-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" @@ -2888,6 +3012,11 @@ core-js@^2.6.5: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== +core-js@^3.3.2: + version "3.6.4" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" + integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== + core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -3377,7 +3506,7 @@ default-gateway@^4.2.0: execa "^1.0.0" ip-regex "^2.1.0" -defaults@^1.0.0: +defaults@^1.0.0, defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= @@ -3431,6 +3560,20 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" +del@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7" + integrity sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA== + dependencies: + globby "^10.0.1" + graceful-fs "^4.2.2" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.1" + p-map "^3.0.0" + rimraf "^3.0.0" + slash "^3.0.0" + delayed-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-0.0.5.tgz#d4b1f43a93e8296dfe02694f4680bc37a313c73f" @@ -3488,6 +3631,13 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -3590,6 +3740,13 @@ domutils@^1.5.1, domutils@^1.7.0: dom-serializer "0" domelementtype "1" +dot-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-2.1.1.tgz#34dcf37f50a8e93c2b3bca8bb7fb9155c7da3bee" + integrity sha1-NNzzf1Co6TwrO8qLt/uRVcfaO+4= + dependencies: + no-case "^2.2.0" + download@^6.2.2: version "6.2.5" resolved "https://registry.yarnpkg.com/download/-/download-6.2.5.tgz#acd6a542e4cd0bb42ca70cfc98c9e43b07039714" @@ -3690,6 +3847,11 @@ emoji-regex@^7.0.1: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -4444,6 +4606,18 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== +fast-glob@^3.0.3, fast-glob@^3.1.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" + integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -4459,6 +4633,13 @@ fastparse@^1.1.1, fastparse@^1.1.2: resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== +fastq@^1.6.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.1.tgz#4570c74f2ded173e71cf0beb08ac70bb85826791" + integrity sha512-mpIH5sKYueh3YyeJwqtVo8sORi0CgtmkVbK6kZStpQlZBYQuTzG2CZ7idSiJuA7bY0SFCWUc5WIs+oYumGCQNw== + dependencies: + reusify "^1.0.4" + fastqueue@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/fastqueue/-/fastqueue-0.1.0.tgz#6c691016b3806186476eeb05a717babb8f633c19" @@ -4505,6 +4686,13 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" @@ -5013,6 +5201,13 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-parent@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob-stream@^3.1.5: version "3.1.18" resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-3.1.18.tgz#9170a5f12b790306fdfe598f313f8f7954fd143b" @@ -5124,6 +5319,32 @@ globals@^9.14.0: resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== +globby@*: + version "11.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.0.tgz#56fd0e9f0d4f8fb0c456f1ab0dee96e1380bc154" + integrity sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +globby@^10.0.1: + version "10.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" + integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== + dependencies: + "@types/glob" "^7.1.1" + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.0.3" + glob "^7.1.3" + ignore "^5.1.1" + merge2 "^1.2.3" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -5201,7 +5422,7 @@ graceful-fs@^3.0.0, graceful-fs@^3.0.2, graceful-fs@~3.0.2: dependencies: natives "^1.1.3" -graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: +graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2: version "4.2.3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== @@ -5575,6 +5796,17 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== +handlebars@*, handlebars@^4.4.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.3.tgz#8ece2797826886cf8082d1726ff21d2a022550ee" + integrity sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + handlebars@1.0.x: version "1.0.12" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-1.0.12.tgz#18c6d3440c35e91b19b3ff582b9151ab4985d4fc" @@ -5698,6 +5930,14 @@ he@1.2.x: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +header-case@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/header-case/-/header-case-1.0.1.tgz#9535973197c144b09613cd65d317ef19963bd02d" + integrity sha1-lTWXMZfBRLCWE81l0xfvGZY70C0= + dependencies: + no-case "^2.2.0" + upper-case "^1.1.3" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -5987,6 +6227,11 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.1, ignore@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" + integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + image-webpack-loader@^4.5.0: version "4.6.0" resolved "https://registry.yarnpkg.com/image-webpack-loader/-/image-webpack-loader-4.6.0.tgz#c38eb723c8a50cca46298654a74d5a4f26f9a501" @@ -6128,6 +6373,11 @@ indent-string@^3.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -6209,6 +6459,25 @@ inquirer@^6.2.2: strip-ansi "^5.1.0" through "^2.3.6" +inquirer@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" + integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^3.0.0" + cli-cursor "^3.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.5.3" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -6217,7 +6486,7 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -interpret@1.2.0, interpret@^1.0.0: +interpret@1.2.0, interpret@^1.0.0, interpret@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== @@ -6414,6 +6683,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-gif@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-gif/-/is-gif-3.0.0.tgz#c4be60b26a301d695bb833b20d9b5d66c6cf83b1" @@ -6428,7 +6702,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0: +is-glob@^4.0.0, is-glob@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -6440,6 +6714,13 @@ is-jpg@^2.0.0: resolved "https://registry.yarnpkg.com/is-jpg/-/is-jpg-2.0.0.tgz#2e1997fa6e9166eaac0242daae443403e4ef1d97" integrity sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc= +is-lower-case@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393" + integrity sha1-fhR75HaNxGbbO/shzGCzHmrWk5M= + dependencies: + lower-case "^1.1.0" + is-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" @@ -6500,7 +6781,7 @@ is-observable@^1.1.0: dependencies: symbol-observable "^1.1.0" -is-path-cwd@^2.0.0: +is-path-cwd@^2.0.0, is-path-cwd@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== @@ -6519,6 +6800,11 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" +is-path-inside@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== + is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -6623,6 +6909,13 @@ is-unc-path@^1.0.0: dependencies: unc-path-regex "^0.1.2" +is-upper-case@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-1.1.2.tgz#8d0b1fa7e7933a1e58483600ec7d9661cbaf756f" + integrity sha1-jQsfp+eTOh5YSDYA7H2WYcuvdW8= + dependencies: + upper-case "^1.1.0" + is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -6663,6 +6956,11 @@ isarray@^2.0.5: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isbinaryfile@^4.0.2: + version "4.0.5" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.5.tgz#7193454fdd7fc0b12855c36c48d4ac7368fa3ec9" + integrity sha512-Jvz0gpTh1AILHMCBUyqq7xv1ZOQrxTDwyp1/QUq1xFpOBvp4AH5uEobPePJht8KnBGqQIH7We6OR73mXsjG0cA== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -6949,7 +7247,7 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -liftoff@^2.1.0, liftoff@~2.5.0: +liftoff@^2.1.0, liftoff@^2.5.0, liftoff@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.5.0.tgz#2009291bb31cea861bbf10a7c15a28caf75c31ec" integrity sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew= @@ -7220,6 +7518,11 @@ lodash.escape@^3.0.0: dependencies: lodash._root "^3.0.0" +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -7335,6 +7638,13 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" +log-symbols@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== + dependencies: + chalk "^2.0.1" + log-symbols@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" @@ -7392,7 +7702,14 @@ loud-rejection@^1.0.0: currently-unhandled "^0.4.1" signal-exit "^3.0.0" -lower-case@^1.1.1: +lower-case-first@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lower-case-first/-/lower-case-first-1.0.2.tgz#e5da7c26f29a7073be02d52bac9980e5922adfa1" + integrity sha1-5dp8JvKacHO+AtUrrJmA5ZIq36E= + dependencies: + lower-case "^1.1.2" + +lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= @@ -7583,6 +7900,11 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.2.3, merge2@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" + integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== + method-override@~2.3.5: version "2.3.10" resolved "https://registry.yarnpkg.com/method-override/-/method-override-2.3.10.tgz#e3daf8d5dee10dd2dce7d4ae88d62bbee77476b4" @@ -7638,11 +7960,23 @@ mime-db@1.43.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + mime-types@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-1.0.2.tgz#995ae1392ab8affcbfcb2641dd054e943c0d5dce" integrity sha1-mVrhOSq4r/y/yyZB3QVOlDwNXc4= +mime-types@~2.1.15: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.6, mime-types@~2.1.9: version "2.1.26" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" @@ -7898,6 +8232,11 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -7950,7 +8289,7 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.5.0, neo-async@^2.6.1: +neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== @@ -7983,7 +8322,7 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -no-case@^2.2.0: +no-case@^2.2.0, no-case@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== @@ -8034,6 +8373,25 @@ node-notifier@5.2.1: shellwords "^0.1.1" which "^1.3.0" +node-plop@~0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/node-plop/-/node-plop-0.25.0.tgz#1d3bdf286bf74baabb6755b4cef8c6ab37110180" + integrity sha512-OFvnTsDw9nxNdLrYcveJhU2Hnzg+AxOz6xBk8uXsi0vCOSP7Rng98pdgfsuZKyCN+qrc+/fSwlNC5hkXhJ6gww== + dependencies: + "@types/globby" "^9.1.0" + "@types/handlebars" "^4.1.0" + "@types/inquirer" "6.0.1" + change-case "^3.1.0" + core-js "^3.3.2" + del "^5.1.0" + globby "^10.0.1" + handlebars "^4.4.3" + inquirer "^7.0.0" + isbinaryfile "^4.0.2" + lodash.get "^4.4.2" + mkdirp "^0.5.1" + resolve "^1.12.0" + node-releases@^1.1.52: version "1.1.52" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9" @@ -8333,7 +8691,7 @@ optimist@0.3.5: dependencies: wordwrap "~0.0.2" -optimist@0.6.x: +optimist@0.6.x, optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= @@ -8374,6 +8732,18 @@ optipng-bin@^5.0.0: bin-wrapper "^4.0.0" logalot "^2.0.0" +ora@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" + integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg== + dependencies: + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-spinners "^2.0.0" + log-symbols "^2.2.0" + strip-ansi "^5.2.0" + wcwidth "^1.0.1" + orchestrator@^0.3.0: version "0.3.8" resolved "https://registry.yarnpkg.com/orchestrator/-/orchestrator-0.3.8.tgz#14e7e9e2764f7315fbac184e506c7aa6df94ad7e" @@ -8530,6 +8900,13 @@ p-map@^2.0.0: resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== +p-map@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" + integrity sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ== + dependencies: + aggregate-error "^3.0.0" + p-pipe@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" @@ -8590,7 +8967,7 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" -param-case@2.1.x: +param-case@2.1.x, param-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= @@ -8665,6 +9042,14 @@ parseurl@~1.3.0, parseurl@~1.3.1, parseurl@~1.3.2, parseurl@~1.3.3: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +pascal-case@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-2.0.1.tgz#2d578d3455f660da65eca18ef95b4e0de912761e" + integrity sha1-LVeNNFX2YNpl7KGO+VtODekSdh4= + dependencies: + camel-case "^3.0.0" + upper-case-first "^1.1.0" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" @@ -8675,6 +9060,13 @@ path-browserify@0.0.1: resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== +path-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/path-case/-/path-case-2.1.1.tgz#94b8037c372d3fe2906e465bb45e25d226e8eea5" + integrity sha1-lLgDfDctP+KQbkZbtF4l0ibo7qU= + dependencies: + no-case "^2.2.0" + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -8727,7 +9119,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.5, path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== @@ -8810,7 +9202,7 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -picomatch@^2.0.5: +picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -8896,6 +9288,19 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +plop@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/plop/-/plop-2.6.0.tgz#b0c3a9879c35008a22bb681bfba975ed2bc3c38c" + integrity sha512-faK3oVbWL7DYdC5ZjM+lJPrmXAPirN28QRFykPWwg6i7dv2T373JyxKlcO+XGMQcPlEYDFyKUXIuiY3Db5Ktkw== + dependencies: + chalk "^1.1.3" + interpret "^1.2.0" + liftoff "^2.5.0" + minimist "^1.2.0" + node-plop "~0.25.0" + ora "^3.4.0" + v8flags "^2.0.10" + pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" @@ -9890,13 +10295,25 @@ resolve@0.5.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.5.1.tgz#15e4a222c4236bcd4cf85454412c2d0fb6524576" integrity sha1-FeSiIsQja81M+FRUQSwtD7ZSRXY= -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.3.2: +resolve@^1.1.6, resolve@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1: version "1.15.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== dependencies: path-parse "^1.0.6" +resolve@^1.3.2: + version "1.8.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" + integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== + dependencies: + path-parse "^1.0.5" + response-time@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/response-time/-/response-time-2.3.2.tgz#ffa71bab952d62f7c1d49b7434355fbc68dffc5a" @@ -9928,6 +10345,14 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -9938,6 +10363,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -9959,6 +10389,13 @@ rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: dependencies: glob "^7.1.3" +rimraf@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + rimraf@~2.1: version "2.1.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.1.4.tgz#5a6eb62eeda068f51ede50f29b3e5cd22f3d9bb2" @@ -9991,13 +10428,18 @@ run-async@^0.1.0: dependencies: once "^1.3.0" -run-async@^2.2.0: +run-async@^2.2.0, run-async@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8" integrity sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg== dependencies: is-promise "^2.1.0" +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -10010,13 +10452,20 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= -rxjs@^6.3.3, rxjs@^6.4.0: +rxjs@>=6.4.0, rxjs@^6.3.3, rxjs@^6.5.3: version "6.5.4" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== dependencies: tslib "^1.9.0" +rxjs@^6.4.0: + version "6.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" + integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -10162,6 +10611,14 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" +sentence-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-2.1.1.tgz#1f6e2dda39c168bf92d13f86d4a918933f667ed4" + integrity sha1-H24t2jnBaL+S0T+G1KkYkz9mftQ= + dependencies: + no-case "^2.2.0" + upper-case-first "^1.1.2" + sequencify@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" @@ -10350,6 +10807,13 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +snake-case@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" + integrity sha1-Qb2xtz8w7GagTU4srRt2OH1NbZ8= + dependencies: + no-case "^2.2.0" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -10724,6 +11188,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string.prototype.trimleft@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" @@ -10740,7 +11213,14 @@ string.prototype.trimright@^2.1.1: define-properties "^1.1.3" function-bind "^1.1.1" -string_decoder@^1.0.0, string_decoder@^1.1.1: +string_decoder@^1.0.0, string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -10752,13 +11232,6 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -10794,6 +11267,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-1.0.0.tgz#85b8862f3844b5a6d5ec8467a93598173a36f794" @@ -10928,6 +11408,14 @@ svgo@^1.3.2: unquote "~1.1.1" util.promisify "~1.0.0" +swap-case@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/swap-case/-/swap-case-1.1.2.tgz#c39203a4587385fad3c850a0bd1bcafa081974e3" + integrity sha1-w5IDpFhzhfrTyFCgvRvK+ggZdOM= + dependencies: + lower-case "^1.1.1" + upper-case "^1.1.1" + symbol-observable@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -11090,6 +11578,14 @@ tinycolor@0.x: resolved "https://registry.yarnpkg.com/tinycolor/-/tinycolor-0.0.1.tgz#320b5a52d83abb5978d81a3e887d4aefb15a6164" integrity sha1-MgtaUtg6u1l42Bo+iH1K77FaYWQ= +title-case@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" + integrity sha1-PhJyFtpY0rxb7PE3q5Ha46fNj6o= + dependencies: + no-case "^2.2.0" + upper-case "^1.0.3" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -11215,7 +11711,20 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-is@~1.6.10, type-is@~1.6.17, type-is@~1.6.18, type-is@~1.6.6: +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + +type-is@~1.6.10, type-is@~1.6.6: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + integrity sha1-yrEPtJCeRByChC6v4a1kbIGARBA= + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -11261,6 +11770,14 @@ uglify-js@^2.4.24: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.1.4: + version "3.8.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.8.0.tgz#f3541ae97b2f048d7e7e3aa4f39fd8a1f5d7a805" + integrity sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ== + dependencies: + commander "~2.20.3" + source-map "~0.6.1" + uglify-js@~2.3: version "2.3.6" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.3.6.tgz#fa0984770b428b7a9b2a8058f46355d14fef211a" @@ -11417,7 +11934,14 @@ upath@^1.1.1: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -upper-case@^1.1.1: +upper-case-first@^1.1.0, upper-case-first@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-1.1.2.tgz#5d79bedcff14419518fd2edb0a0507c9b6859115" + integrity sha1-XXm+3P8UQZUY/S7bCgUHybaFkRU= + dependencies: + upper-case "^1.1.1" + +upper-case@^1.0.3, upper-case@^1.1.0, upper-case@^1.1.1, upper-case@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" integrity sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg= @@ -11574,7 +12098,7 @@ v8-compile-cache@2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== -v8flags@^2.0.2: +v8flags@^2.0.10, v8flags@^2.0.2: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" integrity sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ= @@ -11701,6 +12225,13 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + dependencies: + defaults "^1.0.3" + webpack-build-notifier@^0.1.30: version "0.1.32" resolved "https://registry.yarnpkg.com/webpack-build-notifier/-/webpack-build-notifier-0.1.32.tgz#965e63334147e18716661357b08409958563899e" From 9f4631bb6d02f35313aeeb4e57d60e05f28a81d9 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 4 Jun 2020 08:35:09 +0300 Subject: [PATCH 020/195] feat(edge-compute): add specific edge endpoint checkin interval (#3855) * feat(endpoint): send custom checkin interval * feat(endpoint): update edge checkin interval * feat(endpoint): save checkin interval * feat(endpoints): create endpoint with checkin interval * feat(endpoints): change tooltip * fix(edge-compute): fix typos Co-authored-by: Anthony Lapenna * fix(endpoints): show default interval * fix(endpoint): rename checkin property Co-authored-by: Anthony Lapenna --- api/http/handler/endpoints/endpoint_create.go | 19 +++++--- .../endpoints/endpoint_status_inspect.go | 7 ++- api/http/handler/endpoints/endpoint_update.go | 5 +++ api/portainer.go | 32 +++++++------- app/portainer/services/api/endpointService.js | 32 +++++++++++++- app/portainer/services/fileUpload.js | 8 ++-- .../create/createEndpointController.js | 44 +++++++++++++++++-- .../endpoints/create/createendpoint.html | 21 ++++++++- .../views/endpoints/edit/endpoint.html | 21 ++++++++- .../endpoints/edit/endpointController.js | 26 ++++++++++- app/portainer/views/settings/settings.html | 7 ++- 11 files changed, 183 insertions(+), 39 deletions(-) diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index e53d7f96d..d8a1bb898 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -30,6 +30,7 @@ type endpointCreatePayload struct { TLSCertFile []byte TLSKeyFile []byte TagIDs []portainer.TagID + EdgeCheckinInterval int } func (payload *endpointCreatePayload) Validate(r *http.Request) error { @@ -102,6 +103,9 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true) payload.PublicURL = publicURL + checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true) + payload.EdgeCheckinInterval = checkinInterval + return nil } @@ -193,13 +197,14 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) TLSConfig: portainer.TLSConfiguration{ TLS: false, }, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - TagIDs: payload.TagIDs, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, - EdgeKey: edgeKey, + AuthorizedUsers: []portainer.UserID{}, + AuthorizedTeams: []portainer.TeamID{}, + Extensions: []portainer.EndpointExtension{}, + TagIDs: payload.TagIDs, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, + EdgeKey: edgeKey, + EdgeCheckinInterval: payload.EdgeCheckinInterval, } err = handler.saveEndpointAndUpdateAuthorizations(endpoint) diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index b8b1c4d63..cb64bbfe8 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -60,11 +60,16 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID) + checkinInterval := settings.EdgeAgentCheckinInterval + if endpoint.EdgeCheckinInterval != 0 { + checkinInterval = endpoint.EdgeCheckinInterval + } + statusResponse := endpointStatusInspectResponse{ Status: tunnel.Status, Port: tunnel.Port, Schedules: tunnel.Schedules, - CheckinInterval: settings.EdgeAgentCheckinInterval, + CheckinInterval: checkinInterval, Credentials: tunnel.Credentials, } diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 6ba76c992..4752086c3 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -23,6 +23,7 @@ type endpointUpdatePayload struct { TagIDs []portainer.TagID UserAccessPolicies portainer.UserAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies + EdgeCheckinInterval *int } func (payload *endpointUpdatePayload) Validate(r *http.Request) error { @@ -61,6 +62,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * endpoint.PublicURL = *payload.PublicURL } + if payload.EdgeCheckinInterval != nil { + endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval + } + groupIDChanged := false if payload.GroupID != nil { groupID := portainer.EndpointGroupID(*payload.GroupID) diff --git a/api/portainer.go b/api/portainer.go index ae675174a..8386ce8f2 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -155,21 +155,23 @@ type ( // Endpoint represents a Docker endpoint with all the info required // to connect to it Endpoint struct { - ID EndpointID `json:"Id"` - Name string `json:"Name"` - Type EndpointType `json:"Type"` - URL string `json:"URL"` - GroupID EndpointGroupID `json:"GroupId"` - PublicURL string `json:"PublicURL"` - TLSConfig TLSConfiguration `json:"TLSConfig"` - Extensions []EndpointExtension `json:"Extensions"` - TagIDs []TagID `json:"TagIds"` - Status EndpointStatus `json:"Status"` - Snapshots []Snapshot `json:"Snapshots"` - UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` - TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` - EdgeID string `json:"EdgeID,omitempty"` - EdgeKey string `json:"EdgeKey"` + ID EndpointID `json:"Id"` + Name string `json:"Name"` + Type EndpointType `json:"Type"` + URL string `json:"URL"` + GroupID EndpointGroupID `json:"GroupId"` + PublicURL string `json:"PublicURL"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + Extensions []EndpointExtension `json:"Extensions"` + TagIDs []TagID `json:"TagIds"` + Status EndpointStatus `json:"Status"` + Snapshots []Snapshot `json:"Snapshots"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` + EdgeID string `json:"EdgeID,omitempty"` + EdgeKey string `json:"EdgeKey"` + EdgeCheckinInterval int `json:"EdgeCheckinInterval"` + // Deprecated fields // Deprecated in DBVersion == 4 TLS bool `json:"TLS,omitempty"` diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index fd9b327b0..8d7cddd3f 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -65,7 +65,21 @@ angular.module('portainer.app').factory('EndpointService', [ return deferred.promise; }; - service.createRemoteEndpoint = function (name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + service.createRemoteEndpoint = function ( + name, + type, + URL, + PublicURL, + groupID, + tagIds, + TLS, + TLSSkipVerify, + TLSSkipClientVerify, + TLSCAFile, + TLSCertFile, + TLSKeyFile, + checkinInterval + ) { var deferred = $q.defer(); var endpointURL = URL; @@ -73,7 +87,21 @@ angular.module('portainer.app').factory('EndpointService', [ endpointURL = 'tcp://' + URL; } - FileUploadService.createEndpoint(name, type, endpointURL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + FileUploadService.createEndpoint( + name, + type, + endpointURL, + PublicURL, + groupID, + tagIds, + TLS, + TLSSkipVerify, + TLSSkipClientVerify, + TLSCAFile, + TLSCertFile, + TLSKeyFile, + checkinInterval + ) .then(function success(response) { deferred.resolve(response.data); }) diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 4f024d814..6477d75ba 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -91,12 +91,11 @@ angular.module('portainer.app').factory('FileUploadService', [ data: { file: file, Name: stackName, - EdgeGroups: Upload.json(edgeGroups) + EdgeGroups: Upload.json(edgeGroups), }, - ignoreLoadingBar: true + ignoreLoadingBar: true, }); }; - service.configureRegistry = function (registryId, registryManagementConfigurationModel) { return Upload.upload({ @@ -116,7 +115,7 @@ angular.module('portainer.app').factory('FileUploadService', [ }); }; - service.createEndpoint = function (name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + service.createEndpoint = function (name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, checkinInterval) { return Upload.upload({ url: 'api/endpoints', data: { @@ -132,6 +131,7 @@ angular.module('portainer.app').factory('FileUploadService', [ TLSCACertFile: TLSCAFile, TLSCertFile: TLSCertFile, TLSKeyFile: TLSKeyFile, + CheckinInterval: checkinInterval, }, ignoreLoadingBar: true, }); diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index a1c6b6003..bea559dd7 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -12,6 +12,7 @@ angular EndpointService, GroupService, TagService, + SettingsService, Notifications, Authentication ) { @@ -19,6 +20,24 @@ angular EnvironmentType: 'agent', actionInProgress: false, allowCreateTag: Authentication.isAdmin(), + availableEdgeAgentCheckinOptions: [ + { key: 'Use default interval', value: 0 }, + { + key: '5 seconds', + value: 5, + }, + { + key: '10 seconds', + value: 10, + }, + { + key: '30 seconds', + value: 30, + }, + { key: '5 minutes', value: 300 }, + { key: '1 hour', value: 3600 }, + { key: '1 day', value: 86400 }, + ], }; $scope.formValues = { @@ -28,6 +47,7 @@ angular GroupId: 1, SecurityFormData: new EndpointSecurityFormData(), TagIds: [], + CheckinInterval: $scope.state.availableEdgeAgentCheckinOptions[0].value, }; $scope.copyAgentCommand = function () { @@ -79,7 +99,7 @@ angular var tagIds = $scope.formValues.TagIds; var URL = $scope.formValues.URL; - addEndpoint(name, 4, URL, '', groupId, tagIds, false, false, false, null, null, null); + addEndpoint(name, 4, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); }; $scope.onCreateTag = function onCreateTag(tagName) { @@ -96,9 +116,23 @@ angular } } - function addEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { + function addEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) { $scope.state.actionInProgress = true; - EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) + EndpointService.createRemoteEndpoint( + name, + type, + URL, + PublicURL, + groupId, + tagIds, + TLS, + TLSSkipVerify, + TLSSkipClientVerify, + TLSCAFile, + TLSCertFile, + TLSKeyFile, + CheckinInterval + ) .then(function success(data) { Notifications.success('Endpoint created', name); if (type === 4) { @@ -119,10 +153,14 @@ angular $q.all({ groups: GroupService.groups(), tags: TagService.tags(), + settings: SettingsService.settings(), }) .then(function success(data) { $scope.groups = data.groups; $scope.availableTags = data.tags; + + const settings = data.settings; + $scope.state.availableEdgeAgentCheckinOptions[0].key += ` (${settings.EdgeAgentCheckinInterval} seconds)`; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to load groups'); diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 4a366a22e..4ab68d840 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -145,8 +145,8 @@
-
+
+ +
+ +
+ +
+
-
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 6223629fd..ca4ca5ca7 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -36,10 +36,10 @@
- {{dockerCommands.standalone}} + {{ dockerCommands.standalone }} - {{dockerCommands.swarm}} + {{ dockerCommands.swarm }}
@@ -118,6 +118,23 @@
+
+ +
+ +
+
Metadata diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 221c9282e..816a65734 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -17,13 +17,32 @@ angular TagService, EndpointProvider, Notifications, - Authentication + Authentication, + SettingsService ) { $scope.state = { uploadInProgress: false, actionInProgress: false, deploymentTab: 0, allowCreate: Authentication.isAdmin(), + availableEdgeAgentCheckinOptions: [ + { key: 'Use default interval', value: 0 }, + { + key: '5 seconds', + value: 5, + }, + { + key: '10 seconds', + value: 10, + }, + { + key: '30 seconds', + value: 30, + }, + { key: '5 minutes', value: 300 }, + { key: '1 hour', value: 3600 }, + { key: '1 day', value: 86400 }, + ], }; $scope.formValues = { @@ -83,6 +102,7 @@ angular PublicURL: endpoint.PublicURL, GroupID: endpoint.GroupId, TagIds: endpoint.TagIds, + EdgeCheckinInterval: endpoint.EdgeCheckinInterval, TLS: TLS, TLSSkipVerify: TLSSkipVerify, TLSSkipClientVerify: TLSSkipClientVerify, @@ -133,6 +153,7 @@ angular endpoint: EndpointService.endpoint($transition$.params().id), groups: GroupService.groups(), tags: TagService.tags(), + settings: SettingsService.settings(), }) .then(function success(data) { var endpoint = data.endpoint; @@ -149,6 +170,9 @@ angular standalone: buildStandaloneCommand($scope.randomEdgeID, endpoint.EdgeKey), swarm: buildSwarmCommand($scope.randomEdgeID, endpoint.EdgeKey), }; + + const settings = data.settings; + $scope.state.availableEdgeAgentCheckinOptions[0].key += ` (${settings.EdgeAgentCheckinInterval} seconds)`; } $scope.endpoint = endpoint; $scope.groups = data.groups; diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 3401411e5..d6cb80db0 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -118,8 +118,11 @@
- - - - + +
+
+ +
+ +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+
diff --git a/app/portainer/views/users/edit/userController.js b/app/portainer/views/users/edit/userController.js index ec227fe44..93b3e6d9d 100644 --- a/app/portainer/views/users/edit/userController.js +++ b/app/portainer/views/users/edit/userController.js @@ -14,6 +14,7 @@ angular.module('portainer.app').controller('UserController', [ }; $scope.formValues = { + username: '', newPassword: '', confirmPassword: '', Administrator: false, @@ -28,12 +29,33 @@ angular.module('portainer.app').controller('UserController', [ }); }; - $scope.updatePermissions = function () { - var role = $scope.formValues.Administrator ? 1 : 2; - UserService.updateUser($scope.user.Id, undefined, role, 0) + $scope.updateUser = async function () { + const role = $scope.formValues.Administrator ? 1 : 2; + const oldUsername = $scope.user.Username; + const username = $scope.formValues.username; + let promise = Promise.resolve(true); + if (username != oldUsername) { + promise = new Promise((resolve) => + ModalService.confirm({ + title: 'Are you sure?', + message: `Are you sure you want to rename the user ${oldUsername} to ${username}?`, + buttons: { + confirm: { + label: 'Update', + className: 'btn-primary', + }, + }, + callback: resolve, + }) + ); + } + const confirmed = await promise; + if (!confirmed) { + return; + } + UserService.updateUser($scope.user.Id, { role, username }) .then(function success() { - var newRole = role === 1 ? 'administrator' : 'user'; - Notifications.success('Permissions successfully updated', $scope.user.Username + ' is now ' + newRole); + Notifications.success('User successfully updated'); $state.reload(); }) .catch(function error(err) { @@ -42,7 +64,7 @@ angular.module('portainer.app').controller('UserController', [ }; $scope.updatePassword = function () { - UserService.updateUser($scope.user.Id, $scope.formValues.newPassword, undefined, -1) + UserService.updateUser($scope.user.Id, { password: $scope.formValues.newPassword }) .then(function success() { Notifications.success('Password successfully updated'); $state.reload(); @@ -63,6 +85,12 @@ angular.module('portainer.app').controller('UserController', [ }); } + $scope.isSubmitEnabled = isSubmitEnabled; + function isSubmitEnabled() { + const { user, formValues } = $scope; + return user && (user.Username !== formValues.username || (formValues.Administrator && user.Role !== 1) || (!formValues.Administrator && user.Role === 1)); + } + function initView() { $scope.isAdmin = Authentication.isAdmin(); @@ -74,6 +102,7 @@ angular.module('portainer.app').controller('UserController', [ var user = data.user; $scope.user = user; $scope.formValues.Administrator = user.Role === 1; + $scope.formValues.username = user.Username; $scope.AuthenticationMethod = data.settings.AuthenticationMethod; }) .catch(function error(err) { From b58c2facfee6659d6a30834d59d8ff27eed5d809 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 9 Jun 2020 05:43:32 +0300 Subject: [PATCH 026/195] revert(azure): revert removal (#3890) * Revert "fix(sidebar): show docker sidebar when needed (#3852)" This reverts commit 59da17dde452c014bc8260e15ff4618eec94f6d0. * Revert "refactor(azure): remove Azure ACI endpoint support (#3803)" This reverts commit 493de205406c75fea88ed0a1c5e96e544063d682. --- api/cron/job_snapshot.go | 2 +- api/docker/client.go | 4 +- api/errors.go | 5 + api/go.sum | 1 - api/http/client/client.go | 52 ++++++ api/http/handler/endpointproxy/handler.go | 2 + api/http/handler/endpointproxy/proxy_azure.go | 43 +++++ api/http/handler/endpoints/endpoint_create.go | 108 ++++++++--- .../handler/endpoints/endpoint_snapshot.go | 4 + .../handler/endpoints/endpoint_snapshots.go | 4 + api/http/handler/endpoints/endpoint_update.go | 50 ++++-- api/http/handler/endpoints/handler.go | 1 + api/http/handler/handler.go | 2 + api/http/proxy/factory/azure.go | 20 +++ api/http/proxy/factory/azure/transport.go | 80 +++++++++ api/http/proxy/factory/factory.go | 7 + api/portainer.go | 8 +- api/swagger.yaml | 47 ++++- app/__module.js | 2 + app/assets/css/app.css | 4 + app/azure/_module.js | 51 ++++++ .../azure-endpoint-config.js | 8 + .../azureEndpointConfig.html | 36 ++++ .../azure-sidebar-content.js | 3 + .../azureSidebarContent.html | 6 + .../containerGroupsDatatable.html | 105 +++++++++++ .../containerGroupsDatatable.js | 13 ++ app/azure/models/container_group.js | 66 +++++++ app/azure/models/location.js | 6 + app/azure/models/provider.js | 9 + app/azure/models/resource_group.js | 6 + app/azure/models/subscription.js | 4 + app/azure/rest/azure.js | 20 +++ app/azure/rest/container_group.js | 45 +++++ app/azure/rest/location.js | 18 ++ app/azure/rest/provider.js | 18 ++ app/azure/rest/resource_group.js | 18 ++ app/azure/rest/subscription.js | 18 ++ app/azure/services/azureService.js | 72 ++++++++ app/azure/services/containerGroupService.js | 41 +++++ app/azure/services/locationService.js | 29 +++ app/azure/services/providerService.js | 27 +++ app/azure/services/resourceGroupService.js | 29 +++ app/azure/services/subscriptionService.js | 29 +++ .../containerInstancesController.js | 44 +++++ .../containerinstances.html | 21 +++ .../createContainerInstanceController.js | 93 ++++++++++ .../create/createcontainerinstance.html | 167 ++++++++++++++++++ app/azure/views/dashboard/dashboard.html | 33 ++++ .../views/dashboard/dashboardController.js | 23 +++ .../endpoint-item/endpointItem.html | 2 +- app/portainer/filters/filters.js | 2 + app/portainer/services/api/endpointService.js | 14 ++ app/portainer/services/fileUpload.js | 16 ++ app/portainer/services/stateManager.js | 8 + .../create/createEndpointController.js | 29 +++ .../endpoints/create/createendpoint.html | 114 ++++++++++++ .../views/endpoints/edit/endpoint.html | 8 +- .../endpoints/edit/endpointController.js | 3 + app/portainer/views/home/homeController.js | 17 +- .../views/init/endpoint/initEndpoint.html | 95 ++++++++++ .../init/endpoint/initEndpointController.js | 26 +++ app/portainer/views/sidebar/sidebar.html | 3 +- jsconfig.json | 1 + webpack/webpack.common.js | 1 + 65 files changed, 1793 insertions(+), 50 deletions(-) create mode 100644 api/http/handler/endpointproxy/proxy_azure.go create mode 100644 api/http/proxy/factory/azure.go create mode 100644 api/http/proxy/factory/azure/transport.go create mode 100644 app/azure/_module.js create mode 100644 app/azure/components/azure-endpoint-config/azure-endpoint-config.js create mode 100644 app/azure/components/azure-endpoint-config/azureEndpointConfig.html create mode 100644 app/azure/components/azure-sidebar-content/azure-sidebar-content.js create mode 100644 app/azure/components/azure-sidebar-content/azureSidebarContent.html create mode 100644 app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html create mode 100644 app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js create mode 100644 app/azure/models/container_group.js create mode 100644 app/azure/models/location.js create mode 100644 app/azure/models/provider.js create mode 100644 app/azure/models/resource_group.js create mode 100644 app/azure/models/subscription.js create mode 100644 app/azure/rest/azure.js create mode 100644 app/azure/rest/container_group.js create mode 100644 app/azure/rest/location.js create mode 100644 app/azure/rest/provider.js create mode 100644 app/azure/rest/resource_group.js create mode 100644 app/azure/rest/subscription.js create mode 100644 app/azure/services/azureService.js create mode 100644 app/azure/services/containerGroupService.js create mode 100644 app/azure/services/locationService.js create mode 100644 app/azure/services/providerService.js create mode 100644 app/azure/services/resourceGroupService.js create mode 100644 app/azure/services/subscriptionService.js create mode 100644 app/azure/views/containerinstances/containerInstancesController.js create mode 100644 app/azure/views/containerinstances/containerinstances.html create mode 100644 app/azure/views/containerinstances/create/createContainerInstanceController.js create mode 100644 app/azure/views/containerinstances/create/createcontainerinstance.html create mode 100644 app/azure/views/dashboard/dashboard.html create mode 100644 app/azure/views/dashboard/dashboardController.js diff --git a/api/cron/job_snapshot.go b/api/cron/job_snapshot.go index e9c6e602b..7fedc0f6d 100644 --- a/api/cron/job_snapshot.go +++ b/api/cron/job_snapshot.go @@ -53,7 +53,7 @@ func (runner *SnapshotJobRunner) Run() { } for _, endpoint := range endpoints { - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment { continue } diff --git a/api/docker/client.go b/api/docker/client.go index ce8d21ec1..c1bd7a8d0 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -35,7 +35,9 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers // a specific endpoint configuration. The nodeName parameter can be used // with an agent enabled endpoint to target a specific node in an agent cluster. func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) { - if endpoint.Type == portainer.AgentOnDockerEnvironment { + if endpoint.Type == portainer.AzureEnvironment { + return nil, unsupportedEnvironmentType + } else if endpoint.Type == portainer.AgentOnDockerEnvironment { return createAgentClient(endpoint, factory.signatureService, nodeName) } else if endpoint.Type == portainer.EdgeAgentEnvironment { return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName) diff --git a/api/errors.go b/api/errors.go index bc639d341..8e09838a1 100644 --- a/api/errors.go +++ b/api/errors.go @@ -39,6 +39,11 @@ const ( ErrEndpointAccessDenied = Error("Access denied to endpoint") ) +// Azure environment errors +const ( + ErrAzureInvalidCredentials = Error("Invalid Azure credentials") +) + // Endpoint group errors. const ( ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group") diff --git a/api/go.sum b/api/go.sum index d3eead3d3..621d3a831 100644 --- a/api/go.sum +++ b/api/go.sum @@ -171,7 +171,6 @@ github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yH github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II= github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0= -github.com/portainer/portainer v0.10.1 h1:I8K345CjGWfUGsVA8c8/gqamwLCC6CIAjxZXSklAFq0= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= diff --git a/api/http/client/client.go b/api/http/client/client.go index 0e6d9d41d..fb690105f 100644 --- a/api/http/client/client.go +++ b/api/http/client/client.go @@ -2,9 +2,12 @@ package client import ( "crypto/tls" + "encoding/json" + "fmt" "io/ioutil" "log" "net/http" + "net/url" "strings" "time" @@ -16,6 +19,55 @@ const ( defaultHTTPTimeout = 5 ) +// HTTPClient represents a client to send HTTP requests. +type HTTPClient struct { + *http.Client +} + +// NewHTTPClient is used to build a new HTTPClient. +func NewHTTPClient() *HTTPClient { + return &HTTPClient{ + &http.Client{ + Timeout: time.Second * time.Duration(defaultHTTPTimeout), + }, + } +} + +// AzureAuthenticationResponse represents an Azure API authentication response. +type AzureAuthenticationResponse struct { + AccessToken string `json:"access_token"` + ExpiresOn string `json:"expires_on"` +} + +// ExecuteAzureAuthenticationRequest is used to execute an authentication request +// against the Azure API. It re-uses the same http.Client. +func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portainer.AzureCredentials) (*AzureAuthenticationResponse, error) { + loginURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", credentials.TenantID) + params := url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {credentials.ApplicationID}, + "client_secret": {credentials.AuthenticationKey}, + "resource": {"https://management.azure.com/"}, + } + + response, err := client.PostForm(loginURL, params) + if err != nil { + return nil, err + } + + if response.StatusCode != http.StatusOK { + return nil, portainer.ErrAzureInvalidCredentials + } + + var token AzureAuthenticationResponse + err = json.NewDecoder(response.Body).Decode(&token) + if err != nil { + return nil, err + } + + return &token, nil +} + // Get executes a simple HTTP GET to the specified URL and returns // the content of the response body. Timeout can be specified via the timeout parameter, // will default to defaultHTTPTimeout if set to 0. diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index 394fa0b54..870f2de80 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -23,6 +23,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), requestBouncer: bouncer, } + h.PathPrefix("/{id}/azure").Handler( + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) h.PathPrefix("/{id}/docker").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) h.PathPrefix("/{id}/storidge").Handler( diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go new file mode 100644 index 000000000..a9e66b66b --- /dev/null +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -0,0 +1,43 @@ +package endpointproxy + +import ( + "strconv" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/portainer/api" + + "net/http" +) + +func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + + var proxy http.Handler + proxy = handler.ProxyManager.GetEndpointProxy(endpoint) + if proxy == nil { + proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err} + } + } + + id := strconv.Itoa(endpointID) + http.StripPrefix("/"+id+"/azure", proxy).ServeHTTP(w, r) + return nil +} diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index d8a1bb898..677ce6222 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -18,19 +18,22 @@ import ( ) type endpointCreatePayload struct { - Name string - URL string - EndpointType int - PublicURL string - GroupID int - TLS bool - TLSSkipVerify bool - TLSSkipClientVerify bool - TLSCACertFile []byte - TLSCertFile []byte - TLSKeyFile []byte - TagIDs []portainer.TagID - EdgeCheckinInterval int + Name string + URL string + EndpointType int + PublicURL string + GroupID int + TLS bool + TLSSkipVerify bool + TLSSkipClientVerify bool + TLSCACertFile []byte + TLSCertFile []byte + TLSKeyFile []byte + AzureApplicationID string + AzureTenantID string + AzureAuthenticationKey string + TagIDs []portainer.TagID + EdgeCheckinInterval int } func (payload *endpointCreatePayload) Validate(r *http.Request) error { @@ -42,7 +45,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false) if err != nil || endpointType == 0 { - return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 4 (Edge Agent environment)") + return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)") } payload.EndpointType = endpointType @@ -94,14 +97,35 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } } - endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true) - if err != nil { - return portainer.Error("Invalid endpoint URL") - } - payload.URL = endpointURL + switch portainer.EndpointType(payload.EndpointType) { + case portainer.AzureEnvironment: + azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false) + if err != nil { + return portainer.Error("Invalid Azure application ID") + } + payload.AzureApplicationID = azureApplicationID - publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true) - payload.PublicURL = publicURL + azureTenantID, err := request.RetrieveMultiPartFormValue(r, "AzureTenantID", false) + if err != nil { + return portainer.Error("Invalid Azure tenant ID") + } + payload.AzureTenantID = azureTenantID + + azureAuthenticationKey, err := request.RetrieveMultiPartFormValue(r, "AzureAuthenticationKey", false) + if err != nil { + return portainer.Error("Invalid Azure authentication key") + } + payload.AzureAuthenticationKey = azureAuthenticationKey + default: + url, err := request.RetrieveMultiPartFormValue(r, "URL", true) + if err != nil { + return portainer.Error("Invalid endpoint URL") + } + payload.URL = url + + publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true) + payload.PublicURL = publicURL + } checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true) payload.EdgeCheckinInterval = checkinInterval @@ -158,7 +182,9 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * } func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment { + if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment { + return handler.createAzureEndpoint(payload) + } else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment { return handler.createEdgeAgentEndpoint(payload) } @@ -168,6 +194,44 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain return handler.createUnsecuredEndpoint(payload) } +func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { + credentials := portainer.AzureCredentials{ + ApplicationID: payload.AzureApplicationID, + TenantID: payload.AzureTenantID, + AuthenticationKey: payload.AzureAuthenticationKey, + } + + httpClient := client.NewHTTPClient() + _, err := httpClient.ExecuteAzureAuthenticationRequest(&credentials) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", err} + } + + endpointID := handler.DataStore.Endpoint().GetNextIdentifier() + endpoint := &portainer.Endpoint{ + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: "https://management.azure.com", + Type: portainer.AzureEnvironment, + GroupID: portainer.EndpointGroupID(payload.GroupID), + PublicURL: payload.PublicURL, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + AzureCredentials: credentials, + TagIDs: payload.TagIDs, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, + } + + err = handler.saveEndpointAndUpdateAuthorizations(endpoint) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err} + } + + return endpoint, nil +} + func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { endpointType := portainer.EdgeAgentEnvironment endpointID := handler.DataStore.Endpoint().GetNextIdentifier() diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 21d6b8b0a..18182db17 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -23,6 +23,10 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } + if endpoint.Type == portainer.AzureEnvironment { + return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for Azure endpoints", err} + } + snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint) latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go index 092dc5df1..33d6f30d0 100644 --- a/api/http/handler/endpoints/endpoint_snapshots.go +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -17,6 +17,10 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request } for _, endpoint := range endpoints { + if endpoint.Type == portainer.AzureEnvironment { + continue + } + snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint) latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 4752086c3..766c09207 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -9,21 +9,25 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/client" ) type endpointUpdatePayload struct { - Name *string - URL *string - PublicURL *string - GroupID *int - TLS *bool - TLSSkipVerify *bool - TLSSkipClientVerify *bool - Status *int - TagIDs []portainer.TagID - UserAccessPolicies portainer.UserAccessPolicies - TeamAccessPolicies portainer.TeamAccessPolicies - EdgeCheckinInterval *int + Name *string + URL *string + PublicURL *string + GroupID *int + TLS *bool + TLSSkipVerify *bool + TLSSkipClientVerify *bool + Status *int + AzureApplicationID *string + AzureTenantID *string + AzureAuthenticationKey *string + TagIDs []portainer.TagID + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies + EdgeCheckinInterval *int } func (payload *endpointUpdatePayload) Validate(r *http.Request) error { @@ -138,6 +142,26 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } + if endpoint.Type == portainer.AzureEnvironment { + credentials := endpoint.AzureCredentials + if payload.AzureApplicationID != nil { + credentials.ApplicationID = *payload.AzureApplicationID + } + if payload.AzureTenantID != nil { + credentials.TenantID = *payload.AzureTenantID + } + if payload.AzureAuthenticationKey != nil { + credentials.AuthenticationKey = *payload.AzureAuthenticationKey + } + + httpClient := client.NewHTTPClient() + _, authErr := httpClient.ExecuteAzureAuthenticationRequest(&credentials) + if authErr != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate against Azure", authErr} + } + endpoint.AzureCredentials = credentials + } + if payload.TLS != nil { folder := strconv.Itoa(endpointID) @@ -182,7 +206,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } - if payload.URL != nil || payload.TLS != nil { + if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment { _, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 15e8a55c0..0fb6e5b06 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -12,6 +12,7 @@ import ( ) func hideFields(endpoint *portainer.Endpoint) { + endpoint.AzureCredentials = portainer.AzureCredentials{} if len(endpoint.Snapshots) > 0 { endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{} } diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index ad0fa92df..8b167b12e 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -90,6 +90,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/storidge/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) + case strings.Contains(r.URL.Path, "/azure/"): + http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/edge/"): http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r) default: diff --git a/api/http/proxy/factory/azure.go b/api/http/proxy/factory/azure.go new file mode 100644 index 000000000..27b8a26f8 --- /dev/null +++ b/api/http/proxy/factory/azure.go @@ -0,0 +1,20 @@ +package factory + +import ( + "net/http" + "net/url" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/azure" +) + +func newAzureProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + remoteURL, err := url.Parse(azureAPIBaseURL) + if err != nil { + return nil, err + } + + proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) + proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials) + return proxy, nil +} diff --git a/api/http/proxy/factory/azure/transport.go b/api/http/proxy/factory/azure/transport.go new file mode 100644 index 000000000..0c8505c8b --- /dev/null +++ b/api/http/proxy/factory/azure/transport.go @@ -0,0 +1,80 @@ +package azure + +import ( + "net/http" + "strconv" + "sync" + "time" + + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/client" +) + +type ( + azureAPIToken struct { + value string + expirationTime time.Time + } + + Transport struct { + credentials *portainer.AzureCredentials + client *client.HTTPClient + token *azureAPIToken + mutex sync.Mutex + } +) + +// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport +// interface for proxying requests to the Azure API. +func NewTransport(credentials *portainer.AzureCredentials) *Transport { + return &Transport{ + credentials: credentials, + client: client.NewHTTPClient(), + } +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) { + err := transport.retrieveAuthenticationToken() + if err != nil { + return nil, err + } + + request.Header.Set("Authorization", "Bearer "+transport.token.value) + return http.DefaultTransport.RoundTrip(request) +} + +func (transport *Transport) authenticate() error { + token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials) + if err != nil { + return err + } + + expiresOn, err := strconv.ParseInt(token.ExpiresOn, 10, 64) + if err != nil { + return err + } + + transport.token = &azureAPIToken{ + value: token.AccessToken, + expirationTime: time.Unix(expiresOn, 0), + } + + return nil +} + +func (transport *Transport) retrieveAuthenticationToken() error { + transport.mutex.Lock() + defer transport.mutex.Unlock() + + if transport.token == nil { + return transport.authenticate() + } + + timeLimit := time.Now().Add(-5 * time.Minute) + if timeLimit.After(transport.token.expirationTime) { + return transport.authenticate() + } + + return nil +} diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go index b81291ab9..6ebedbb9f 100644 --- a/api/http/proxy/factory/factory.go +++ b/api/http/proxy/factory/factory.go @@ -10,6 +10,8 @@ import ( "github.com/portainer/portainer/api/docker" ) +const azureAPIBaseURL = "https://management.azure.com" + var extensionPorts = map[portainer.ExtensionID]string{ portainer.RegistryManagementExtension: "7001", portainer.OAuthAuthenticationExtension: "7002", @@ -69,6 +71,11 @@ func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (ht // NewEndpointProxy returns a new reverse proxy (filesystem based or HTTP) to an endpoint API server func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + switch endpoint.Type { + case portainer.AzureEnvironment: + return newAzureProxy(endpoint) + } + return factory.newDockerProxy(endpoint) } diff --git a/api/portainer.go b/api/portainer.go index 8386ce8f2..a2ec3efd6 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -27,7 +27,7 @@ type ( Authorizations map[Authorization]bool // AzureCredentials represents the credentials used to connect to an Azure - // environment (deprecated). + // environment. AzureCredentials struct { ApplicationID string `json:"ApplicationID"` TenantID string `json:"TenantID"` @@ -163,6 +163,7 @@ type ( PublicURL string `json:"PublicURL"` TLSConfig TLSConfiguration `json:"TLSConfig"` Extensions []EndpointExtension `json:"Extensions"` + AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` TagIDs []TagID `json:"TagIds"` Status EndpointStatus `json:"Status"` Snapshots []Snapshot `json:"Snapshots"` @@ -185,9 +186,6 @@ type ( // Deprecated in DBVersion == 22 Tags []string `json:"Tags"` - - // Deprecated in DBVersion == 24 - AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` } // EndpointAuthorizations represents the authorizations associated to a set of endpoints @@ -1101,7 +1099,7 @@ const ( DockerEnvironment // AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment AgentOnDockerEnvironment - // AzureEnvironment represents an endpoint connected to an Azure environment (deprecated) + // AzureEnvironment represents an endpoint connected to an Azure environment AzureEnvironment // EdgeAgentEnvironment represents an endpoint connected to an Edge agent EdgeAgentEnvironment diff --git a/api/swagger.yaml b/api/swagger.yaml index abb4aa686..2ecd61139 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -254,7 +254,7 @@ paths: - name: "EndpointType" in: "formData" type: "integer" - description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 4 (Edge agent environment)" + description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge agent environment)" required: true - name: "URL" in: "formData" @@ -294,6 +294,18 @@ paths: in: "formData" type: "file" description: "TLS client key file" + - name: "AzureApplicationID" + in: "formData" + type: "string" + description: "Azure application ID. Required if endpoint type is set to 3" + - name: "AzureTenantID" + in: "formData" + type: "string" + description: "Azure tenant ID. Required if endpoint type is set to 3" + - name: "AzureAuthenticationKey" + in: "formData" + type: "string" + description: "Azure authentication key. Required if endpoint type is set to 3" responses: 200: description: "Success" @@ -3209,6 +3221,21 @@ definitions: type: "string" example: "/data/tls/key.pem" description: "Path to the TLS client key file" + AzureCredentials: + type: "object" + properties: + ApplicationID: + type: "string" + example: "eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4" + description: "Azure application ID" + TenantID: + type: "string" + example: "34ddc78d-4fel-2358-8cc1-df84c8o839f5" + description: "Azure tenant ID" + AuthenticationKey: + type: "string" + example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" + description: "Azure authentication key" LDAPSearchSettings: type: "object" properties: @@ -3480,7 +3507,7 @@ definitions: Type: type: "integer" example: 1 - description: "Endpoint environment type. 1 for a Docker environment or 2 for an agent on Docker environment" + description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment or 3 for an Azure environment." URL: type: "string" example: "docker.mydomain.tld:2375" @@ -3509,6 +3536,8 @@ definitions: description: "Team identifier" TLSConfig: $ref: "#/definitions/TLSConfiguration" + AzureCredentials: + $ref: "#/definitions/AzureCredentials" EndpointSubset: type: "object" properties: @@ -3523,7 +3552,7 @@ definitions: Type: type: "integer" example: 1 - description: "Endpoint environment type. 1 for a Docker environment or 2 for an agent on Docker environment" + description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment, 3 for an Azure environment." URL: type: "string" example: "docker.mydomain.tld:2375" @@ -3703,6 +3732,18 @@ definitions: type: "boolean" example: false description: "Skip client verification when using TLS" + ApplicationID: + type: "string" + example: "eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4" + description: "Azure application ID" + TenantID: + type: "string" + example: "34ddc78d-4fel-2358-8cc1-df84c8o839f5" + description: "Azure tenant ID" + AuthenticationKey: + type: "string" + example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" + description: "Azure authentication key" UserAccessPolicies: $ref: "#/definitions/UserAccessPolicies" TeamAccessPolicies: diff --git a/app/__module.js b/app/__module.js index 8e878ded2..0aed3bfa1 100644 --- a/app/__module.js +++ b/app/__module.js @@ -2,6 +2,7 @@ import './assets/css'; import angular from 'angular'; import './agent/_module'; +import './azure/_module'; import './docker/__module'; import './edge/__module'; import './portainer/__module'; @@ -27,6 +28,7 @@ angular.module('portainer', [ 'luegg.directives', 'portainer.app', 'portainer.agent', + 'portainer.azure', 'portainer.docker', 'portainer.edge', 'portainer.extensions', diff --git a/app/assets/css/app.css b/app/assets/css/app.css index f1b18aa53..f57e3802f 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -236,6 +236,10 @@ a[ng-click] { margin: 10px 4px 0 6px; } +.blocklist-item-logo.endpoint-item.azure { + margin: 0 0 0 10px; +} + .blocklist-item-title { font-size: 1.8em; font-weight: bold; diff --git a/app/azure/_module.js b/app/azure/_module.js new file mode 100644 index 000000000..a11a5aa5e --- /dev/null +++ b/app/azure/_module.js @@ -0,0 +1,51 @@ +angular.module('portainer.azure', ['portainer.app']).config([ + '$stateRegistryProvider', + function ($stateRegistryProvider) { + 'use strict'; + + var azure = { + name: 'azure', + url: '/azure', + parent: 'root', + abstract: true, + }; + + var containerInstances = { + name: 'azure.containerinstances', + url: '/containerinstances', + views: { + 'content@': { + templateUrl: './views/containerinstances/containerinstances.html', + controller: 'AzureContainerInstancesController', + }, + }, + }; + + var containerInstanceCreation = { + name: 'azure.containerinstances.new', + url: '/new/', + views: { + 'content@': { + templateUrl: './views/containerinstances/create/createcontainerinstance.html', + controller: 'AzureCreateContainerInstanceController', + }, + }, + }; + + var dashboard = { + name: 'azure.dashboard', + url: '/dashboard', + views: { + 'content@': { + templateUrl: './views/dashboard/dashboard.html', + controller: 'AzureDashboardController', + }, + }, + }; + + $stateRegistryProvider.register(azure); + $stateRegistryProvider.register(containerInstances); + $stateRegistryProvider.register(containerInstanceCreation); + $stateRegistryProvider.register(dashboard); + }, +]); diff --git a/app/azure/components/azure-endpoint-config/azure-endpoint-config.js b/app/azure/components/azure-endpoint-config/azure-endpoint-config.js new file mode 100644 index 000000000..ff09f0908 --- /dev/null +++ b/app/azure/components/azure-endpoint-config/azure-endpoint-config.js @@ -0,0 +1,8 @@ +angular.module('portainer.azure').component('azureEndpointConfig', { + bindings: { + applicationId: '=', + tenantId: '=', + authenticationKey: '=', + }, + templateUrl: './azureEndpointConfig.html', +}); diff --git a/app/azure/components/azure-endpoint-config/azureEndpointConfig.html b/app/azure/components/azure-endpoint-config/azureEndpointConfig.html new file mode 100644 index 000000000..efc8bd79f --- /dev/null +++ b/app/azure/components/azure-endpoint-config/azureEndpointConfig.html @@ -0,0 +1,36 @@ +
+
+ Azure configuration +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ +
diff --git a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js new file mode 100644 index 000000000..daec3ef12 --- /dev/null +++ b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js @@ -0,0 +1,3 @@ +angular.module('portainer.azure').component('azureSidebarContent', { + templateUrl: './azureSidebarContent.html', +}); diff --git a/app/azure/components/azure-sidebar-content/azureSidebarContent.html b/app/azure/components/azure-sidebar-content/azureSidebarContent.html new file mode 100644 index 000000000..01986e8e7 --- /dev/null +++ b/app/azure/components/azure-sidebar-content/azureSidebarContent.html @@ -0,0 +1,6 @@ + + diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html new file mode 100644 index 000000000..f9936d78b --- /dev/null +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html @@ -0,0 +1,105 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Location + + + + + Published Ports +
+ + + + + {{ item.Name | truncate: 50 }} + {{ item.Location }} + + :{{ p.port }} + + - +
Loading...
No container available.
+
+ +
+
+
diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js new file mode 100644 index 000000000..8d91518a9 --- /dev/null +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.azure').component('containergroupsDatatable', { + templateUrl: './containerGroupsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + title: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + removeAction: '<', + }, +}); diff --git a/app/azure/models/container_group.js b/app/azure/models/container_group.js new file mode 100644 index 000000000..dfc9adeef --- /dev/null +++ b/app/azure/models/container_group.js @@ -0,0 +1,66 @@ +export function ContainerGroupDefaultModel() { + this.Location = ''; + this.OSType = 'Linux'; + this.Name = ''; + this.Image = ''; + this.AllocatePublicIP = true; + this.Ports = [ + { + container: 80, + host: 80, + protocol: 'TCP', + }, + ]; + this.CPU = 1; + this.Memory = 1; +} + +export function ContainerGroupViewModel(data) { + this.Id = data.id; + this.Name = data.name; + this.Location = data.location; + this.IPAddress = data.properties.ipAddress.ip; + this.Ports = data.properties.ipAddress.ports; +} + +export function CreateContainerGroupRequest(model) { + this.location = model.Location; + + var containerPorts = []; + var addressPorts = []; + for (var i = 0; i < model.Ports.length; i++) { + var binding = model.Ports[i]; + + containerPorts.push({ + port: binding.container, + }); + + addressPorts.push({ + port: binding.host, + protocol: binding.protocol, + }); + } + + this.properties = { + osType: model.OSType, + containers: [ + { + name: model.Name, + properties: { + image: model.Image, + ports: containerPorts, + resources: { + requests: { + cpu: model.CPU, + memoryInGB: model.Memory, + }, + }, + }, + }, + ], + ipAddress: { + type: model.AllocatePublicIP ? 'Public' : 'Private', + ports: addressPorts, + }, + }; +} diff --git a/app/azure/models/location.js b/app/azure/models/location.js new file mode 100644 index 000000000..6d4031331 --- /dev/null +++ b/app/azure/models/location.js @@ -0,0 +1,6 @@ +export function LocationViewModel(data) { + this.Id = data.id; + this.SubscriptionId = data.subscriptionId; + this.DisplayName = data.displayName; + this.Name = data.name; +} diff --git a/app/azure/models/provider.js b/app/azure/models/provider.js new file mode 100644 index 000000000..d9d6c8075 --- /dev/null +++ b/app/azure/models/provider.js @@ -0,0 +1,9 @@ +import _ from 'lodash-es'; + +export function ContainerInstanceProviderViewModel(data) { + this.Id = data.id; + this.Namespace = data.namespace; + + var containerGroupType = _.find(data.resourceTypes, { resourceType: 'containerGroups' }); + this.Locations = containerGroupType.locations; +} diff --git a/app/azure/models/resource_group.js b/app/azure/models/resource_group.js new file mode 100644 index 000000000..894ce326d --- /dev/null +++ b/app/azure/models/resource_group.js @@ -0,0 +1,6 @@ +export function ResourceGroupViewModel(data, subscriptionId) { + this.Id = data.id; + this.SubscriptionId = subscriptionId; + this.Name = data.name; + this.Location = data.location; +} diff --git a/app/azure/models/subscription.js b/app/azure/models/subscription.js new file mode 100644 index 000000000..eb9bfaf52 --- /dev/null +++ b/app/azure/models/subscription.js @@ -0,0 +1,4 @@ +export function SubscriptionViewModel(data) { + this.Id = data.subscriptionId; + this.Name = data.displayName; +} diff --git a/app/azure/rest/azure.js b/app/azure/rest/azure.js new file mode 100644 index 000000000..f463624d6 --- /dev/null +++ b/app/azure/rest/azure.js @@ -0,0 +1,20 @@ +angular.module('portainer.azure').factory('Azure', [ + '$http', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function AzureFactory($http, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + + var service = {}; + + service.delete = function (id, apiVersion) { + var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/azure' + id + '?api-version=' + apiVersion; + return $http({ + method: 'DELETE', + url: url, + }); + }; + + return service; + }, +]); diff --git a/app/azure/rest/container_group.js b/app/azure/rest/container_group.js new file mode 100644 index 000000000..4dc7a002d --- /dev/null +++ b/app/azure/rest/container_group.js @@ -0,0 +1,45 @@ +angular.module('portainer.azure').factory('ContainerGroup', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function ContainerGroupFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + + var resource = {}; + + var base = $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/providers/Microsoft.ContainerInstance/containerGroups', + { + endpointId: EndpointProvider.endpointID, + 'api-version': '2018-04-01', + }, + { + query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }, + } + ); + + var withResourceGroup = $resource( + API_ENDPOINT_ENDPOINTS + + '/:endpointId/azure/subscriptions/:subscriptionId/resourceGroups/:resourceGroupName/providers/Microsoft.ContainerInstance/containerGroups/:containerGroupName', + { + endpointId: EndpointProvider.endpointID, + 'api-version': '2018-04-01', + }, + { + create: { + method: 'PUT', + params: { + subscriptionId: '@subscriptionId', + resourceGroupName: '@resourceGroupName', + containerGroupName: '@containerGroupName', + }, + }, + } + ); + + resource.query = base.query; + resource.create = withResourceGroup.create; + + return resource; + }, +]); diff --git a/app/azure/rest/location.js b/app/azure/rest/location.js new file mode 100644 index 000000000..7503d9fc9 --- /dev/null +++ b/app/azure/rest/location.js @@ -0,0 +1,18 @@ +angular.module('portainer.azure').factory('Location', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function LocationFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/locations', + { + endpointId: EndpointProvider.endpointID, + 'api-version': '2016-06-01', + }, + { + query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }, + } + ); + }, +]); diff --git a/app/azure/rest/provider.js b/app/azure/rest/provider.js new file mode 100644 index 000000000..b8e76d81e --- /dev/null +++ b/app/azure/rest/provider.js @@ -0,0 +1,18 @@ +angular.module('portainer.azure').factory('Provider', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function ProviderFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/providers/:providerNamespace', + { + endpointId: EndpointProvider.endpointID, + 'api-version': '2018-02-01', + }, + { + get: { method: 'GET', params: { subscriptionId: '@subscriptionId', providerNamespace: '@providerNamespace' } }, + } + ); + }, +]); diff --git a/app/azure/rest/resource_group.js b/app/azure/rest/resource_group.js new file mode 100644 index 000000000..644279f3b --- /dev/null +++ b/app/azure/rest/resource_group.js @@ -0,0 +1,18 @@ +angular.module('portainer.azure').factory('ResourceGroup', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function ResourceGroupFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions/:subscriptionId/resourcegroups', + { + endpointId: EndpointProvider.endpointID, + 'api-version': '2018-02-01', + }, + { + query: { method: 'GET', params: { subscriptionId: '@subscriptionId' } }, + } + ); + }, +]); diff --git a/app/azure/rest/subscription.js b/app/azure/rest/subscription.js new file mode 100644 index 000000000..0711d5f92 --- /dev/null +++ b/app/azure/rest/subscription.js @@ -0,0 +1,18 @@ +angular.module('portainer.azure').factory('Subscription', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function SubscriptionFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/azure/subscriptions', + { + endpointId: EndpointProvider.endpointID, + 'api-version': '2016-06-01', + }, + { + query: { method: 'GET' }, + } + ); + }, +]); diff --git a/app/azure/services/azureService.js b/app/azure/services/azureService.js new file mode 100644 index 000000000..b6c3ba0aa --- /dev/null +++ b/app/azure/services/azureService.js @@ -0,0 +1,72 @@ +angular.module('portainer.azure').factory('AzureService', [ + '$q', + 'Azure', + 'SubscriptionService', + 'ResourceGroupService', + 'ContainerGroupService', + 'ProviderService', + function AzureServiceFactory($q, Azure, SubscriptionService, ResourceGroupService, ContainerGroupService, ProviderService) { + 'use strict'; + var service = {}; + + service.deleteContainerGroup = function (id) { + return Azure.delete(id, '2018-04-01'); + }; + + service.createContainerGroup = function (model, subscriptionId, resourceGroupName) { + return ContainerGroupService.create(model, subscriptionId, resourceGroupName); + }; + + service.subscriptions = function () { + return SubscriptionService.subscriptions(); + }; + + service.containerInstanceProvider = function (subscriptions) { + return retrieveResourcesForEachSubscription(subscriptions, ProviderService.containerInstanceProvider); + }; + + service.resourceGroups = function (subscriptions) { + return retrieveResourcesForEachSubscription(subscriptions, ResourceGroupService.resourceGroups); + }; + + service.containerGroups = function (subscriptions) { + return retrieveResourcesForEachSubscription(subscriptions, ContainerGroupService.containerGroups); + }; + + service.aggregate = function (resourcesBySubcription) { + var aggregatedResources = []; + Object.keys(resourcesBySubcription).forEach(function (key) { + aggregatedResources = aggregatedResources.concat(resourcesBySubcription[key]); + }); + return aggregatedResources; + }; + + function retrieveResourcesForEachSubscription(subscriptions, resourceQuery) { + var deferred = $q.defer(); + + var resources = {}; + + var resourceQueries = []; + for (var i = 0; i < subscriptions.length; i++) { + var subscription = subscriptions[i]; + resourceQueries.push(resourceQuery(subscription.Id)); + } + + $q.all(resourceQueries) + .then(function success(data) { + for (var i = 0; i < data.length; i++) { + var result = data[i]; + resources[subscriptions[i].Id] = result; + } + deferred.resolve(resources); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve resources', err: err }); + }); + + return deferred.promise; + } + + return service; + }, +]); diff --git a/app/azure/services/containerGroupService.js b/app/azure/services/containerGroupService.js new file mode 100644 index 000000000..c99b98ada --- /dev/null +++ b/app/azure/services/containerGroupService.js @@ -0,0 +1,41 @@ +import { ContainerGroupViewModel, CreateContainerGroupRequest } from '../models/container_group'; + +angular.module('portainer.azure').factory('ContainerGroupService', [ + '$q', + 'ContainerGroup', + function ContainerGroupServiceFactory($q, ContainerGroup) { + 'use strict'; + var service = {}; + + service.containerGroups = function (subscriptionId) { + var deferred = $q.defer(); + + ContainerGroup.query({ subscriptionId: subscriptionId }) + .$promise.then(function success(data) { + var containerGroups = data.value.map(function (item) { + return new ContainerGroupViewModel(item); + }); + deferred.resolve(containerGroups); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve container groups', err: err }); + }); + + return deferred.promise; + }; + + service.create = function (model, subscriptionId, resourceGroupName) { + var payload = new CreateContainerGroupRequest(model); + return ContainerGroup.create( + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + containerGroupName: model.Name, + }, + payload + ).$promise; + }; + + return service; + }, +]); diff --git a/app/azure/services/locationService.js b/app/azure/services/locationService.js new file mode 100644 index 000000000..a21e7fa0a --- /dev/null +++ b/app/azure/services/locationService.js @@ -0,0 +1,29 @@ +import { LocationViewModel } from '../models/location'; + +angular.module('portainer.azure').factory('LocationService', [ + '$q', + 'Location', + function LocationServiceFactory($q, Location) { + 'use strict'; + var service = {}; + + service.locations = function (subscriptionId) { + var deferred = $q.defer(); + + Location.query({ subscriptionId: subscriptionId }) + .$promise.then(function success(data) { + var locations = data.value.map(function (item) { + return new LocationViewModel(item); + }); + deferred.resolve(locations); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve locations', err: err }); + }); + + return deferred.promise; + }; + + return service; + }, +]); diff --git a/app/azure/services/providerService.js b/app/azure/services/providerService.js new file mode 100644 index 000000000..edc42ae9e --- /dev/null +++ b/app/azure/services/providerService.js @@ -0,0 +1,27 @@ +import { ContainerInstanceProviderViewModel } from '../models/provider'; + +angular.module('portainer.azure').factory('ProviderService', [ + '$q', + 'Provider', + function ProviderServiceFactory($q, Provider) { + 'use strict'; + var service = {}; + + service.containerInstanceProvider = function (subscriptionId) { + var deferred = $q.defer(); + + Provider.get({ subscriptionId: subscriptionId, providerNamespace: 'Microsoft.ContainerInstance' }) + .$promise.then(function success(data) { + var provider = new ContainerInstanceProviderViewModel(data); + deferred.resolve(provider); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve provider', err: err }); + }); + + return deferred.promise; + }; + + return service; + }, +]); diff --git a/app/azure/services/resourceGroupService.js b/app/azure/services/resourceGroupService.js new file mode 100644 index 000000000..4110835f4 --- /dev/null +++ b/app/azure/services/resourceGroupService.js @@ -0,0 +1,29 @@ +import { ResourceGroupViewModel } from '../models/resource_group'; + +angular.module('portainer.azure').factory('ResourceGroupService', [ + '$q', + 'ResourceGroup', + function ResourceGroupServiceFactory($q, ResourceGroup) { + 'use strict'; + var service = {}; + + service.resourceGroups = function (subscriptionId) { + var deferred = $q.defer(); + + ResourceGroup.query({ subscriptionId: subscriptionId }) + .$promise.then(function success(data) { + var resourceGroups = data.value.map(function (item) { + return new ResourceGroupViewModel(item, subscriptionId); + }); + deferred.resolve(resourceGroups); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve resource groups', err: err }); + }); + + return deferred.promise; + }; + + return service; + }, +]); diff --git a/app/azure/services/subscriptionService.js b/app/azure/services/subscriptionService.js new file mode 100644 index 000000000..3b22ac664 --- /dev/null +++ b/app/azure/services/subscriptionService.js @@ -0,0 +1,29 @@ +import { SubscriptionViewModel } from '../models/subscription'; + +angular.module('portainer.azure').factory('SubscriptionService', [ + '$q', + 'Subscription', + function SubscriptionServiceFactory($q, Subscription) { + 'use strict'; + var service = {}; + + service.subscriptions = function () { + var deferred = $q.defer(); + + Subscription.query({}) + .$promise.then(function success(data) { + var subscriptions = data.value.map(function (item) { + return new SubscriptionViewModel(item); + }); + deferred.resolve(subscriptions); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve subscriptions', err: err }); + }); + + return deferred.promise; + }; + + return service; + }, +]); diff --git a/app/azure/views/containerinstances/containerInstancesController.js b/app/azure/views/containerinstances/containerInstancesController.js new file mode 100644 index 000000000..4863d5cac --- /dev/null +++ b/app/azure/views/containerinstances/containerInstancesController.js @@ -0,0 +1,44 @@ +angular.module('portainer.azure').controller('AzureContainerInstancesController', [ + '$scope', + '$state', + 'AzureService', + 'Notifications', + function ($scope, $state, AzureService, Notifications) { + function initView() { + AzureService.subscriptions() + .then(function success(data) { + var subscriptions = data; + return AzureService.containerGroups(subscriptions); + }) + .then(function success(data) { + $scope.containerGroups = AzureService.aggregate(data); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to load container groups'); + }); + } + + $scope.deleteAction = function (selectedItems) { + var actionCount = selectedItems.length; + angular.forEach(selectedItems, function (item) { + AzureService.deleteContainerGroup(item.Id) + .then(function success() { + Notifications.success('Container group successfully removed', item.Name); + var index = $scope.containerGroups.indexOf(item); + $scope.containerGroups.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove container group'); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); + }); + }; + + initView(); + }, +]); diff --git a/app/azure/views/containerinstances/containerinstances.html b/app/azure/views/containerinstances/containerinstances.html new file mode 100644 index 000000000..6c0223852 --- /dev/null +++ b/app/azure/views/containerinstances/containerinstances.html @@ -0,0 +1,21 @@ + + + + + + + Container instances + + +
+
+ +
+
diff --git a/app/azure/views/containerinstances/create/createContainerInstanceController.js b/app/azure/views/containerinstances/create/createContainerInstanceController.js new file mode 100644 index 000000000..7c2774946 --- /dev/null +++ b/app/azure/views/containerinstances/create/createContainerInstanceController.js @@ -0,0 +1,93 @@ +import { ContainerGroupDefaultModel } from '../../../models/container_group'; + +angular.module('portainer.azure').controller('AzureCreateContainerInstanceController', [ + '$q', + '$scope', + '$state', + 'AzureService', + 'Notifications', + function ($q, $scope, $state, AzureService, Notifications) { + var allResourceGroups = []; + var allProviders = []; + + $scope.state = { + actionInProgress: false, + selectedSubscription: null, + selectedResourceGroup: null, + }; + + $scope.changeSubscription = function () { + var selectedSubscription = $scope.state.selectedSubscription; + updateResourceGroupsAndLocations(selectedSubscription, allResourceGroups, allProviders); + }; + + $scope.addPortBinding = function () { + $scope.model.Ports.push({ host: '', container: '', protocol: 'TCP' }); + }; + + $scope.removePortBinding = function (index) { + $scope.model.Ports.splice(index, 1); + }; + + $scope.create = function () { + var model = $scope.model; + var subscriptionId = $scope.state.selectedSubscription.Id; + var resourceGroupName = $scope.state.selectedResourceGroup.Name; + + $scope.state.actionInProgress = true; + AzureService.createContainerGroup(model, subscriptionId, resourceGroupName) + .then(function success() { + Notifications.success('Container successfully created', model.Name); + $state.go('azure.containerinstances'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create container'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + }; + + function updateResourceGroupsAndLocations(subscription, resourceGroups, providers) { + $scope.state.selectedResourceGroup = resourceGroups[subscription.Id][0]; + $scope.resourceGroups = resourceGroups[subscription.Id]; + + var currentSubLocations = providers[subscription.Id].Locations; + $scope.model.Location = currentSubLocations[0]; + $scope.locations = currentSubLocations; + } + + function initView() { + var model = new ContainerGroupDefaultModel(); + + AzureService.subscriptions() + .then(function success(data) { + var subscriptions = data; + $scope.state.selectedSubscription = subscriptions[0]; + $scope.subscriptions = subscriptions; + + return $q.all({ + resourceGroups: AzureService.resourceGroups(subscriptions), + containerInstancesProviders: AzureService.containerInstanceProvider(subscriptions), + }); + }) + .then(function success(data) { + var resourceGroups = data.resourceGroups; + allResourceGroups = resourceGroups; + + var containerInstancesProviders = data.containerInstancesProviders; + allProviders = containerInstancesProviders; + + $scope.model = model; + + var selectedSubscription = $scope.state.selectedSubscription; + updateResourceGroupsAndLocations(selectedSubscription, resourceGroups, containerInstancesProviders); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve Azure resources'); + }); + } + + initView(); + }, +]); diff --git a/app/azure/views/containerinstances/create/createcontainerinstance.html b/app/azure/views/containerinstances/create/createcontainerinstance.html new file mode 100644 index 000000000..625e50bc5 --- /dev/null +++ b/app/azure/views/containerinstances/create/createcontainerinstance.html @@ -0,0 +1,167 @@ + + + Container instances > Add container + + +
+
+ + +
+
+ Azure settings +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ +
+ Container configuration +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+ + + map additional port + +
+ +
+
+ +
+ host + +
+ + + + + +
+ container + +
+ + +
+
+ + +
+ +
+ +
+
+ +
+ + +
+
+ + +
+
+ +
+ Container resources +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/app/azure/views/dashboard/dashboard.html b/app/azure/views/dashboard/dashboard.html new file mode 100644 index 000000000..eaa60a53e --- /dev/null +++ b/app/azure/views/dashboard/dashboard.html @@ -0,0 +1,33 @@ + + + Dashboard + + + diff --git a/app/azure/views/dashboard/dashboardController.js b/app/azure/views/dashboard/dashboardController.js new file mode 100644 index 000000000..643f900a7 --- /dev/null +++ b/app/azure/views/dashboard/dashboardController.js @@ -0,0 +1,23 @@ +angular.module('portainer.azure').controller('AzureDashboardController', [ + '$scope', + 'AzureService', + 'Notifications', + function ($scope, AzureService, Notifications) { + function initView() { + AzureService.subscriptions() + .then(function success(data) { + var subscriptions = data; + $scope.subscriptions = subscriptions; + return AzureService.resourceGroups(subscriptions); + }) + .then(function success(data) { + $scope.resourceGroups = AzureService.aggregate(data); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to load dashboard data'); + }); + } + + initView(); + }, +]); diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html index c37b1fa86..de88bfa12 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html @@ -1,6 +1,6 @@
-
+
+ + +
@@ -87,6 +97,29 @@
+
+
+ Information +
+
+
+ +

This feature is experimental.

+

+ Connect to Microsoft Azure to manage Azure Container Instances (ACI). +

+

+ + Have a look at + the Azure documentation + to retrieve the credentials required below. +

+
+
+
+
Environment details
@@ -198,6 +231,76 @@
+ +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ +
+ @@ -264,6 +367,17 @@ Add endpoint Creating endpoint... + diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index ca4ca5ca7..367ace6f8 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -118,7 +118,7 @@ -
+
+
Metadata diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 85d6c9bcb..561f38893 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -109,6 +109,9 @@ angular TLSCACert: TLSSkipVerify || securityData.TLSCACert === endpoint.TLSConfig.TLSCACert ? null : securityData.TLSCACert, TLSCert: TLSSkipClientVerify || securityData.TLSCert === endpoint.TLSConfig.TLSCert ? null : securityData.TLSCert, TLSKey: TLSSkipClientVerify || securityData.TLSKey === endpoint.TLSConfig.TLSKey ? null : securityData.TLSKey, + AzureApplicationID: endpoint.AzureCredentials.ApplicationID, + AzureTenantID: endpoint.AzureCredentials.TenantID, + AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey, }; if ($scope.endpointType !== 'local' && endpoint.Type !== 3) { diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index 4322c0f30..af41523f7 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -26,7 +26,9 @@ angular }; $scope.goToDashboard = function (endpoint) { - if (endpoint.Type === 4) { + if (endpoint.Type === 3) { + return switchToAzureEndpoint(endpoint); + } else if (endpoint.Type === 4) { return switchToEdgeEndpoint(endpoint); } @@ -87,6 +89,19 @@ angular return deferred.promise; } + function switchToAzureEndpoint(endpoint) { + EndpointProvider.setEndpointID(endpoint.Id); + EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + EndpointProvider.setOfflineModeFromStatus(endpoint.Status); + StateManager.updateEndpointState(endpoint, []) + .then(function success() { + $state.go('azure.dashboard'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint'); + }); + } + function switchToEdgeEndpoint(endpoint) { if (!endpoint.EdgeID) { $state.go('portainer.endpoints.endpoint', { id: endpoint.Id }); diff --git a/app/portainer/views/init/endpoint/initEndpoint.html b/app/portainer/views/init/endpoint/initEndpoint.html index 30310d5bd..c67f678b6 100644 --- a/app/portainer/views/init/endpoint/initEndpoint.html +++ b/app/portainer/views/init/endpoint/initEndpoint.html @@ -55,6 +55,16 @@

Connect to a Portainer agent

+
+ + +
@@ -158,6 +168,91 @@ + +
+
+ Information +
+
+
+ +

This feature is experimental.

+

+ Connect to Microsoft Azure to manage Azure Container Instances (ACI). +

+

+ + Have a look at + the Azure documentation + to retrieve the credentials required below. +

+
+
+
+
+ Environment +
+ +
+ +
+ +
+
+ +
+ Azure credentials +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+ +
+
+ +
+
diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index 3c41c60af..fc4ff905a 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -28,6 +28,9 @@ angular.module('portainer.app').controller('InitEndpointController', [ TLSCACert: null, TLSCert: null, TLSKey: null, + AzureApplicationId: '', + AzureTenantId: '', + AzureAuthenticationKey: '', }; $scope.createLocalEndpoint = function () { @@ -44,6 +47,15 @@ angular.module('portainer.app').controller('InitEndpointController', [ }); }; + $scope.createAzureEndpoint = function () { + var name = $scope.formValues.Name; + var applicationId = $scope.formValues.AzureApplicationId; + var tenantId = $scope.formValues.AzureTenantId; + var authenticationKey = $scope.formValues.AzureAuthenticationKey; + + createAzureEndpoint(name, applicationId, tenantId, authenticationKey); + }; + $scope.createAgentEndpoint = function () { var name = $scope.formValues.Name; var URL = $scope.formValues.URL; @@ -66,6 +78,20 @@ angular.module('portainer.app').controller('InitEndpointController', [ createRemoteEndpoint(name, 1, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); }; + function createAzureEndpoint(name, applicationId, tenantId, authenticationKey) { + $scope.state.actionInProgress = true; + EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, []) + .then(function success() { + $state.go('portainer.home'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to connect to the Azure environment'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + } + function createRemoteEndpoint(name, type, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { $scope.state.actionInProgress = true; EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 3ed2e8d7f..6e57ac2c5 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -13,8 +13,9 @@ Home + Date: Tue, 9 Jun 2020 12:55:36 +0300 Subject: [PATCH 027/195] feat(auth): add custom user timeout (#3871) * feat(auth): introduce new timeout constant * feat(auth): pass timeout from handler * feat(auth): add timeout selector to auth settings view * feat(settings): add user session timeout property * feat(auth): load user session timeout from settings * fix(settings): use correct time format * feat(auth): remove no-auth flag * refactor(auth): move timeout mgmt to jwt service * refactor(client): remove no-auth checks from client * refactor(cli): remove defaultNoAuth * feat(settings): create settings with default user timeout value * refactor(db): save user session timeout always * refactor(jwt): return error * feat(auth): set session timeout in jwt service on update * feat(auth): add description and time settings * feat(auth): parse duration * feat(settings): validate user timeout format * refactor(settings): remove unneccesary import --- api/bolt/init.go | 1 + api/bolt/migrator/migrate_dbversion23.go | 6 +- api/cli/cli.go | 11 +--- api/cli/defaults.go | 1 - api/cli/defaults_windows.go | 1 - api/cmd/portainer/main.go | 35 +++++----- api/http/handler/auth/authenticate.go | 4 -- api/http/handler/auth/handler.go | 12 +--- api/http/handler/settings/handler.go | 5 +- api/http/handler/settings/settings_update.go | 17 +++++ api/http/security/bouncer.go | 64 ++++++++----------- api/http/server.go | 10 +-- api/jwt/jwt.go | 19 +++++- api/portainer.go | 10 +-- .../configs-datatable/configsDatatable.html | 4 +- .../configs-datatable/configsDatatable.js | 1 - .../containersDatatable.html | 4 +- .../containersDatatable.js | 1 - .../networkRowContent.html | 2 +- .../networks-datatable/networksDatatable.html | 2 +- .../networks-datatable/networksDatatable.js | 1 - .../secrets-datatable/secretsDatatable.html | 4 +- .../secrets-datatable/secretsDatatable.js | 1 - .../services-datatable/servicesDatatable.html | 4 +- .../services-datatable/servicesDatatable.js | 1 - .../volumes-datatable/volumesDatatable.html | 4 +- .../volumes-datatable/volumesDatatable.js | 1 - .../networkMacvlanForm.html | 2 +- .../networkMacvlanFormController.js | 7 +- app/docker/views/configs/configs.html | 1 - .../views/configs/create/createconfig.html | 2 +- app/docker/views/configs/edit/config.html | 3 +- app/docker/views/containers/containers.html | 1 - .../containers/create/createcontainer.html | 6 +- .../views/containers/edit/container.html | 8 +-- .../views/networks/create/createnetwork.html | 2 +- app/docker/views/networks/edit/network.html | 2 +- app/docker/views/networks/networks.html | 1 - .../views/secrets/create/createsecret.html | 2 +- app/docker/views/secrets/edit/secret.html | 3 +- app/docker/views/secrets/secrets.html | 1 - .../views/services/create/createservice.html | 2 +- app/docker/views/services/edit/service.html | 9 +-- app/docker/views/services/services.html | 1 - app/docker/views/swarm/swarm.html | 2 +- app/docker/views/swarm/swarmController.js | 4 +- .../views/volumes/create/createvolume.html | 10 +-- app/docker/views/volumes/edit/volume.html | 3 +- app/docker/views/volumes/volumes.html | 1 - .../registryRepositoriesController.js | 5 +- app/portainer/__module.js | 4 +- .../endpointsDatatable.html | 2 +- .../endpoints-datatable/endpointsDatatable.js | 1 - .../groups-datatable/groupsDatatable.html | 2 +- .../groups-datatable/groupsDatatable.js | 1 - .../stacks-datatable/stacksDatatable.html | 4 +- .../stacks-datatable/stacksDatatable.js | 1 - app/portainer/models/settings.js | 1 + app/portainer/services/stateManager.js | 1 - app/portainer/views/auth/authController.js | 13 ++-- app/portainer/views/endpoints/endpoints.html | 1 - app/portainer/views/groups/groups.html | 10 +-- app/portainer/views/home/home.html | 2 +- .../views/registries/registries.html | 4 +- .../views/registries/registriesController.js | 5 +- .../settingsAuthentication.html | 22 +++++++ .../settingsAuthenticationController.js | 23 +++++++ app/portainer/views/sidebar/sidebar.html | 15 ++--- .../views/sidebar/sidebarController.js | 23 +++---- .../views/stacks/create/createstack.html | 2 +- app/portainer/views/stacks/edit/stack.html | 4 +- app/portainer/views/stacks/stacks.html | 1 - app/portainer/views/templates/templates.html | 4 +- 73 files changed, 214 insertions(+), 236 deletions(-) diff --git a/api/bolt/init.go b/api/bolt/init.go index cc0a0f261..53fb28044 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -27,6 +27,7 @@ func (store *Store) Init() error { EnableHostManagementFeatures: false, EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, TemplatesURL: portainer.DefaultTemplatesURL, + UserSessionTimeout: portainer.DefaultUserSessionTimeout, } err = store.SettingsService.UpdateSettings(defaultSettings) diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go index 2688eecd5..fe4deca48 100644 --- a/api/bolt/migrator/migrate_dbversion23.go +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -10,9 +10,9 @@ func (m *Migrator) updateSettingsToDB24() error { if legacySettings.TemplatesURL == "" { legacySettings.TemplatesURL = portainer.DefaultTemplatesURL - - return m.settingsService.UpdateSettings(legacySettings) } - return nil + legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout + + return m.settingsService.UpdateSettings(legacySettings) } diff --git a/api/cli/cli.go b/api/cli/cli.go index 914f01cb4..13e20e94b 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -1,7 +1,6 @@ package cli import ( - "log" "time" "github.com/portainer/portainer/api" @@ -20,7 +19,6 @@ const ( errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://") errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe") errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval") - errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file") errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file") ) @@ -35,7 +33,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), - NoAuth: kingpin.Flag("no-auth", "Disable authentication (deprecated)").Default(defaultNoAuth).Bool(), NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(), TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(), @@ -81,10 +78,6 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { return err } - if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") { - return errNoAuthExcludeAdminPassword - } - if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" { return errAdminPassExcludeAdminPassFile } @@ -93,9 +86,7 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { } func displayDeprecationWarnings(flags *portainer.CLIFlags) { - if *flags.NoAuth { - log.Println("Warning: the --no-auth flag is deprecated and will likely be removed in a future version of Portainer.") - } + } func validateEndpointURL(endpointURL string) error { diff --git a/api/cli/defaults.go b/api/cli/defaults.go index b79cddc5a..47e88755f 100644 --- a/api/cli/defaults.go +++ b/api/cli/defaults.go @@ -8,7 +8,6 @@ const ( defaultTunnelServerPort = "8000" defaultDataDirectory = "/data" defaultAssetsDirectory = "./" - defaultNoAuth = "false" defaultNoAnalytics = "false" defaultTLS = "false" defaultTLSSkipVerify = "false" diff --git a/api/cli/defaults_windows.go b/api/cli/defaults_windows.go index 2d1f2b2ab..bf42086ff 100644 --- a/api/cli/defaults_windows.go +++ b/api/cli/defaults_windows.go @@ -6,7 +6,6 @@ const ( defaultTunnelServerPort = "8000" defaultDataDirectory = "C:\\data" defaultAssetsDirectory = "./" - defaultNoAuth = "false" defaultNoAnalytics = "false" defaultTLS = "false" defaultTLSSkipVerify = "false" diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 34eed9cef..0598b0c6a 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -77,15 +77,17 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService) } -func initJWTService(authenticationEnabled bool) portainer.JWTService { - if authenticationEnabled { - jwtService, err := jwt.NewService() - if err != nil { - log.Fatal(err) - } - return jwtService +func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) { + settings, err := dataStore.Settings().Settings() + if err != nil { + return nil, err } - return nil + + jwtService, err := jwt.NewService(settings.UserSessionTimeout) + if err != nil { + return nil, err + } + return jwtService, nil } func initDigitalSignatureService() portainer.DigitalSignatureService { @@ -188,9 +190,8 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p func initStatus(flags *portainer.CLIFlags) *portainer.Status { return &portainer.Status{ - Analytics: !*flags.NoAnalytics, - Authentication: !*flags.NoAuth, - Version: portainer.APIVersion, + Analytics: !*flags.NoAnalytics, + Version: portainer.APIVersion, } } @@ -392,7 +393,10 @@ func main() { dataStore := initDataStore(*flags.Data, fileService) defer dataStore.Close() - jwtService := initJWTService(!*flags.NoAuth) + jwtService, err := initJWTService(dataStore) + if err != nil { + log.Fatal(err) + } ldapService := initLDAPService() @@ -402,7 +406,7 @@ func main() { digitalSignatureService := initDigitalSignatureService() - err := initKeyPair(fileService, digitalSignatureService) + err = initKeyPair(fileService, digitalSignatureService) if err != nil { log.Fatal(err) } @@ -492,9 +496,7 @@ func main() { } } - if !*flags.NoAuth { - go terminateIfNoAdminCreated(dataStore) - } + go terminateIfNoAdminCreated(dataStore) err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter) if err != nil { @@ -506,7 +508,6 @@ func main() { Status: applicationStatus, BindAddress: *flags.Addr, AssetsPath: *flags.Assets, - AuthDisabled: *flags.NoAuth, DataStore: dataStore, SwarmStackManager: swarmStackManager, ComposeStackManager: composeStackManager, diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index 1db970bb3..6a1d47308 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -32,10 +32,6 @@ func (payload *authenticatePayload) Validate(r *http.Request) error { } func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - if handler.authDisabled { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Cannot authenticate user. Portainer was started with the --no-auth flag", ErrAuthDisabled} - } - var payload authenticatePayload err := request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index 345884980..bfb33c6f9 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -10,16 +10,9 @@ import ( "github.com/portainer/portainer/api/http/security" ) -const ( - // ErrAuthDisabled is an error raised when trying to access the authentication endpoints - // when the server has been started with the --no-auth flag - ErrAuthDisabled = portainer.Error("Authentication is disabled") -) - // Handler is the HTTP handler used to handle authentication operations. type Handler struct { *mux.Router - authDisabled bool DataStore portainer.DataStore CryptoService portainer.CryptoService JWTService portainer.JWTService @@ -29,10 +22,9 @@ type Handler struct { } // NewHandler creates a handler to manage authentication operations. -func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, authDisabled bool) *Handler { +func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter) *Handler { h := &Handler{ - Router: mux.NewRouter(), - authDisabled: authDisabled, + Router: mux.NewRouter(), } h.Handle("/auth/oauth/validate", diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index 349d05228..9bbe1cf52 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -17,11 +17,12 @@ func hideFields(settings *portainer.Settings) { // Handler is the HTTP handler used to handle settings operations. type Handler struct { *mux.Router + AuthorizationService *portainer.AuthorizationService DataStore portainer.DataStore - LDAPService portainer.LDAPService FileService portainer.FileService JobScheduler portainer.JobScheduler - AuthorizationService *portainer.AuthorizationService + JWTService portainer.JWTService + LDAPService portainer.LDAPService } // NewHandler creates a handler to manage settings operations. diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 86d26bdaa..81c1aa249 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -2,6 +2,7 @@ package settings import ( "net/http" + "time" "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" @@ -25,6 +26,7 @@ type settingsUpdatePayload struct { TemplatesURL *string EdgeAgentCheckinInterval *int EnableEdgeComputeFeatures *bool + UserSessionTimeout *string } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -37,6 +39,13 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error { if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) { return portainer.Error("Invalid external templates URL. Must correspond to a valid URL format") } + if payload.UserSessionTimeout != nil { + _, err := time.ParseDuration(*payload.UserSessionTimeout) + if err != nil { + return portainer.Error("Invalid user session timeout") + } + } + return nil } @@ -125,6 +134,14 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval } + if payload.UserSessionTimeout != nil { + settings.UserSessionTimeout = *payload.UserSessionTimeout + + userSessionDuration, _ := time.ParseDuration(*payload.UserSessionTimeout) + + handler.JWTService.SetUserSessionDuration(userSessionDuration) + } + tlsError := handler.updateTLS(settings) if tlsError != nil { return tlsError diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index d6ded7f62..3fc61656a 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -16,7 +16,6 @@ type ( dataStore portainer.DataStore jwtService portainer.JWTService rbacExtensionClient *rbacExtensionClient - authDisabled bool } // RestrictedRequestContext is a data structure containing information @@ -30,12 +29,11 @@ type ( ) // NewRequestBouncer initializes a new RequestBouncer -func NewRequestBouncer(dataStore portainer.DataStore, jwtService portainer.JWTService, authenticationDisabled bool, rbacExtensionURL string) *RequestBouncer { +func NewRequestBouncer(dataStore portainer.DataStore, jwtService portainer.JWTService, rbacExtensionURL string) *RequestBouncer { return &RequestBouncer{ dataStore: dataStore, jwtService: jwtService, rbacExtensionClient: newRBACExtensionClient(rbacExtensionURL), - authDisabled: authenticationDisabled, } } @@ -289,44 +287,38 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var tokenData *portainer.TokenData - if !bouncer.authDisabled { - var token string + var token string - // Optionally, token might be set via the "token" query parameter. - // For example, in websocket requests - token = r.URL.Query().Get("token") + // Optionally, token might be set via the "token" query parameter. + // For example, in websocket requests + token = r.URL.Query().Get("token") - // Get token from the Authorization header - tokens, ok := r.Header["Authorization"] - if ok && len(tokens) >= 1 { - token = tokens[0] - token = strings.TrimPrefix(token, "Bearer ") - } + // Get token from the Authorization header + tokens, ok := r.Header["Authorization"] + if ok && len(tokens) >= 1 { + token = tokens[0] + token = strings.TrimPrefix(token, "Bearer ") + } - if token == "" { - httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) - return - } + if token == "" { + httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) + return + } - var err error - tokenData, err = bouncer.jwtService.ParseAndVerifyToken(token) - if err != nil { - httperror.WriteError(w, http.StatusUnauthorized, "Invalid JWT token", err) - return - } + var err error + tokenData, err = bouncer.jwtService.ParseAndVerifyToken(token) + if err != nil { + httperror.WriteError(w, http.StatusUnauthorized, "Invalid JWT token", err) + return + } - _, err = bouncer.dataStore.User().User(tokenData.ID) - if err != nil && err == portainer.ErrObjectNotFound { - httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) - return - } else if err != nil { - httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user details from the database", err) - return - } - } else { - tokenData = &portainer.TokenData{ - Role: portainer.AdministratorRole, - } + _, err = bouncer.dataStore.User().User(tokenData.ID) + if err != nil && err == portainer.ErrObjectNotFound { + httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) + return + } else if err != nil { + httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user details from the database", err) + return } ctx := storeTokenData(r, tokenData) diff --git a/api/http/server.go b/api/http/server.go index 40e2a880f..1834c4100 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -47,7 +47,6 @@ import ( type Server struct { BindAddress string AssetsPath string - AuthDisabled bool Status *portainer.Status ReverseTunnelService portainer.ReverseTunnelService ExtensionManager portainer.ExtensionManager @@ -77,11 +76,11 @@ func (server *Server) Start() error { authorizationService := portainer.NewAuthorizationService(server.DataStore) rbacExtensionURL := proxyManager.GetExtensionURL(portainer.RBACExtension) - requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService, server.AuthDisabled, rbacExtensionURL) + requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService, rbacExtensionURL) rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour) - var authHandler = auth.NewHandler(requestBouncer, rateLimiter, server.AuthDisabled) + var authHandler = auth.NewHandler(requestBouncer, rateLimiter) authHandler.DataStore = server.DataStore authHandler.CryptoService = server.CryptoService authHandler.JWTService = server.JWTService @@ -153,11 +152,12 @@ func (server *Server) Start() error { schedulesHandler.ReverseTunnelService = server.ReverseTunnelService var settingsHandler = settings.NewHandler(requestBouncer) + settingsHandler.AuthorizationService = authorizationService settingsHandler.DataStore = server.DataStore - settingsHandler.LDAPService = server.LDAPService settingsHandler.FileService = server.FileService settingsHandler.JobScheduler = server.JobScheduler - settingsHandler.AuthorizationService = authorizationService + settingsHandler.JWTService = server.JWTService + settingsHandler.LDAPService = server.LDAPService var stackHandler = stacks.NewHandler(requestBouncer) stackHandler.DataStore = server.DataStore diff --git a/api/jwt/jwt.go b/api/jwt/jwt.go index 316495724..aedb264de 100644 --- a/api/jwt/jwt.go +++ b/api/jwt/jwt.go @@ -12,7 +12,8 @@ import ( // Service represents a service for managing JWT tokens. type Service struct { - secret []byte + secret []byte + userSessionTimeout time.Duration } type claims struct { @@ -23,20 +24,27 @@ type claims struct { } // NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens. -func NewService() (*Service, error) { +func NewService(userSessionDuration string) (*Service, error) { + userSessionTimeout, err := time.ParseDuration(userSessionDuration) + if err != nil { + return nil, err + } + secret := securecookie.GenerateRandomKey(32) if secret == nil { return nil, portainer.ErrSecretGeneration } + service := &Service{ secret, + userSessionTimeout, } return service, nil } // GenerateToken generates a new JWT token. func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) { - expireToken := time.Now().Add(time.Hour * 8).Unix() + expireToken := time.Now().Add(service.userSessionTimeout).Unix() cl := claims{ UserID: int(data.ID), Username: data.Username, @@ -77,3 +85,8 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, return nil, portainer.ErrInvalidJWTToken } + +// SetUserSessionDuration sets the user session duration +func (service *Service) SetUserSessionDuration(userSessionDuration time.Duration) { + service.userSessionTimeout = userSessionDuration +} diff --git a/api/portainer.go b/api/portainer.go index a2ec3efd6..0f3b3fed0 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -46,7 +46,6 @@ type ( EndpointURL *string Labels *[]Pair Logo *string - NoAuth *bool NoAnalytics *bool Templates *string TLS *bool @@ -459,6 +458,7 @@ type ( EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + UserSessionTimeout string `json:"UserSessionTimeout"` // Deprecated fields DisplayDonationHeader bool @@ -517,9 +517,8 @@ type ( // Status represents the application status Status struct { - Authentication bool `json:"Authentication"` - Analytics bool `json:"Analytics"` - Version string `json:"Version"` + Analytics bool `json:"Analytics"` + Version string `json:"Version"` } // Tag represents a tag that can be associated to a resource @@ -842,6 +841,7 @@ type ( JWTService interface { GenerateToken(data *TokenData) (string, error) ParseAndVerifyToken(token string) (*TokenData, error) + SetUserSessionDuration(userSessionDuration time.Duration) } // LDAPService represents a service used to authenticate users against a LDAP/AD @@ -1057,6 +1057,8 @@ const ( LocalExtensionManifestFile = "/extensions.json" // DefaultTemplatesURL represents the URL to the official templates supported by Portainer DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json" + // DefaultUserSessionTimeout represents the default timeout after which the user session is cleared + DefaultUserSessionTimeout = "8h" ) const ( diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.html b/app/docker/components/datatables/configs-datatable/configsDatatable.html index a6b8aa6a7..8939b44be 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.html +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.html @@ -90,7 +90,7 @@ - + Ownership @@ -112,7 +112,7 @@ {{ item.Name }} {{ item.CreatedAt | getisodate }} - + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.js b/app/docker/components/datatables/configs-datatable/configsDatatable.js index 0e75d7013..569281fcc 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.js +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.js @@ -8,7 +8,6 @@ angular.module('portainer.docker').component('configsDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - showOwnershipColumn: '<', removeAction: '<', refreshCallback: '<', }, diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 44aa27c7e..819fc8c0b 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -246,7 +246,7 @@ - + Ownership @@ -319,7 +319,7 @@ - - + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.js b/app/docker/components/datatables/containers-datatable/containersDatatable.js index e136cf191..0edc97de0 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.js +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.js @@ -8,7 +8,6 @@ angular.module('portainer.docker').component('containersDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - showOwnershipColumn: '<', showHostColumn: '<', showAddAction: '<', offlineMode: '<', diff --git a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html index 04fb4d94d..b2152f746 100644 --- a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html +++ b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html @@ -28,7 +28,7 @@ {{ item.IPAM.IPV6Configs[0].Subnet ? item.IPAM.IPV6Configs[0].Subnet : '-' }} {{ item.IPAM.IPV6Configs[0].Gateway ? item.IPAM.IPV6Configs[0].Gateway : '-' }} {{ item.NodeName ? item.NodeName : '-' }} - + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.html b/app/docker/components/datatables/networks-datatable/networksDatatable.html index 0c8f1e905..bf54ca837 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.html +++ b/app/docker/components/datatables/networks-datatable/networksDatatable.html @@ -151,7 +151,7 @@ - + Ownership diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.js b/app/docker/components/datatables/networks-datatable/networksDatatable.js index a46961009..20d48108b 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.js +++ b/app/docker/components/datatables/networks-datatable/networksDatatable.js @@ -8,7 +8,6 @@ angular.module('portainer.docker').component('networksDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - showOwnershipColumn: '<', showHostColumn: '<', removeAction: '<', offlineMode: '<', diff --git a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html index e804e738a..f7d9b0c6e 100644 --- a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html +++ b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html @@ -90,7 +90,7 @@ - + Ownership @@ -112,7 +112,7 @@ {{ item.Name }} {{ item.CreatedAt | getisodate }} - + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/secrets-datatable/secretsDatatable.js b/app/docker/components/datatables/secrets-datatable/secretsDatatable.js index c3f0c0ed7..840a98fc2 100644 --- a/app/docker/components/datatables/secrets-datatable/secretsDatatable.js +++ b/app/docker/components/datatables/secrets-datatable/secretsDatatable.js @@ -8,7 +8,6 @@ angular.module('portainer.docker').component('secretsDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - showOwnershipColumn: '<', removeAction: '<', refreshCallback: '<', }, diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html index c5fbc3dff..3e72d09f4 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.html +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html @@ -115,7 +115,7 @@ - + Ownership @@ -180,7 +180,7 @@ - {{ item.UpdatedAt | getisodate }} - + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.js b/app/docker/components/datatables/services-datatable/servicesDatatable.js index 7f8731892..6e5bbfb77 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.js +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.js @@ -10,7 +10,6 @@ angular.module('portainer.docker').component('servicesDatatable', { reverseOrder: '<', nodes: '<', agentProxy: '<', - showOwnershipColumn: '<', showUpdateAction: '<', showAddAction: '<', showStackColumn: '<', diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html index 12a9c776f..573acd415 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html @@ -142,7 +142,7 @@ - + Ownership @@ -177,7 +177,7 @@ {{ item.Mountpoint | truncatelr }} {{ item.CreatedAt | getisodate }} {{ item.NodeName ? item.NodeName : '-' }} - + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.js b/app/docker/components/datatables/volumes-datatable/volumesDatatable.js index af00b13ce..0ee1524e3 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.js +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.js @@ -8,7 +8,6 @@ angular.module('portainer.docker').component('volumesDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - showOwnershipColumn: '<', showHostColumn: '<', removeAction: '<', showBrowseAction: '<', diff --git a/app/docker/components/network-macvlan-form/networkMacvlanForm.html b/app/docker/components/network-macvlan-form/networkMacvlanForm.html index fca929aa5..59898e2ee 100644 --- a/app/docker/components/network-macvlan-form/networkMacvlanForm.html +++ b/app/docker/components/network-macvlan-form/networkMacvlanForm.html @@ -74,7 +74,7 @@ state="$ctrl.data.DatatableState" order-by="Hostname" show-ip-address-column="$ctrl.applicationState.endpoint.apiVersion >= 1.25" - access-to-node-details="!$ctrl.applicationState.application.authentication || $ctrl.isAdmin" + access-to-node-details="$ctrl.isAdmin" name="node_selector" ng-model="tmp" ng-required="$ctrl.requiredNodeSelection()" diff --git a/app/docker/components/network-macvlan-form/networkMacvlanFormController.js b/app/docker/components/network-macvlan-form/networkMacvlanFormController.js index 8a6fa0084..e3873648a 100644 --- a/app/docker/components/network-macvlan-form/networkMacvlanFormController.js +++ b/app/docker/components/network-macvlan-form/networkMacvlanFormController.js @@ -23,10 +23,9 @@ angular.module('portainer.docker').controller('NetworkMacvlanFormController', [ }; function initComponent() { - if (StateManager.getState().application.authentication) { - var isAdmin = Authentication.isAdmin(); - ctrl.isAdmin = isAdmin; - } + var isAdmin = Authentication.isAdmin(); + ctrl.isAdmin = isAdmin; + var provider = ctrl.applicationState.endpoint.mode.provider; var apiVersion = ctrl.applicationState.endpoint.apiVersion; $q.all({ diff --git a/app/docker/views/configs/configs.html b/app/docker/views/configs/configs.html index 0145c737e..ec47421e7 100644 --- a/app/docker/views/configs/configs.html +++ b/app/docker/views/configs/configs.html @@ -15,7 +15,6 @@ dataset="ctrl.configs" table-key="configs" order-by="Name" - show-ownership-column="applicationState.application.authentication" remove-action="ctrl.removeAction" refresh-callback="ctrl.getConfigs" > diff --git a/app/docker/views/configs/create/createconfig.html b/app/docker/views/configs/create/createconfig.html index e92186639..06e0af527 100644 --- a/app/docker/views/configs/create/createconfig.html +++ b/app/docker/views/configs/create/createconfig.html @@ -57,7 +57,7 @@
- +
diff --git a/app/docker/views/configs/edit/config.html b/app/docker/views/configs/edit/config.html index d11a48616..591ae9b8a 100644 --- a/app/docker/views/configs/edit/config.html +++ b/app/docker/views/configs/edit/config.html @@ -59,8 +59,7 @@
- - +
diff --git a/app/docker/views/containers/containers.html b/app/docker/views/containers/containers.html index 98d246a89..b1ba1e791 100644 --- a/app/docker/views/containers/containers.html +++ b/app/docker/views/containers/containers.html @@ -15,7 +15,6 @@ dataset="containers" table-key="containers" order-by="Status" - show-ownership-column="applicationState.application.authentication" show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" show-add-action="true" offline-mode="offlineMode" diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index f339a9090..e6d1ea848 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -126,11 +126,7 @@
- +
diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index b8e9ff0a2..41d8349f3 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -135,13 +135,7 @@
- - +
diff --git a/app/docker/views/networks/create/createnetwork.html b/app/docker/views/networks/create/createnetwork.html index 21877f025..7cfc402c8 100644 --- a/app/docker/views/networks/create/createnetwork.html +++ b/app/docker/views/networks/create/createnetwork.html @@ -194,7 +194,7 @@
- +
diff --git a/app/docker/views/networks/edit/network.html b/app/docker/views/networks/edit/network.html index 9adcba96b..bf4a04170 100644 --- a/app/docker/views/networks/edit/network.html +++ b/app/docker/views/networks/edit/network.html @@ -66,7 +66,7 @@ - +
diff --git a/app/docker/views/secrets/edit/secret.html b/app/docker/views/secrets/edit/secret.html index 6375294eb..daa5bc912 100644 --- a/app/docker/views/secrets/edit/secret.html +++ b/app/docker/views/secrets/edit/secret.html @@ -56,6 +56,5 @@
- - + diff --git a/app/docker/views/secrets/secrets.html b/app/docker/views/secrets/secrets.html index a6ddde5af..f668ec70e 100644 --- a/app/docker/views/secrets/secrets.html +++ b/app/docker/views/secrets/secrets.html @@ -15,7 +15,6 @@ dataset="secrets" table-key="secrets" order-by="Name" - show-ownership-column="applicationState.application.authentication" remove-action="removeAction" refresh-callback="getSecrets" > diff --git a/app/docker/views/services/create/createservice.html b/app/docker/views/services/create/createservice.html index 3494a88f0..639274cc8 100644 --- a/app/docker/views/services/create/createservice.html +++ b/app/docker/views/services/create/createservice.html @@ -112,7 +112,7 @@
- +
diff --git a/app/docker/views/services/edit/service.html b/app/docker/views/services/edit/service.html index 37579731a..82cc85bca 100644 --- a/app/docker/views/services/edit/service.html +++ b/app/docker/views/services/edit/service.html @@ -99,7 +99,6 @@

Note: you can only rollback one level of changes. Clicking the rollback button without making a new change will undo your previous rollback

- - +

diff --git a/app/docker/views/swarm/swarmController.js b/app/docker/views/swarm/swarmController.js index 9a5b4c08b..ff4f7c409 100644 --- a/app/docker/views/swarm/swarmController.js +++ b/app/docker/views/swarm/swarmController.js @@ -82,9 +82,7 @@ angular.module('portainer.docker').controller('SwarmController', [ } function initView() { - if (StateManager.getState().application.authentication) { - $scope.isAdmin = Authentication.isAdmin(); - } + $scope.isAdmin = Authentication.isAdmin(); var provider = $scope.applicationState.endpoint.mode.provider; $q.all({ diff --git a/app/docker/views/volumes/create/createvolume.html b/app/docker/views/volumes/create/createvolume.html index d3985b68c..2dc4ef6a4 100644 --- a/app/docker/views/volumes/create/createvolume.html +++ b/app/docker/views/volumes/create/createvolume.html @@ -72,9 +72,7 @@ -
- Ensure nfs-utils are installed on your hosts. -
+
Ensure nfs-utils are installed on your hosts.
@@ -87,9 +85,7 @@ -
- Ensure cifs-utils are installed on your hosts. -
+
Ensure cifs-utils are installed on your hosts.
@@ -110,7 +106,7 @@ - +
diff --git a/app/docker/views/volumes/edit/volume.html b/app/docker/views/volumes/edit/volume.html index 3d9fcf342..d55d317be 100644 --- a/app/docker/views/volumes/edit/volume.html +++ b/app/docker/views/volumes/edit/volume.html @@ -78,8 +78,7 @@
- - +
diff --git a/app/docker/views/volumes/volumes.html b/app/docker/views/volumes/volumes.html index c43c8a430..f3c860390 100644 --- a/app/docker/views/volumes/volumes.html +++ b/app/docker/views/volumes/volumes.html @@ -16,7 +16,6 @@ table-key="volumes" order-by="Id" remove-action="removeAction" - show-ownership-column="applicationState.application.authentication" show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'" show-browse-action="showBrowseAction" offline-mode="offlineMode" diff --git a/app/extensions/registry-management/views/repositories/registryRepositoriesController.js b/app/extensions/registry-management/views/repositories/registryRepositoriesController.js index 3dbbd7a2b..00ad8b0cb 100644 --- a/app/extensions/registry-management/views/repositories/registryRepositoriesController.js +++ b/app/extensions/registry-management/views/repositories/registryRepositoriesController.js @@ -42,10 +42,7 @@ angular.module('portainer.extensions.registrymanagement').controller('RegistryRe function initView() { const registryId = $transition$.params().id; - var authenticationEnabled = $scope.applicationState.application.authentication; - if (authenticationEnabled) { - $scope.isAdmin = Authentication.isAdmin(); - } + $scope.isAdmin = Authentication.isAdmin(); RegistryService.registry(registryId) .then(function success(data) { diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 6d03c1d8f..7a7df77e9 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -55,9 +55,7 @@ angular.module('portainer.app', []).config([ if (state.application.analytics) { initAnalytics(Analytics, $rootScope); } - if (state.application.authentication) { - return $async(initAuthentication, authManager, Authentication, $rootScope, $state); - } + return $async(initAuthentication, authManager, Authentication, $rootScope, $state); }) .then(() => deferred.resolve()) .catch(function error(err) { diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html index e053c0adb..524cf1215 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html @@ -83,7 +83,7 @@ {{ item.URL | stripprotocol }} {{ item.GroupName }} - Manage access + Manage access diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js index b717fe566..807f05192 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js @@ -7,7 +7,6 @@ angular.module('portainer.app').component('endpointsDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - accessManagement: '<', removeAction: '<', retrievePage: '<', }, diff --git a/app/portainer/components/datatables/groups-datatable/groupsDatatable.html b/app/portainer/components/datatables/groups-datatable/groupsDatatable.html index 47f424735..58ed43cc0 100644 --- a/app/portainer/components/datatables/groups-datatable/groupsDatatable.html +++ b/app/portainer/components/datatables/groups-datatable/groupsDatatable.html @@ -53,7 +53,7 @@ {{ item.Name }} - Manage access + Manage access diff --git a/app/portainer/components/datatables/groups-datatable/groupsDatatable.js b/app/portainer/components/datatables/groups-datatable/groupsDatatable.js index a71931360..f8e69b55a 100644 --- a/app/portainer/components/datatables/groups-datatable/groupsDatatable.js +++ b/app/portainer/components/datatables/groups-datatable/groupsDatatable.js @@ -8,7 +8,6 @@ angular.module('portainer.app').component('groupsDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - accessManagement: '<', removeAction: '<', }, }); diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html index a4010af61..073e3db2b 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html @@ -91,7 +91,7 @@ Control - + Ownership @@ -127,7 +127,7 @@ Total - + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.js b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.js index 1e1e11c98..cdf581415 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.js +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.js @@ -8,7 +8,6 @@ angular.module('portainer.app').component('stacksDatatable', { tableKey: '@', orderBy: '@', reverseOrder: '<', - showOwnershipColumn: '<', removeAction: '<', offlineMode: '<', refreshCallback: '<', diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 8e0247db9..f7498f81b 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -12,6 +12,7 @@ export function SettingsViewModel(data) { this.EnableHostManagementFeatures = data.EnableHostManagementFeatures; this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; + this.UserSessionTimeout = data.UserSessionTimeout; } export function PublicSettingsViewModel(settings) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index fa2b1c72b..e46502334 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -77,7 +77,6 @@ angular.module('portainer.app').factory('StateManager', [ }; function assignStateFromStatusAndSettings(status, settings) { - state.application.authentication = status.Authentication; state.application.analytics = status.Analytics; state.application.version = status.Version; state.application.logo = settings.LogoURL; diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 27384fe4c..a0eb03acc 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -128,10 +128,10 @@ class AuthenticationController { } } - async checkForEndpointsAsync(noAuth) { + async checkForEndpointsAsync() { try { const endpoints = await this.EndpointService.endpoints(0, 1); - const isAdmin = noAuth || this.Authentication.isAdmin(); + const isAdmin = this.Authentication.isAdmin(); if (endpoints.value.length === 0 && isAdmin) { return this.$state.go('portainer.init.endpoint'); @@ -162,7 +162,7 @@ class AuthenticationController { async postLoginSteps() { await this.retrieveAndSaveEnabledExtensionsAsync(); - await this.checkForEndpointsAsync(false); + await this.checkForEndpointsAsync(); await this.checkForLatestVersionAsync(); } /** @@ -282,12 +282,7 @@ class AuthenticationController { } this.state.loginInProgress = false; - const authenticationEnabled = this.$scope.applicationState.application.authentication; - if (!authenticationEnabled) { - await this.checkForEndpointsAsync(true); - } else { - await this.authEnabledFlowAsync(); - } + await this.authEnabledFlowAsync(); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve public settings'); } diff --git a/app/portainer/views/endpoints/endpoints.html b/app/portainer/views/endpoints/endpoints.html index 35f0190ff..7f1813ca7 100644 --- a/app/portainer/views/endpoints/endpoints.html +++ b/app/portainer/views/endpoints/endpoints.html @@ -14,7 +14,6 @@ title-icon="fa-plug" table-key="endpoints" order-by="Name" - access-management="applicationState.application.authentication" remove-action="removeAction" retrieve-page="getPaginatedEndpoints" > diff --git a/app/portainer/views/groups/groups.html b/app/portainer/views/groups/groups.html index 1899d9e18..42e491286 100644 --- a/app/portainer/views/groups/groups.html +++ b/app/portainer/views/groups/groups.html @@ -9,14 +9,6 @@
- +
diff --git a/app/portainer/views/home/home.html b/app/portainer/views/home/home.html index 669002a5e..489254d9b 100644 --- a/app/portainer/views/home/home.html +++ b/app/portainer/views/home/home.html @@ -37,7 +37,7 @@ table-key="home_endpoints" tags="tags" dashboard-action="goToDashboard" - show-snapshot-action="!applicationState.application.authentication || isAdmin" + show-snapshot-action="isAdmin" snapshot-action="triggerSnapshot" edit-action="goToEdit" is-admin="isAdmin" diff --git a/app/portainer/views/registries/registries.html b/app/portainer/views/registries/registries.html index c6a13584e..22f89ade9 100644 --- a/app/portainer/views/registries/registries.html +++ b/app/portainer/views/registries/registries.html @@ -7,7 +7,7 @@ Registry management -
+
@@ -79,7 +79,7 @@ dataset="registries" table-key="registries" order-by="Name" - access-management="!applicationState.application.authentication || isAdmin" + access-management="isAdmin" remove-action="removeAction" registry-management="registryManagementAvailable" can-browse="canBrowse" diff --git a/app/portainer/views/registries/registriesController.js b/app/portainer/views/registries/registriesController.js index f4f6b49ea..72e9e51eb 100644 --- a/app/portainer/views/registries/registriesController.js +++ b/app/portainer/views/registries/registriesController.js @@ -81,10 +81,7 @@ angular.module('portainer.app').controller('RegistriesController', [ $scope.registries = data.registries; $scope.dockerhub = data.dockerhub; $scope.registryManagementAvailable = data.registryManagement; - var authenticationEnabled = $scope.applicationState.application.authentication; - if (authenticationEnabled) { - $scope.isAdmin = Authentication.isAdmin(); - } + $scope.isAdmin = Authentication.isAdmin(); }) .catch(function error(err) { $scope.registries = []; diff --git a/app/portainer/views/settings/authentication/settingsAuthentication.html b/app/portainer/views/settings/authentication/settingsAuthentication.html index 5270ca50b..be58b78da 100644 --- a/app/portainer/views/settings/authentication/settingsAuthentication.html +++ b/app/portainer/views/settings/authentication/settingsAuthentication.html @@ -9,6 +9,28 @@
+
+ Configuration +
+
+ +
+ +
+
+
+ + Changing from default is only recommended if you have additional layers of authentication in front of Portainer. + +
Authentication method
diff --git a/app/portainer/views/settings/authentication/settingsAuthenticationController.js b/app/portainer/views/settings/authentication/settingsAuthenticationController.js index 34e6c4124..97eb2a5dc 100644 --- a/app/portainer/views/settings/authentication/settingsAuthenticationController.js +++ b/app/portainer/views/settings/authentication/settingsAuthenticationController.js @@ -14,9 +14,32 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [ uploadInProgress: false, connectivityCheckInProgress: false, actionInProgress: false, + availableUserSessionTimeoutOptions: [ + { + key: '1 hour', + value: '1h', + }, + { + key: '4 hours', + value: '4h', + }, + { + key: '8 hours', + value: '8h', + }, + { + key: '24 hours', + value: '24h', + }, + { key: '1 week', value: `${24 * 7}h` }, + { key: '1 month', value: `${24 * 30}h` }, + { key: '6 months', value: `${24 * 30 * 6}h` }, + { key: '1 year', value: `${24 * 30 * 12}h` }, + ], }; $scope.formValues = { + UserSessionTimeout: $scope.state.availableUserSessionTimeoutOptions[0], TLSCACert: '', LDAPSettings: { AnonymousMode: true, diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 6e57ac2c5..2ba2696af 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -19,7 +19,7 @@ endpoint-api-version="applicationState.endpoint.apiVersion" swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'" standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'" - admin-access="!applicationState.application.authentication || isAdmin" + admin-access="isAdmin" offline-mode="endpointState.OfflineMode" >
- - - - - -
- +
@@ -136,7 +136,7 @@
- +
From 06911ad2c6ec5561673a02bde62264f5cd180271 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Wed, 10 Jun 2020 21:59:11 +0200 Subject: [PATCH 028/195] refactor(app): remove all VMWARE_VIC related code (#3914) --- app/docker/helpers/infoHelper.js | 6 +----- app/docker/views/containers/stats/containerstats.html | 2 +- app/docker/views/images/edit/imageController.js | 3 +-- app/docker/views/networks/edit/networkController.js | 5 +---- app/docker/views/volumes/create/createVolumeController.js | 3 +-- app/portainer/views/sidebar/sidebar.html | 2 +- 6 files changed, 6 insertions(+), 15 deletions(-) diff --git a/app/docker/helpers/infoHelper.js b/app/docker/helpers/infoHelper.js index 3ec824793..7bd7bcfff 100644 --- a/app/docker/helpers/infoHelper.js +++ b/app/docker/helpers/infoHelper.js @@ -18,11 +18,7 @@ angular.module('portainer.docker').factory('InfoHelper', [ } if (!info.Swarm || _.isEmpty(info.Swarm.NodeID)) { - if (info.ID === 'vSphere Integrated Containers') { - mode.provider = 'VMWARE_VIC'; - } else { - mode.provider = 'DOCKER_STANDALONE'; - } + mode.provider = 'DOCKER_STANDALONE'; } else { mode.provider = 'DOCKER_SWARM_MODE'; if (info.Swarm.ControlAvailable) { diff --git a/app/docker/views/containers/stats/containerstats.html b/app/docker/views/containers/stats/containerstats.html index c4d800b9a..0a9d019c8 100644 --- a/app/docker/views/containers/stats/containerstats.html +++ b/app/docker/views/containers/stats/containerstats.html @@ -80,7 +80,7 @@
-
+
From e0c47b644e88a2b22f9ced3353d15bd5e2fc8cca Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 11 Jun 2020 10:51:49 +1200 Subject: [PATCH 029/195] feat(pulldog): update configuration --- pull-dog.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pull-dog.json b/pull-dog.json index a2c647ff1..eaeed2e08 100644 --- a/pull-dog.json +++ b/pull-dog.json @@ -1,5 +1,4 @@ { "dockerComposeYmlFilePaths": ["docker-compose.pull-dog.yml"], - "isLazy": true, - "conversationMode": "multipleComments" -} \ No newline at end of file + "isLazy": true +} From 381e372c4ce85c95b15c1f0a5f837c704c5cc5bd Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Thu, 11 Jun 2020 23:06:41 +0200 Subject: [PATCH 030/195] chore(app): clean and update dependencies (#3917) --- app/portainer/services/modalService.js | 2 + package.json | 35 +- yarn.lock | 1507 +++++------------------- 3 files changed, 321 insertions(+), 1223 deletions(-) diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 236b2cea2..2a9e40f64 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -129,6 +129,7 @@ angular.module('portainer.app').factory('ModalService', [ }; service.confirmContainerDeletion = function (title, callback) { + title = $sanitize(title); prompt({ title: title, inputType: 'checkbox', @@ -202,6 +203,7 @@ angular.module('portainer.app').factory('ModalService', [ }; service.confirmServiceForceUpdate = function (message, callback) { + message = $sanitize(message); customPrompt( { title: 'Are you sure ?', diff --git a/package.json b/package.json index 200e7d49b..579626fd9 100644 --- a/package.json +++ b/package.json @@ -70,18 +70,18 @@ "angularjs-scroll-glue": "^2.2.0", "angularjs-slider": "^6.4.0", "babel-plugin-angularjs-annotate": "^0.10.0", - "bootbox": "^4.4.0", + "bootbox": "^5.4.0", "bootstrap": "^3.4.0", "chart.js": "~2.6.0", "codemirror": "~5.30.0", "filesize": "~3.3.0", - "jquery": "3.4.0", - "js-yaml": "~3.13.1", + "jquery": "^3.5.1", + "js-yaml": "^3.14.0", "lodash-es": "^4.17.15", "moment": "^2.21.0", "ng-file-upload": "~12.2.13", "splitargs": "github:deviantony/splitargs#semver:~0.2.0", - "toastr": "github:CodeSeven/toastr#semver:~2.1.3", + "toastr": "^2.1.4", "ui-select": "^0.19.8", "uuid": "^3.3.2", "xterm": "^3.8.0" @@ -102,23 +102,13 @@ "eslint-loader": "^2.1.2", "eslint-plugin-import": "^2.20.2", "file-loader": "^1.1.11", - "grunt": "~0.4.0", - "grunt-cli": "^1.2.0", + "grunt": "^1.1.0", + "grunt-cli": "^1.3.2", "grunt-config": "^1.0.0", - "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-concat": "~0.1.3", - "grunt-contrib-copy": "~0.4.0", - "grunt-contrib-jshint": "^1.1.0", - "grunt-contrib-uglify": "^0.9.2", - "grunt-contrib-watch": "~0.3.1", + "grunt-contrib-clean": "^2.0.0", + "grunt-contrib-copy": "^1.0.0", "grunt-env": "^0.4.4", - "grunt-filerev": "^2.3.1", - "grunt-html2js": "~0.1.0", - "grunt-karma": "~0.4.4", - "grunt-postcss": "^0.8.0", - "grunt-replace": "^1.0.1", "grunt-shell": "^1.1.2", - "grunt-usemin": "^3.1.1", "grunt-webpack": "^3.1.3", "gruntify-eslint": "^3.1.0", "html-loader": "^0.5.5", @@ -139,11 +129,16 @@ "webpack": "^4.26.0", "webpack-build-notifier": "^0.1.30", "webpack-cli": "^3.1.2", - "webpack-dev-server": "^3.1.10", + "webpack-dev-server": "^3.11.0", "webpack-merge": "^4.1.4" }, "resolutions": { - "jquery": "^3.3.1" + "jquery": "^3.5.1", + "decompress": "^4.2.1", + "lodash": "^4.17.15", + "js-yaml": "^3.14.0", + "minimist": "^1.2.5", + "http-proxy": "^1.18.1" }, "husky": { "hooks": { diff --git a/yarn.lock b/yarn.lock index 7e25f98d3..cdcde4adc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -710,6 +710,14 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" +"@babel/runtime-corejs3@^7.9.2": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.2.tgz#3511797ddf9a3d6f3ce46b99cc835184817eaa4e" + integrity sha512-+a2M/u7r15o3dV1NEizr9bRi+KUVnrs/qYxF0Z06DAPx/4VCWaz1WA7EcbE+uqGgt39lp5akWGmHsTseIkHkHg== + dependencies: + core-js-pure "^3.0.0" + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" @@ -798,6 +806,11 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/fined@*": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/fined/-/fined-1.1.2.tgz#05d2b9f93d144855c97c18c9675f424ed01400c4" + integrity sha512-hzzTS+X9EqDhx4vwdch/DOZci/bfh5J6Nyz8lqvyfBg2ROx2fPafX+LpDfpVgSvQKj0EYkwTYpBO3z2etwbkOw== + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -807,27 +820,29 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/globby@^9.1.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@types/globby/-/globby-9.1.0.tgz#08e2cf99c64f8e45c6cfbe05e9d8ac763aca6482" - integrity sha512-9du/HCA71EBz7syHRnM4Q/u4Fbx3SyN/Uu+4Of9lyPX4A6Xi+A8VMxvx8j5/CMTfrae2Zwdwg0fAaKvKXfRbAw== - dependencies: - globby "*" - -"@types/handlebars@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.1.0.tgz#3fcce9bf88f85fe73dc932240ab3fb682c624850" - integrity sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA== - dependencies: - handlebars "*" - -"@types/inquirer@6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.0.1.tgz#ea9b483a81f16f4f0d27d5c8d9d081dfa36c4ee9" - integrity sha512-O9rEHE9iBvYaFAGS0fAlDzqY/3CsOrRKzni4zwnAEce2JrHUEbXAce2Pwwe8ZGzmQkucwSXn1tSiKig37INgfA== +"@types/inquirer@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" + integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw== dependencies: "@types/through" "*" - rxjs ">=6.4.0" + rxjs "^6.4.0" + +"@types/interpret@*": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/interpret/-/interpret-1.1.1.tgz#b1bf85b0420e2414b989ce237658ad20dc03719b" + integrity sha512-HZ4d0m2Ebl8DmrOdYZHgYyipj/8Ftq1/ssB/oQR7fqfUrwtTP7IW3BDi2V445nhPBLzZjEkApaPVp83moSCXlA== + dependencies: + "@types/node" "*" + +"@types/liftoff@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@types/liftoff/-/liftoff-2.5.0.tgz#aa5f030ae0952d1b86225f3e9f27f6d5b69714aa" + integrity sha512-1jsThE//wKDK+hYM+NJqswI+K9lfR0YNMctteOxAzk/aemI0rQsVDk6Dia0zkPfBWFTh+hiDmrGQXqP1tyM+eg== + dependencies: + "@types/fined" "*" + "@types/interpret" "*" + "@types/node" "*" "@types/minimatch@*": version "3.0.3" @@ -1023,23 +1038,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -LiveScript@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/LiveScript/-/LiveScript-1.0.1.tgz#fd314990e5d75010425789f525a67531b5a0b143" - integrity sha1-/TFJkOXXUBBCV4n1JaZ1MbWgsUM= - dependencies: - prelude-ls ">= 0.6.0" - abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abbrev@1.0.x: - version "1.0.9" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" - integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= - accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -1075,13 +1078,6 @@ acorn@^6.0.7, acorn@^6.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -active-x-obfuscator@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/active-x-obfuscator/-/active-x-obfuscator-0.0.1.tgz#089b89b37145ff1d9ec74af6530be5526cae1f1a" - integrity sha1-CJuJs3FF/x2ex0r2UwvlUmyuHxo= - dependencies: - zeparser "0.0.5" - aggregate-error@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" @@ -1123,25 +1119,11 @@ ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -align-text@^0.1.1, align-text@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" - integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc= - dependencies: - kind-of "^3.0.2" - longest "^1.0.1" - repeat-string "^1.5.2" - alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= - angular-clipboard@^1.6.2: version "1.7.0" resolved "https://registry.yarnpkg.com/angular-clipboard/-/angular-clipboard-1.7.0.tgz#9621a6ce66eab1ea9549aa8bfb3b71352307554f" @@ -1341,15 +1323,6 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -applause@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/applause/-/applause-1.2.2.tgz#a8468579e81f67397bb5634c29953bedcd0f56c0" - integrity sha1-qEaFeegfZzl7tWNMKZU77c0PVsA= - dependencies: - cson-parser "^1.1.0" - js-yaml "^3.3.0" - lodash "^3.10.0" - aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -1374,14 +1347,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -"argparse@~ 0.1.11": - version "0.1.16" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-0.1.16.tgz#cfd01e0fbba3d6caed049fbd758d40f65196f57c" - integrity sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw= - dependencies: - underscore "~1.7.0" - underscore.string "~2.4.0" - arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -1513,22 +1478,17 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async@0.2.x, async@~0.2.6, async@~0.2.9: - version "0.2.10" - resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= - -async@^2.6.2: +async@^2.6.1, async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== dependencies: lodash "^4.17.14" -async@~0.1.22: - version "0.1.22" - resolved "https://registry.yarnpkg.com/async/-/async-0.1.22.tgz#0fc1aaa088a0e3ef0ebe2d8831bab0dcf8845061" - integrity sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE= +async@~1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= atob@^2.1.2: version "2.1.2" @@ -1632,11 +1592,6 @@ base64-js@^1.0.2: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -base64id@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/base64id/-/base64id-0.1.0.tgz#02ce0fdeee0cef4f40080e1e73e834f0b1bfce3f" - integrity sha1-As4P3u4M709ACA4ec+g08LG/zj8= - base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -1781,16 +1736,25 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= -bootbox@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/bootbox/-/bootbox-4.4.0.tgz#ff7f898fb87d4527e547feb64158f88450d1a0c9" - integrity sha1-/3+Jj7h9RSflR/62QVj4hFDRoMk= +bootbox@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/bootbox/-/bootbox-5.4.0.tgz#2857a63c270b1b797d62e4c5597e74b497267655" + integrity sha512-GCPrDwZpJsUnqzrto3ZURVafypl13+DAyE3YZx5jR5EIoTDSyREPhr77hlCuPKvM6VvXR5Mh/34W3DYMC7JE9g== + dependencies: + bootstrap "^4.4.0" + jquery "^3.4.1" + popper.js "^1.16.0" bootstrap@^3.4.0: version "3.4.1" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-3.4.1.tgz#c3a347d419e289ad11f4033e3c4132b87c081d72" integrity sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA== +bootstrap@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.0.tgz#97d9dbcb5a8972f8722c9962483543b907d9b9ec" + integrity sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1879,13 +1843,6 @@ browserify-sign@^4.0.0: inherits "^2.0.1" parse-asn1 "^5.0.0" -browserify-zlib@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" - integrity sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0= - dependencies: - pako "~0.2.0" - browserify-zlib@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" @@ -2090,11 +2047,6 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" -camelcase@^1.0.2: - version "1.2.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" - integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= - camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -2135,15 +2087,7 @@ caw@^2.0.0, caw@^2.0.1: tunnel-agent "^0.6.0" url-to-options "^1.0.1" -center-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" - integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60= - dependencies: - align-text "^0.1.3" - lazy-cache "^1.0.3" - -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2242,11 +2186,6 @@ chokidar@^2.0.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@~0.6: - version "0.6.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-0.6.3.tgz#e85968fa235f21773d388c617af085bf2104425a" - integrity sha1-6Flo+iNfIXc9OIxhevCFvyEEQlo= - chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -2357,32 +2296,6 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= -cli@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14" - integrity sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ= - dependencies: - exit "0.1.2" - glob "^7.1.1" - -cliui@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" - integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE= - dependencies: - center-align "^0.1.1" - right-align "^0.1.1" - wordwrap "0.0.2" - -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -2435,20 +2348,10 @@ codemirror@~5.30.0: resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.30.0.tgz#86e57dd5ea5535acbcf9c720797b4cefe05b5a70" integrity sha512-pfJV/7fLAUUenuGK3iANkQu1AxNLuWpeF7HV6YFDjSBMp53F8FTa2F6oPs9NKAHFweT2m08usmXUIA+7sohdew== -coffee-script@^1.10.0: - version "1.12.7" - resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" - integrity sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw== - -coffee-script@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.3.3.tgz#150d6b4cb522894369efed6a2101c20bc7f4a4f4" - integrity sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ= - -coffee-script@~1.6: - version "1.6.3" - resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.6.3.tgz#6355d32cf1b04cdff6b484e5e711782b2f0c39be" - integrity sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4= +coffeescript@~1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-1.10.0.tgz#e7aa8301917ef621b35d8a39f348dcdd1db7e33e" + integrity sha1-56qDAZF+9iGzXYo580jc3R234z4= collection-visit@^1.0.0: version "1.0.0" @@ -2507,16 +2410,6 @@ colormin@^1.0.5: css-color-names "0.0.4" has "^1.0.1" -colors@0.6.0-1: - version "0.6.0-1" - resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.0-1.tgz#6dbb68ceb8bc60f2b313dcc5ce1599f06d19e67a" - integrity sha1-bbtozri8YPKzE9zFzhWZ8G0Z5no= - -colors@0.x.x, colors@~0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc" - integrity sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w= - colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -2537,11 +2430,6 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -commander@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781" - integrity sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E= - commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -2594,7 +2482,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.4.1, concat-stream@^1.5.0, concat-stream@^1.5.2: +concat-stream@^1.5.0, concat-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -2617,13 +2505,6 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== -console-browserify@1.1.x: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" - integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= - dependencies: - date-now "^0.1.4" - console-browserify@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" @@ -2664,7 +2545,7 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@^1.0.0, convert-source-map@^1.7.0: +convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== @@ -2706,16 +2587,16 @@ core-js-compat@^3.6.2: browserslist "^4.8.3" semver "7.0.0" +core-js-pure@^3.0.0: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" + integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== + core-js@^2.6.5: version "2.6.11" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== -core-js@^3.3.2: - version "3.6.4" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" - integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== - core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2819,13 +2700,6 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -cson-parser@^1.1.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-1.3.5.tgz#7ec675e039145533bf2a6a856073f1599d9c2d24" - integrity sha1-fsZ14DkUVTO/KmqFYHPxWZ2cLSQ= - dependencies: - coffee-script "^1.10.0" - css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -3001,17 +2875,15 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== -date-now@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" - integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= +dateformat@~1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" -dateformat@1.0.2-1.2.3: - version "1.0.2-1.2.3" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.2-1.2.3.tgz#b0220c02de98617433b72851cf47de3df2cdbee9" - integrity sha1-sCIMAt6YYXQztyhRz0fePfLNvuk= - -debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: +debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -3032,7 +2904,7 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -decamelize@^1.0.0, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -3088,10 +2960,10 @@ decompress-unzip@^4.0.1: pify "^2.3.0" yauzl "^2.4.2" -decompress@^4.0.0, decompress@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.0.tgz#7aedd85427e5a92dacfe55674a7c505e96d01f9d" - integrity sha1-eu3YVCflqS2s/lVnSnxQXpbQH50= +decompress@^4.0.0, decompress@^4.2.0, decompress@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.1.tgz#007f55cc6a62c055afa37c07eb6a4ee1b773f118" + integrity sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ== dependencies: decompress-tar "^4.0.0" decompress-tarbz2 "^4.0.0" @@ -3107,24 +2979,6 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= -deep-equal@*: - version "2.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.1.tgz#fc12bbd6850e93212f21344748682ccc5a8813cf" - integrity sha512-7Et6r6XfNW61CPPCIYfm1YPGSmh6+CliYeL4km7GWJcpX5LTAflGF8drLLR+MZX+2P3NZfAfSduutBbSWqER4g== - dependencies: - es-abstract "^1.16.3" - es-get-iterator "^1.0.1" - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - isarray "^2.0.5" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - side-channel "^1.0.1" - which-boxed-primitive "^1.0.1" - which-collection "^1.0.0" - deep-equal@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -3253,11 +3107,6 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== -diff@^2.0.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" - integrity sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k= - diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3346,13 +3195,6 @@ domelementtype@^2.0.1: resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== -domhandler@2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" - integrity sha1-LeWaCCLVAn+r/28DLCsloqir5zg= - dependencies: - domelementtype "1" - domhandler@^2.3.0: version "2.4.2" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" @@ -3360,7 +3202,7 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -domutils@1.5, domutils@1.5.1: +domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= @@ -3433,11 +3275,6 @@ duplexify@^3.4.2, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" -each-async@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/each-async/-/each-async-0.1.3.tgz#b436025b08da2f86608025519e3096763dedfca3" - integrity sha1-tDYCWwjaL4ZggCVRnjCWdj3t/KM= - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3516,11 +3353,6 @@ enhanced-resolve@^4.1.0: memory-fs "^0.5.0" tapable "^1.0.0" -entities@1.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26" - integrity sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY= - entities@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" @@ -3545,7 +3377,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.16.3, es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4: +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: version "1.17.5" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== @@ -3562,19 +3394,6 @@ es-abstract@^1.16.3, es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstrac string.prototype.trimleft "^2.1.1" string.prototype.trimright "^2.1.1" -es-get-iterator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" - integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== - dependencies: - es-abstract "^1.17.4" - has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" - is-string "^1.0.5" - isarray "^2.0.5" - es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -3669,16 +3488,6 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escodegen@0.0.23: - version "0.0.23" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-0.0.23.tgz#9acf978164368e42276571f18839c823b3a844df" - integrity sha1-ms+XgWQ2jkInZXHxiDnII7OoRN8= - dependencies: - esprima "~1.0.2" - estraverse "~0.0.4" - optionalDependencies: - source-map ">= 0.1.2" - escope@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" @@ -3861,16 +3670,6 @@ espree@^5.0.1: acorn-jsx "^5.0.0" eslint-visitor-keys "^1.0.0" -esprima@1.0.x, "esprima@~ 1.0.2", esprima@~1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.0.4.tgz#9f557e08fc3b4d26ece9dd34f8fbf476b62585ad" - integrity sha1-n1V+CPw7TSbs6d00+Pv0drYlha0= - -esprima@^2.6.0: - version "2.7.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" - integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE= - esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -3905,11 +3704,6 @@ estraverse@^5.0.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.0.0.tgz#ac81750b482c11cca26e4b07e83ed8f75fbcdc22" integrity sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A== -estraverse@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-0.0.4.tgz#01a0932dfee574684a598af5a67c3bf9b6428db2" - integrity sha1-AaCTLf7ldGhKWYr1pnw7+bZCjbI= - esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -4036,7 +3830,7 @@ exit-hook@^1.0.0: resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= -exit@0.1.2, exit@0.1.x, exit@~0.1.1: +exit@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= @@ -4167,7 +3961,7 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== -fast-glob@^3.0.3, fast-glob@^3.1.1: +fast-glob@^3.0.3: version "3.2.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== @@ -4227,7 +4021,7 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== -figures@^1.0.1, figures@^1.3.5, figures@^1.7.0: +figures@^1.3.5, figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= @@ -4331,14 +4125,6 @@ filenamify@^2.0.0: strip-outer "^1.0.0" trim-repeated "^1.0.0" -fileset@0.1.x, fileset@~0.1.5: - version "0.1.8" - resolved "https://registry.yarnpkg.com/fileset/-/fileset-0.1.8.tgz#506b91a9396eaa7e32fb42a84077c7a0c736b741" - integrity sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E= - dependencies: - glob "3.x" - minimatch "0.x" - filesize@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.3.0.tgz#53149ea3460e3b2e024962a51648aa572cf98122" @@ -4449,13 +4235,12 @@ findup-sync@^2.0.0: micromatch "^3.0.4" resolve-dir "^1.0.1" -findup-sync@~0.1.2: - version "0.1.3" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.1.3.tgz#7f3e7a97b82392c653bf06589bd85190e93c3683" - integrity sha1-fz56l7gjksZTvwZYm9hRkOk8NoM= +findup-sync@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" + integrity sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY= dependencies: - glob "~3.2.9" - lodash "~2.4.1" + glob "~5.0.0" fined@^1.0.1: version "1.2.0" @@ -4592,14 +4377,6 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -gaze@~0.3.3: - version "0.3.4" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-0.3.4.tgz#5f94bdda0afe53bc710969bcd6f282548d60c279" - integrity sha1-X5S92gr+U7xxCWm81vKCVI1gwnk= - dependencies: - fileset "~0.1.5" - minimatch "~0.2.9" - generate-function@^2.0.0: version "2.3.1" resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f" @@ -4619,11 +4396,6 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -4713,14 +4485,6 @@ glob-parent@^5.1.0: dependencies: is-glob "^4.0.1" -glob@3.x, glob@~3.2.9: - version "3.2.11" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" - integrity sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0= - dependencies: - inherits "2" - minimatch "0.3" - glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -4733,14 +4497,28 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@~3.1.21: - version "3.1.21" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" - integrity sha1-0p4KBV3qUTj00H7UDomC6DwgZs0= +glob@~5.0.0: + version "5.0.15" + resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" + integrity sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= dependencies: - graceful-fs "~1.2.0" - inherits "1" - minimatch "~0.2.11" + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@~7.0.0: + version "7.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" + integrity sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo= + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" global-modules@2.0.0: version "2.0.0" @@ -4788,18 +4566,6 @@ globals@^9.14.0: resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== -globby@*: - version "11.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.0.tgz#56fd0e9f0d4f8fb0c456f1ab0dee96e1380bc154" - integrity sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.1.1" - ignore "^5.1.4" - merge2 "^1.3.0" - slash "^3.0.0" - globby@^10.0.1: version "10.0.2" resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" @@ -4873,11 +4639,6 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1. resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== -graceful-fs@~1, graceful-fs@~1.2.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" - integrity sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q= - "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" @@ -4888,12 +4649,7 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= -growly@~1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.1.1.tgz#eb434a0e56f0241d82864cbfd4112c70911242fa" - integrity sha1-60NKDlbwJB2Chky/1BEscJESQvo= - -grunt-cli@^1.2.0: +grunt-cli@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/grunt-cli/-/grunt-cli-1.3.2.tgz#60f12d12c1b5aae94ae3469c6b5fe24e960014e8" integrity sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ== @@ -4904,6 +4660,16 @@ grunt-cli@^1.2.0: nopt "~4.0.1" v8flags "~3.1.1" +grunt-cli@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/grunt-cli/-/grunt-cli-1.2.0.tgz#562b119ebb069ddb464ace2845501be97b35b6a8" + integrity sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg= + dependencies: + findup-sync "~0.3.0" + grunt-known-options "~1.1.0" + nopt "~3.0.6" + resolve "~1.1.0" + grunt-config@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/grunt-config/-/grunt-config-1.0.0.tgz#e0a20e4cbadb8ae90843697a8afa05af8aeb860c" @@ -4911,47 +4677,21 @@ grunt-config@^1.0.0: dependencies: chalk "^1.1.0" -grunt-contrib-clean@~0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/grunt-contrib-clean/-/grunt-contrib-clean-0.4.1.tgz#7f8f46e2f2a7187e9c2d083ab30262aa6a64e334" - integrity sha1-f49G4vKnGH6cLQg6swJiqmpk4zQ= +grunt-contrib-clean@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/grunt-contrib-clean/-/grunt-contrib-clean-2.0.0.tgz#3be7ca480da4b740aa5e9d863e2f7e8b24f8a68b" + integrity sha512-g5ZD3ORk6gMa5ugZosLDQl3dZO7cI3R14U75hTM+dVLVxdMNJCPVmwf9OUt4v4eWgpKKWWoVK9DZc1amJp4nQw== + dependencies: + async "^2.6.1" + rimraf "^2.6.2" -grunt-contrib-concat@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/grunt-contrib-concat/-/grunt-contrib-concat-0.1.3.tgz#df9a1a9bc8d75fcd00794b3d0f6d8ae8278523ac" - integrity sha1-35oam8jXX80AeUs9D22K6CeFI6w= - -grunt-contrib-copy@~0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/grunt-contrib-copy/-/grunt-contrib-copy-0.4.1.tgz#f0753b40ae21bb706daefb0b299e03cdf5fa9d6e" - integrity sha1-8HU7QK4hu3BtrvsLKZ4DzfX6nW4= - -grunt-contrib-jshint@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/grunt-contrib-jshint/-/grunt-contrib-jshint-1.1.0.tgz#369d909b2593c40e8be79940b21340850c7939ac" - integrity sha1-Np2QmyWTxA6L55lAshNAhQx5Oaw= +grunt-contrib-copy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz#7060c6581e904b8ab0d00f076e0a8f6e3e7c3573" + integrity sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM= dependencies: chalk "^1.1.1" - hooker "^0.2.3" - jshint "~2.9.4" - -grunt-contrib-uglify@^0.9.2: - version "0.9.2" - resolved "https://registry.yarnpkg.com/grunt-contrib-uglify/-/grunt-contrib-uglify-0.9.2.tgz#1a61c6f212410e4abb4f7c89153717b101560260" - integrity sha1-GmHG8hJBDkq7T3yJFTcXsQFWAmA= - dependencies: - chalk "^1.0.0" - lodash "^3.2.0" - maxmin "^1.0.0" - uglify-js "^2.4.24" - uri-path "0.0.2" - -grunt-contrib-watch@~0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/grunt-contrib-watch/-/grunt-contrib-watch-0.3.1.tgz#177b1309ac2d0ba01d12caa9ff90f67e2598307c" - integrity sha1-F3sTCawtC6AdEsqp/5D2fiWYMHw= - dependencies: - gaze "~0.3.3" + file-sync-cmp "^0.1.0" grunt-env@^0.4.4: version "0.4.4" @@ -4961,83 +4701,41 @@ grunt-env@^0.4.4: ini "~1.3.0" lodash "~2.4.1" -grunt-filerev@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/grunt-filerev/-/grunt-filerev-2.3.1.tgz#2990210f0b5a9edc5e7198987fd1c029c6d5f4df" - integrity sha1-KZAhDwtantxecZiYf9HAKcbV9N8= - dependencies: - chalk "^1.0.0" - convert-source-map "^1.0.0" - each-async "^0.1.3" - -grunt-html2js@~0.1.0: - version "0.1.9" - resolved "https://registry.yarnpkg.com/grunt-html2js/-/grunt-html2js-0.1.9.tgz#0de0139ffabebc25f51828e8b5dcf6c654b984be" - integrity sha1-DeATn/q+vCX1GCjotdz2xlS5hL4= - -grunt-karma@~0.4.4: - version "0.4.6" - resolved "https://registry.yarnpkg.com/grunt-karma/-/grunt-karma-0.4.6.tgz#60736c14968fddf4060afe2da816e5261cab2d90" - integrity sha1-YHNsFJaP3fQGCv4tqBblJhyrLZA= - dependencies: - karma "~0.8.5" - grunt-known-options@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.1.tgz#6cc088107bd0219dc5d3e57d91923f469059804d" integrity sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ== -grunt-legacy-log-utils@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz#c0706b9dd9064e116f36f23fe4e6b048672c0f7e" - integrity sha1-wHBrndkGThFvNvI/5OawSGcsD34= +grunt-legacy-log-utils@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz#d2f442c7c0150065d9004b08fd7410d37519194e" + integrity sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA== dependencies: - colors "~0.6.2" - lodash "~2.4.1" - underscore.string "~2.3.3" + chalk "~2.4.1" + lodash "~4.17.10" -grunt-legacy-log@~0.1.0: - version "0.1.3" - resolved "https://registry.yarnpkg.com/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz#ec29426e803021af59029f87d2f9cd7335a05531" - integrity sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE= +grunt-legacy-log@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz#c8cd2c6c81a4465b9bbf2d874d963fef7a59ffb9" + integrity sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw== dependencies: - colors "~0.6.2" - grunt-legacy-log-utils "~0.1.1" + colors "~1.1.2" + grunt-legacy-log-utils "~2.0.0" hooker "~0.2.3" - lodash "~2.4.1" - underscore.string "~2.3.3" + lodash "~4.17.5" -grunt-legacy-util@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz#93324884dbf7e37a9ff7c026dff451d94a9e554b" - integrity sha1-kzJIhNv343qf98Am3/RR2UqeVUs= +grunt-legacy-util@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz#e10624e7c86034e5b870c8a8616743f0a0845e42" + integrity sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A== dependencies: - async "~0.1.22" + async "~1.5.2" exit "~0.1.1" getobject "~0.1.0" hooker "~0.2.3" - lodash "~0.9.2" - underscore.string "~2.2.1" - which "~1.0.5" - -grunt-postcss@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/grunt-postcss/-/grunt-postcss-0.8.0.tgz#8f30a8af607903ce0c45f01f0be42c60e31ceb0e" - integrity sha1-jzCor2B5A84MRfAfC+QsYOMc6w4= - dependencies: - chalk "^1.0.0" - diff "^2.0.2" - postcss "^5.0.0" - -grunt-replace@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/grunt-replace/-/grunt-replace-1.0.1.tgz#90a79532fb89041fe427c87d425238b0f886651a" - integrity sha1-kKeVMvuJBB/kJ8h9QlI4sPiGZRo= - dependencies: - applause "1.2.2" - chalk "^1.1.0" - file-sync-cmp "^0.1.0" - lodash "^4.11.0" + lodash "~4.17.10" + underscore.string "~3.3.4" + which "~1.3.0" grunt-shell@^1.1.2: version "1.3.1" @@ -5048,16 +4746,6 @@ grunt-shell@^1.1.2: npm-run-path "^1.0.0" object-assign "^4.0.0" -grunt-usemin@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/grunt-usemin/-/grunt-usemin-3.1.1.tgz#5ab679510d672cea566cc717abe8b8a009f641c2" - integrity sha1-WrZ5UQ1nLOpWbMcXq+i4oAn2QcI= - dependencies: - chalk "^1.1.1" - debug "^2.1.3" - lodash "^3.6.0" - path-exists "^1.0.0" - grunt-webpack@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/grunt-webpack/-/grunt-webpack-3.1.3.tgz#7e0a016773b105bb87718c19f308100b498ce39a" @@ -5066,31 +4754,28 @@ grunt-webpack@^3.1.3: deep-for-each "^2.0.2" lodash "^4.7.0" -grunt@~0.4.0: - version "0.4.5" - resolved "https://registry.yarnpkg.com/grunt/-/grunt-0.4.5.tgz#56937cd5194324adff6d207631832a9d6ba4e7f0" - integrity sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A= +grunt@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/grunt/-/grunt-1.1.0.tgz#97dc6e6add901459774a988e4f454a12e24c9d3d" + integrity sha512-+NGod0grmviZ7Nzdi9am7vuRS/h76PcWDsV635mEXF0PEQMUV6Kb+OjTdsVxbi0PZmfQOjCMKb3w8CVZcqsn1g== dependencies: - async "~0.1.22" - coffee-script "~1.3.3" - colors "~0.6.2" - dateformat "1.0.2-1.2.3" + coffeescript "~1.10.0" + dateformat "~1.0.12" eventemitter2 "~0.4.13" exit "~0.1.1" - findup-sync "~0.1.2" - getobject "~0.1.0" - glob "~3.1.21" - grunt-legacy-log "~0.1.0" - grunt-legacy-util "~0.2.0" - hooker "~0.2.3" - iconv-lite "~0.2.11" - js-yaml "~2.0.5" - lodash "~0.9.2" - minimatch "~0.2.12" - nopt "~1.0.10" - rimraf "~2.2.8" - underscore.string "~2.2.1" - which "~1.0.5" + findup-sync "~0.3.0" + glob "~7.0.0" + grunt-cli "~1.2.0" + grunt-known-options "~1.1.0" + grunt-legacy-log "~2.0.0" + grunt-legacy-util "~1.1.1" + iconv-lite "~0.4.13" + js-yaml "~3.13.1" + minimatch "~3.0.2" + mkdirp "~1.0.3" + nopt "~3.0.6" + path-is-absolute "~1.0.0" + rimraf "~2.6.2" gruntify-eslint@^3.1.0: version "3.1.0" @@ -5099,20 +4784,12 @@ gruntify-eslint@^3.1.0: dependencies: eslint "^3.0.0" -gzip-size@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-1.0.0.tgz#66cf8b101047227b95bace6ea1da0c177ed5c22f" - integrity sha1-Zs+LEBBHInuVus5uodoMF37Vwi8= - dependencies: - browserify-zlib "^0.1.4" - concat-stream "^1.4.1" - handle-thing@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@*, handlebars@^4.4.3: +handlebars@^4.4.3: version "4.7.3" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.3.tgz#8ece2797826886cf8082d1726ff21d2a022550ee" integrity sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg== @@ -5123,14 +4800,6 @@ handlebars@*, handlebars@^4.4.3: optionalDependencies: uglify-js "^3.1.4" -handlebars@1.0.x: - version "1.0.12" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-1.0.12.tgz#18c6d3440c35e91b19b3ff582b9151ab4985d4fc" - integrity sha1-GMbTRAw16RsZs/9YK5FRq0mF1Pw= - dependencies: - optimist "~0.3" - uglify-js "~2.3" - has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -5253,7 +4922,7 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" -hooker@^0.2.3, hooker@~0.2.3: +hooker@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959" integrity sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk= @@ -5278,10 +4947,10 @@ html-comment-regex@^1.1.0, html-comment-regex@^1.1.2: resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" integrity sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ== -html-entities@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" - integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= +html-entities@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" + integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== html-loader@^0.5.5, html-loader@~0.5.5: version "0.5.5" @@ -5320,17 +4989,6 @@ html-webpack-plugin@^3.2.0: toposort "^1.0.0" util.promisify "1.0.0" -htmlparser2@3.8.x: - version "3.8.3" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" - integrity sha1-mWwosZFRaovoZQGn15dX5ccMEGg= - dependencies: - domelementtype "1" - domhandler "2.3" - domutils "1.5" - entities "1.0" - readable-stream "1.1" - htmlparser2@^3.3.0: version "3.10.1" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" @@ -5400,25 +5058,15 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy@^1.17.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" - integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== +http-proxy@^1.17.0, http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: eventemitter3 "^4.0.0" follow-redirects "^1.0.0" requires-port "^1.0.0" -http-proxy@~0.10: - version "0.10.4" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-0.10.4.tgz#14ba0ceaa2197f89fa30dea9e7b09e19cd93c22f" - integrity sha1-FLoM6qIZf4n6MN6p57CeGc2Twi8= - dependencies: - colors "0.x.x" - optimist "0.6.x" - pkginfo "0.3.x" - utile "~0.2.1" - https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" @@ -5445,23 +5093,13 @@ husky@>=4: slash "^3.0.0" which-pm-runs "^1.0.0" -i@0.3.x: - version "0.3.6" - resolved "https://registry.yarnpkg.com/i/-/i-0.3.6.tgz#d96c92732076f072711b6b10fd7d4f65ad8ee23d" - integrity sha1-2WyScyB28HJxG2sQ/X1PZa2O4j0= - -iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@~0.2.11: - version "0.2.11" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8" - integrity sha1-HOYKOleGSiktEyH/RgnKS7llrcg= - icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -5494,7 +5132,7 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1, ignore@^5.1.4: +ignore@^5.1.1: version "5.1.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== @@ -5663,11 +5301,6 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" - integrity sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js= - inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" @@ -5726,7 +5359,7 @@ inquirer@^6.2.2: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^7.0.0: +inquirer@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== @@ -5840,11 +5473,6 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= -is-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" - integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== - is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" @@ -5852,11 +5480,6 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-boolean-object@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== - is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5988,11 +5611,6 @@ is-lower-case@^1.1.0: dependencies: lower-case "^1.1.0" -is-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== - is-my-ip-valid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" @@ -6014,11 +5632,6 @@ is-natural-number@^4.0.1: resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= -is-number-object@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -6128,11 +5741,6 @@ is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== - is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -6143,7 +5751,7 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-string@^1.0.4, is-string@^1.0.5: +is-string@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== @@ -6188,16 +5796,6 @@ is-utf8@^0.2.0: resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - -is-weakset@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" - integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== - is-windows@^1.0.1, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -6208,21 +5806,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - isbinaryfile@^4.0.2: version "4.0.5" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.5.tgz#7193454fdd7fc0b12855c36c48d4ac7368fa3ec9" @@ -6245,23 +5833,6 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -istanbul@~0.1.40: - version "0.1.46" - resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.1.46.tgz#cefeb1c787d125a6db23bd0f63b0eb9390b0b40d" - integrity sha1-zv6xx4fRJabbI70PY7Drk5CwtA0= - dependencies: - abbrev "1.0.x" - async "0.2.x" - escodegen "0.0.23" - esprima "1.0.x" - fileset "0.1.x" - handlebars "1.0.x" - mkdirp "0.3.x" - nopt "2.1.x" - resolve "0.5.x" - which "1.0.x" - wordwrap "0.0.x" - isurl@^1.0.0-alpha5: version "1.0.0" resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" @@ -6270,15 +5841,10 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" -jquery@3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.0.tgz#8de513fa0fa4b2c7d2e48a530e26f0596936efdf" - integrity sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ== - -jquery@>=1.12.0, jquery@^3.3.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" - integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== +jquery@>=1.12.0, jquery@^3.4.1, jquery@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" + integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== js-base64@^2.1.9: version "2.5.2" @@ -6295,30 +5861,14 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.3.0, js-yaml@^3.5.1, js-yaml@~3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== +js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.5.1, js-yaml@~3.13.1, js-yaml@~3.7.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@~2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-2.0.5.tgz#a25ae6509999e97df278c6719da11bd0687743a8" - integrity sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g= - dependencies: - argparse "~ 0.1.11" - esprima "~ 1.0.2" - -js-yaml@~3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" - integrity sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A= - dependencies: - argparse "^1.0.7" - esprima "^2.6.0" - jsesc@^0.5.0, jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" @@ -6329,20 +5879,6 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jshint@~2.9.4: - version "2.9.7" - resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.9.7.tgz#038a3fa5c328fa3ab03ddfd85df88d3d87bedcbd" - integrity sha512-Q8XN38hGsVQhdlM+4gd1Xl7OB1VieSuCJf+fEJjpo59JH99bVJhXRXAh26qQ15wfdd1VPMuDWNeSWoNl53T4YA== - dependencies: - cli "~1.0.0" - console-browserify "1.1.x" - exit "0.1.x" - htmlparser2 "3.8.x" - lodash "~4.17.10" - minimatch "~3.0.2" - shelljs "0.3.x" - strip-json-comments "1.0.x" - json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -6404,31 +5940,6 @@ jsonpointer@^4.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= -karma@~0.8.5: - version "0.8.8" - resolved "https://registry.yarnpkg.com/karma/-/karma-0.8.8.tgz#b7fbb2aad0d43001f7d38820c1d9cc749af2e8a0" - integrity sha1-t/uyqtDUMAH304ggwdnMdJry6KA= - dependencies: - LiveScript "1.0.1" - chokidar "~0.6" - coffee-script "~1.6" - colors "0.6.0-1" - dateformat "1.0.2-1.2.3" - glob "~3.1.21" - growly "~1.1" - http-proxy "~0.10" - istanbul "~0.1.40" - lodash "~1.1" - log4js "~0.6.3" - mime "~1.2" - minimatch "~0.2" - optimist "0.3.5" - pause "0.0.1" - q "~0.9" - rimraf "~2.1" - socket.io "~0.9.13" - xmlbuilder "0.4.2" - keyv@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" @@ -6465,11 +5976,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -lazy-cache@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" - integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4= - lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" @@ -6706,31 +6212,11 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^3.10.0, lodash@^3.2.0, lodash@^3.6.0: - version "3.10.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" - integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= - -lodash@^4.0.0, lodash@^4.11.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.7.0, lodash@~4.17.10, lodash@~4.17.15: +lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.7.0, lodash@~2.4.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@~0.9.2: - version "0.9.2" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-0.9.2.tgz#8f3499c5245d346d682e5b0d3b40767e09f1a92c" - integrity sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw= - -lodash@~1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.1.1.tgz#41a2b2e9a00e64d6d1999f143ff6b0755f6bbb24" - integrity sha1-QaKy6aAOZNbRmZ8UP/awdV9ruyQ= - -lodash@~2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e" - integrity sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4= - log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" @@ -6761,14 +6247,6 @@ log-update@^2.3.0: cli-cursor "^2.0.0" wrap-ansi "^3.0.1" -log4js@~0.6.3: - version "0.6.38" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd" - integrity sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0= - dependencies: - readable-stream "~1.0.2" - semver "~4.3.3" - logalot@^2.0.0, logalot@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/logalot/-/logalot-2.1.0.tgz#5f8e8c90d304edf12530951a5554abb8c5e3f552" @@ -6777,12 +6255,12 @@ logalot@^2.0.0, logalot@^2.1.0: figures "^1.3.5" squeak "^1.0.0" -loglevel@^1.6.6: - version "1.6.7" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56" - integrity sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A== +loglevel@^1.6.8: + version "1.6.8" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" + integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== -longest@^1.0.0, longest@^1.0.1: +longest@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc= @@ -6834,11 +6312,6 @@ lpad-align@^1.0.1: longest "^1.0.0" meow "^3.3.0" -lru-cache@2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" - integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= - lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -6905,16 +6378,6 @@ math-expression-evaluator@^1.2.14: resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz#c14dcb3d8b4d150e5dcea9c68c8dad80309b0d5e" integrity sha512-L0j0tFVZBQQLeEjmWOvDLoRciIY8gQGWahvkztXUal8jH8R5Rlqo9GCvgqvXcy9LQhEWdQCVvzqAbxgYNt4blQ== -maxmin@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/maxmin/-/maxmin-1.1.0.tgz#71365e84a99dd8f8b3f7d5fde2f00d1e7f73be61" - integrity sha1-cTZehKmd2Piz99X94vANHn9zvmE= - dependencies: - chalk "^1.0.0" - figures "^1.0.1" - gzip-size "^1.0.0" - pretty-bytes "^1.0.0" - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -6964,7 +6427,7 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.1.0, meow@^3.3.0: +meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= @@ -7057,11 +6520,6 @@ mime@^2.0.3, mime@^2.4.4: resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== -mime@~1.2: - version "1.2.11" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10" - integrity sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA= - mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -7096,47 +6554,18 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@0.3: - version "0.3.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" - integrity sha1-J12O2qxPG7MyZHIInnlJyDlGmd0= - dependencies: - lru-cache "2" - sigmund "~1.0.0" - -minimatch@0.x: - version "0.4.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.4.0.tgz#bd2c7d060d2c8c8fd7cde7f1f2ed2d5b270fdb1b" - integrity sha1-vSx9Bg0sjI/Xzefx8u0tWycP2xs= - dependencies: - lru-cache "2" - sigmund "~1.0.0" - -minimatch@^3.0.0, minimatch@^3.0.4, minimatch@~3.0.2: +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" -minimatch@~0.2, minimatch@~0.2.11, minimatch@~0.2.12, minimatch@~0.2.9: - version "0.2.14" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" - integrity sha1-x054BXT2PG+aCQ6Q775u9TpqdWo= - dependencies: - lru-cache "2" - sigmund "~1.0.0" - -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~0.0.1: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -7161,18 +6590,18 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.3.x: - version "0.3.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" - integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= - -mkdirp@0.x.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== dependencies: minimist "^1.2.5" +mkdirp@~1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + moment@^2.10.6, moment@^2.16.0, moment@^2.21.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" @@ -7257,11 +6686,6 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== -nan@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-1.0.0.tgz#ae24f8850818d662fcab5acf7f3b95bfaa2ccf38" - integrity sha1-riT4hQgY1mL8q1rPfzuVv6oszzg= - nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -7284,11 +6708,6 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -ncp@0.4.x: - version "0.4.2" - resolved "https://registry.yarnpkg.com/ncp/-/ncp-0.4.2.tgz#abcc6cbd3ec2ed2a729ff6e7c1fa8f01784a8574" - integrity sha1-q8xsvT7C7Spyn/bnwfqPAXhKhXQ= - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -7373,20 +6792,18 @@ node-notifier@5.2.1: shellwords "^0.1.1" which "^1.3.0" -node-plop@~0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/node-plop/-/node-plop-0.25.0.tgz#1d3bdf286bf74baabb6755b4cef8c6ab37110180" - integrity sha512-OFvnTsDw9nxNdLrYcveJhU2Hnzg+AxOz6xBk8uXsi0vCOSP7Rng98pdgfsuZKyCN+qrc+/fSwlNC5hkXhJ6gww== +node-plop@~0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/node-plop/-/node-plop-0.26.0.tgz#9666b22cd46c7240fec2387e6000d43df8f2104d" + integrity sha512-jVFKfAIwHcURjk1Gl6wVZ0Csqo5irlOKhdCQppn/yoSUSkHFP4LomMjpO3tUj2ADMlguKbqTEg7LKCi59vDsxg== dependencies: - "@types/globby" "^9.1.0" - "@types/handlebars" "^4.1.0" - "@types/inquirer" "6.0.1" + "@babel/runtime-corejs3" "^7.9.2" + "@types/inquirer" "^6.5.0" change-case "^3.1.0" - core-js "^3.3.2" del "^5.1.0" globby "^10.0.1" handlebars "^4.4.3" - inquirer "^7.0.0" + inquirer "^7.1.0" isbinaryfile "^4.0.2" lodash.get "^4.4.2" mkdirp "^0.5.1" @@ -7399,17 +6816,10 @@ node-releases@^1.1.52: dependencies: semver "^6.3.0" -nopt@2.1.x: - version "2.1.2" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af" - integrity sha1-bMzZd7gBMqB3MdbozljCyDA8+a8= - dependencies: - abbrev "1" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= +nopt@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= dependencies: abbrev "1" @@ -7662,14 +7072,7 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" -optimist@0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.5.tgz#03654b52417030312d109f39b159825b60309304" - integrity sha1-A2VLUkFwMDEtEJ85sVmCW2AwkwQ= - dependencies: - wordwrap "~0.0.2" - -optimist@0.6.x, optimist@^0.6.1: +optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= @@ -7677,13 +7080,6 @@ optimist@0.6.x, optimist@^0.6.1: minimist "~0.0.1" wordwrap "~0.0.2" -optimist@~0.3, optimist@~0.3.5: - version "0.3.7" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" - integrity sha1-yQlBrVnkJzMokjB00s8ufLxuwNk= - dependencies: - wordwrap "~0.0.2" - optionator@^0.8.2: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -7696,11 +7092,6 @@ optionator@^0.8.2: type-check "~0.3.2" word-wrap "~1.2.3" -options@>=0.0.5: - version "0.0.6" - resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" - integrity sha1-7CLTEoBrtT5zF3Pnza788cZDEo8= - optipng-bin@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/optipng-bin/-/optipng-bin-5.1.0.tgz#a7c7ab600a3ab5a177dae2f94c2d800aa386b5a9" @@ -7746,7 +7137,7 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.0.0, os-locale@^3.1.0: +os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -7912,11 +7303,6 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pako@~0.2.0: - version "0.2.9" - resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" - integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU= - pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -8031,11 +7417,6 @@ path-dirname@^1.0.0: resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= -path-exists@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081" - integrity sha1-1aiZjrce83p0w06w2eum6HjuoIE= - path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -8053,7 +7434,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: +path-is-absolute@^1.0.0, path-is-absolute@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -8121,11 +7502,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pause@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" - integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= - pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -8216,11 +7592,6 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -pkginfo@0.3.x: - version "0.3.1" - resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" - integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= - please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" @@ -8229,15 +7600,16 @@ please-upgrade-node@^3.2.0: semver-compare "^1.0.0" plop@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/plop/-/plop-2.6.0.tgz#b0c3a9879c35008a22bb681bfba975ed2bc3c38c" - integrity sha512-faK3oVbWL7DYdC5ZjM+lJPrmXAPirN28QRFykPWwg6i7dv2T373JyxKlcO+XGMQcPlEYDFyKUXIuiY3Db5Ktkw== + version "2.7.1" + resolved "https://registry.yarnpkg.com/plop/-/plop-2.7.1.tgz#b48af861f19cb4d5ce141852e4385180419b4b83" + integrity sha512-2c1CDgi/88AmDNWyQcHILvjFxiyG/nS7CbDUUvzuWdrunW5QUWtQRKJRoITkGNze3ZcpUO9OOGCdh6BxHI+viA== dependencies: + "@types/liftoff" "^2.5.0" chalk "^1.1.3" interpret "^1.2.0" liftoff "^2.5.0" minimist "^1.2.0" - node-plop "~0.25.0" + node-plop "~0.26.0" ora "^3.4.0" v8flags "^2.0.10" @@ -8256,15 +7628,15 @@ pngquant-bin@^5.0.0: execa "^0.10.0" logalot "^2.0.0" -policyfile@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/policyfile/-/policyfile-0.0.4.tgz#d6b82ead98ae79ebe228e2daf5903311ec982e4d" - integrity sha1-1rgurZiueeviKOLa9ZAzEeyYLk0= +popper.js@^1.16.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" + integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== -portfinder@^1.0.25: - version "1.0.25" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" - integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== +portfinder@^1.0.26: + version "1.0.26" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" + integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== dependencies: async "^2.6.2" debug "^3.1.1" @@ -8553,7 +7925,7 @@ postcss-zindex@^2.0.1: postcss "^5.0.4" uniqs "^2.0.0" -postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.2.16: +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.2.16: version "5.2.18" resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== @@ -8581,7 +7953,7 @@ postcss@^7.0.0: source-map "^0.6.1" supports-color "^6.1.0" -"prelude-ls@>= 0.6.0", prelude-ls@~1.1.2: +prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= @@ -8601,14 +7973,6 @@ prettier@^2.0.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.2.tgz#1ba8f3eb92231e769b7fcd7cb73ae1b6b74ade08" integrity sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg== -pretty-bytes@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84" - integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ= - dependencies: - get-stdin "^4.0.1" - meow "^3.1.0" - pretty-error@^2.0.2: version "2.1.1" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3" @@ -8727,11 +8091,6 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -q@~0.9: - version "0.9.7" - resolved "https://registry.yarnpkg.com/q/-/q-0.9.7.tgz#4de2e6cb3b29088c9e4cbc03bf9d42fb96ce2f75" - integrity sha1-TeLmyzspCIyeTLwDv51C+5bOL3U= - qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -8846,16 +8205,6 @@ read-pkg@^2.0.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@1.1: - version "1.1.13" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" - integrity sha1-9u73ZPUUyJ4rniMUanW6EGdW0j4= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@^3.0.6, readable-stream@^3.1.1: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -8865,16 +8214,6 @@ readable-stream@^3.0.6, readable-stream@^3.1.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@~1.0.2: - version "1.0.34" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" - integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -8918,11 +8257,6 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" -redis@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/redis/-/redis-0.7.3.tgz#ee57b7a44d25ec1594e44365d8165fa7d1d4811a" - integrity sha1-7le3pE0l7BWU5ENl2BZfp9HUgRo= - reduce-css-calc@^1.2.6: version "1.3.0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" @@ -9035,7 +8369,7 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== -repeat-string@^1.5.2, repeat-string@^1.6.1: +repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -9057,11 +8391,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -9132,12 +8461,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@0.5.x: - version "0.5.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.5.1.tgz#15e4a222c4236bcd4cf85454412c2d0fb6524576" - integrity sha1-FeSiIsQja81M+FRUQSwtD7ZSRXY= - -resolve@^1.1.6, resolve@^1.1.7: +resolve@^1.1.6, resolve@^1.1.7, resolve@~1.1.0: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= @@ -9202,13 +8526,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -right-align@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" - integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8= - dependencies: - align-text "^0.1.1" - rimraf@2.6.3, rimraf@~2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" @@ -9216,7 +8533,7 @@ rimraf@2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" -rimraf@2.x.x, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: +rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -9230,18 +8547,6 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" -rimraf@~2.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.1.4.tgz#5a6eb62eeda068f51ede50f29b3e5cd22f3d9bb2" - integrity sha1-Wm62Lu2gaPUe3lDymz5c0i89m7I= - optionalDependencies: - graceful-fs "~1" - -rimraf@~2.2.8: - version "2.2.8" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" - integrity sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI= - ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -9281,7 +8586,7 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= -rxjs@>=6.4.0, rxjs@^6.3.3, rxjs@^6.5.3: +rxjs@^6.3.3, rxjs@^6.5.3: version "6.5.4" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== @@ -9398,11 +8703,6 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@~4.3.3: - version "4.3.6" - resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" - integrity sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto= - send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -9520,11 +8820,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@0.3.x: - version "0.3.0" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" - integrity sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E= - shelljs@^0.7.5: version "0.7.8" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" @@ -9539,19 +8834,6 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -side-channel@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" - integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA== - dependencies: - es-abstract "^1.17.0-next.1" - object-inspect "^1.7.0" - -sigmund@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -9618,27 +8900,6 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socket.io-client@0.9.16: - version "0.9.16" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-0.9.16.tgz#4da7515c5e773041d1b423970415bcc430f35fc6" - integrity sha1-TadRXF53MEHRtCOXBBW8xDDzX8Y= - dependencies: - active-x-obfuscator "0.0.1" - uglify-js "1.2.5" - ws "0.4.x" - xmlhttprequest "1.4.2" - -socket.io@~0.9.13: - version "0.9.19" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-0.9.19.tgz#490bb5fd0dc54cf002ee04e67fadfc43b848a38f" - integrity sha1-SQu1/Q3FTPAC7gTmf638Q7hIo48= - dependencies: - base64id "0.1.0" - policyfile "0.0.4" - socket.io-client "0.9.16" - optionalDependencies: - redis "0.7.3" - sockjs-client@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" @@ -9651,13 +8912,14 @@ sockjs-client@1.4.0: json3 "^3.3.2" url-parse "^1.4.3" -sockjs@0.3.19: - version "0.3.19" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" - integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== +sockjs@0.3.20: + version "0.3.20" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855" + integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA== dependencies: faye-websocket "^0.10.0" - uuid "^3.0.1" + uuid "^3.4.0" + websocket-driver "0.6.5" sort-keys-length@^1.0.0: version "1.0.1" @@ -9709,12 +8971,7 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= -"source-map@>= 0.1.2": - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0, source-map@~0.5.1: +source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -9724,13 +8981,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@~0.1.7: - version "0.1.43" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" - integrity sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y= - dependencies: - amdefine ">=0.0.4" - spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" @@ -9769,10 +9019,10 @@ spdy-transport@^3.0.0: readable-stream "^3.0.6" wbuf "^1.7.3" -spdy@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.1.tgz#6f12ed1c5db7ea4f24ebb8b89ba58c87c08257f2" - integrity sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA== +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== dependencies: debug "^4.1.0" handle-thing "^2.0.0" @@ -9798,6 +9048,11 @@ split-string@^3.0.1, split-string@^3.0.2: version "0.0.7" resolved "https://codeload.github.com/deviantony/splitargs/tar.gz/2a87a1dfb1f9698b94e28e3106ad34057841dbd1" +sprintf-js@^1.0.3: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -9944,11 +9199,6 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -10022,11 +9272,6 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-json-comments@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" - integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E= - strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -10237,11 +9482,6 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -tinycolor@0.x: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tinycolor/-/tinycolor-0.0.1.tgz#320b5a52d83abb5978d81a3e887d4aefb15a6164" - integrity sha1-MgtaUtg6u1l42Bo+iH1K77FaYWQ= - title-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" @@ -10304,9 +9544,10 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -"toastr@github:CodeSeven/toastr#semver:~2.1.3": +toastr@^2.1.4: version "2.1.4" - resolved "https://codeload.github.com/CodeSeven/toastr/tar.gz/1ef00d723691b563b610077a08539391386826b3" + resolved "https://registry.yarnpkg.com/toastr/-/toastr-2.1.4.tgz#8b43be64fb9d0c414871446f2db8e8ca4e95f181" + integrity sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE= dependencies: jquery ">=1.12.0" @@ -10384,11 +9625,6 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -uglify-js@1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-1.2.5.tgz#b542c2c76f78efb34b200b20177634330ff702b6" - integrity sha1-tULCx29477NLIAsgF3Y0Mw/3ArY= - uglify-js@3.4.x: version "3.4.10" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" @@ -10397,16 +9633,6 @@ uglify-js@3.4.x: commander "~2.19.0" source-map "~0.6.1" -uglify-js@^2.4.24: - version "2.8.29" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" - integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0= - dependencies: - source-map "~0.5.1" - yargs "~3.10.0" - optionalDependencies: - uglify-to-browserify "~1.0.0" - uglify-js@^3.1.4: version "3.8.0" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.8.0.tgz#f3541ae97b2f048d7e7e3aa4f39fd8a1f5d7a805" @@ -10415,20 +9641,6 @@ uglify-js@^3.1.4: commander "~2.20.3" source-map "~0.6.1" -uglify-js@~2.3: - version "2.3.6" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.3.6.tgz#fa0984770b428b7a9b2a8058f46355d14fef211a" - integrity sha1-+gmEdwtCi3qbKoBY9GNV0U/vIRo= - dependencies: - async "~0.2.6" - optimist "~0.3.5" - source-map "~0.1.7" - -uglify-to-browserify@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" - integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc= - ui-select@^0.19.8: version "0.19.8" resolved "https://registry.yarnpkg.com/ui-select/-/ui-select-0.19.8.tgz#74860848a7fd8bc494d9856d2f62776ea98637c1" @@ -10447,25 +9659,13 @@ unc-path-regex@^0.1.2: resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= -underscore.string@~2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.2.1.tgz#d7c0fa2af5d5a1a67f4253daee98132e733f0f19" - integrity sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk= - -underscore.string@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.3.3.tgz#71c08bf6b428b1133f37e78fa3a21c82f7329b0d" - integrity sha1-ccCL9rQosRM/N+ePo6Icgvcymw0= - -underscore.string@~2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.4.0.tgz#8cdd8fbac4e2d2ea1e7e2e8097c42f442280f85b" - integrity sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs= - -underscore@~1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" - integrity sha1-a7rwh3UA02vjTsqlhODbn+8DUgk= +underscore.string@~3.3.4: + version "3.3.5" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.5.tgz#fc2ad255b8bd309e239cbc5816fd23a9b7ea4023" + integrity sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg== + dependencies: + sprintf-js "^1.0.3" + util-deprecate "^1.0.2" unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" @@ -10566,11 +9766,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -uri-path@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/uri-path/-/uri-path-0.0.2.tgz#803eb01f2feb17927dcce0f6187e72b75f53f554" - integrity sha1-gD6wHy/rF5J9zOD2GH5yt19T9VQ= - urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" @@ -10637,7 +9832,7 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -10679,24 +9874,12 @@ utila@^0.4.0, utila@~0.4: resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= -utile@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/utile/-/utile-0.2.1.tgz#930c88e99098d6220834c356cbd9a770522d90d7" - integrity sha1-kwyI6ZCY1iIINMNWy9mncFItkNc= - dependencies: - async "~0.2.9" - deep-equal "*" - i "0.3.x" - mkdirp "0.x.x" - ncp "0.4.x" - rimraf "2.x.x" - utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.0.1, uuid@^3.3.2: +uuid@^3.0.1, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -10808,10 +9991,10 @@ webpack-dev-middleware@^3.7.2: range-parser "^1.2.1" webpack-log "^2.0.0" -webpack-dev-server@^3.1.10: - version "3.10.3" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz#f35945036813e57ef582c2420ef7b470e14d3af0" - integrity sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ== +webpack-dev-server@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c" + integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg== dependencies: ansi-html "0.0.7" bonjour "^3.5.0" @@ -10821,31 +10004,31 @@ webpack-dev-server@^3.1.10: debug "^4.1.1" del "^4.1.1" express "^4.17.1" - html-entities "^1.2.1" + html-entities "^1.3.1" http-proxy-middleware "0.19.1" import-local "^2.0.0" internal-ip "^4.3.0" ip "^1.1.5" is-absolute-url "^3.0.3" killable "^1.0.1" - loglevel "^1.6.6" + loglevel "^1.6.8" opn "^5.5.0" p-retry "^3.0.1" - portfinder "^1.0.25" + portfinder "^1.0.26" schema-utils "^1.0.0" selfsigned "^1.10.7" semver "^6.3.0" serve-index "^1.9.1" - sockjs "0.3.19" + sockjs "0.3.20" sockjs-client "1.4.0" - spdy "^4.0.1" + spdy "^4.0.2" strip-ansi "^3.0.1" supports-color "^6.1.0" url "^0.11.0" webpack-dev-middleware "^3.7.2" webpack-log "^2.0.0" ws "^6.2.1" - yargs "12.0.5" + yargs "^13.3.2" webpack-log@^2.0.0: version "2.0.0" @@ -10899,6 +10082,13 @@ webpack@^4.26.0: watchpack "^1.6.0" webpack-sources "^1.4.1" +websocket-driver@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY= + dependencies: + websocket-extensions ">=0.1.1" + websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" @@ -10918,27 +10108,6 @@ whet.extend@~0.9.9: resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" integrity sha1-+HfVv2SMl+WqVC+twW1qJZucEaE= -which-boxed-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" - integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== - dependencies: - is-bigint "^1.0.0" - is-boolean-object "^1.0.0" - is-number-object "^1.0.3" - is-string "^1.0.4" - is-symbol "^1.0.2" - -which-collection@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -10949,12 +10118,7 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= -which@1.0.x, which@~1.0.5: - version "1.0.9" - resolved "https://registry.yarnpkg.com/which/-/which-1.0.9.tgz#460c1da0f810103d0321a9b633af9e575e64486f" - integrity sha1-RgwdoPgQED0DIam2M6+eV15kSG8= - -which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: +which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1, which@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== @@ -10968,22 +10132,12 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -window-size@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" - integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0= - word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" - integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8= - -wordwrap@0.0.x, wordwrap@~0.0.2: +wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= @@ -10995,14 +10149,6 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" @@ -11039,16 +10185,6 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@0.4.x: - version "0.4.32" - resolved "https://registry.yarnpkg.com/ws/-/ws-0.4.32.tgz#787a6154414f3c99ed83c5772153b20feb0cec32" - integrity sha1-eHphVEFPPJntg8V3IVOyD+sM7DI= - dependencies: - commander "~2.1.0" - nan "~1.0.0" - options ">=0.0.5" - tinycolor "0.x" - ws@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" @@ -11056,16 +10192,6 @@ ws@^6.2.1: dependencies: async-limiter "~1.0.0" -xmlbuilder@0.4.2: - version "0.4.2" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.2.tgz#1776d65f3fdbad470a08d8604cdeb1c4e540ff83" - integrity sha1-F3bWXz/brUcKCNhgTN6xxOVA/4M= - -xmlhttprequest@1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.4.2.tgz#01453a1d9bed1e8f172f6495bbf4c8c426321500" - integrity sha1-AUU6HZvtHo8XL2SVu/TIxCYyFQA= - xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -11076,7 +10202,7 @@ xterm@^3.8.0: resolved "https://registry.yarnpkg.com/xterm/-/xterm-3.14.5.tgz#c9d14e48be6873aa46fb429f22f2165557fd2dea" integrity sha512-DVmQ8jlEtL+WbBKUZuMxHMBgK/yeIZwkXB81bH+MGaKKnJGYwA+770hzhXPfwEIokK9On9YIFPRleVp/5G7z9g== -"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: +y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== @@ -11098,15 +10224,7 @@ yaml@^1.7.2: dependencies: "@babel/runtime" "^7.8.7" -yargs-parser@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" - integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs-parser@^13.1.0: +yargs-parser@^13.1.0, yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== @@ -11114,24 +10232,6 @@ yargs-parser@^13.1.0: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@12.0.5: - version "12.0.5" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== - dependencies: - cliui "^4.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" - yargs@13.2.4: version "13.2.4" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" @@ -11149,15 +10249,21 @@ yargs@13.2.4: y18n "^4.0.0" yargs-parser "^13.1.0" -yargs@~3.10.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" - integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E= +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== dependencies: - camelcase "^1.0.2" - cliui "^2.1.0" - decamelize "^1.0.0" - window-size "0.1.0" + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" yauzl@^2.4.2: version "2.10.0" @@ -11166,8 +10272,3 @@ yauzl@^2.4.2: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" - -zeparser@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/zeparser/-/zeparser-0.0.5.tgz#03726561bc268f2e5444f54c665b7fd4a8c029e2" - integrity sha1-A3JlYbwmjy5URPVMZlt/1KjAKeI= From 24888fbbae24f868f7e030b436b70f89884b7479 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 15 Jun 2020 02:48:58 +0300 Subject: [PATCH 031/195] feat(users): prevent the removal of initial admin account (#3912) * feat(users): prevent the removal of initial admin account * feat(users): disabled init admin delete button --- api/http/handler/users/user_delete.go | 5 +++++ .../users-datatable/usersDatatable.html | 2 +- .../datatables/users-datatable/usersDatatable.js | 5 ++++- .../users-datatable/usersDatatableController.js | 15 +++++++++++++++ app/portainer/views/users/edit/user.html | 4 +++- app/portainer/views/users/edit/userController.js | 6 ++++++ 6 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 app/portainer/components/datatables/users-datatable/usersDatatableController.js diff --git a/api/http/handler/users/user_delete.go b/api/http/handler/users/user_delete.go index df72fe673..e192cd055 100644 --- a/api/http/handler/users/user_delete.go +++ b/api/http/handler/users/user_delete.go @@ -1,6 +1,7 @@ package users import ( + "errors" "net/http" httperror "github.com/portainer/libhttp/error" @@ -17,6 +18,10 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err} } + if userID == 1 { + return &httperror.HandlerError{http.StatusForbidden, "Cannot remove the initial admin account", errors.New("Cannot remove the initial admin account")} + } + tokenData, err := security.RetrieveTokenData(r) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err} diff --git a/app/portainer/components/datatables/users-datatable/usersDatatable.html b/app/portainer/components/datatables/users-datatable/usersDatatable.html index 188dc0ba7..06e5f99cd 100644 --- a/app/portainer/components/datatables/users-datatable/usersDatatable.html +++ b/app/portainer/components/datatables/users-datatable/usersDatatable.html @@ -58,7 +58,7 @@ > - + {{ item.Username }} diff --git a/app/portainer/components/datatables/users-datatable/usersDatatable.js b/app/portainer/components/datatables/users-datatable/usersDatatable.js index a3d38637e..258595c80 100644 --- a/app/portainer/components/datatables/users-datatable/usersDatatable.js +++ b/app/portainer/components/datatables/users-datatable/usersDatatable.js @@ -1,6 +1,9 @@ +import angular from 'angular'; +import UsersDatatableController from './usersDatatableController'; + angular.module('portainer.app').component('usersDatatable', { templateUrl: './usersDatatable.html', - controller: 'GenericDatatableController', + controller: UsersDatatableController, bindings: { titleText: '@', titleIcon: '@', diff --git a/app/portainer/components/datatables/users-datatable/usersDatatableController.js b/app/portainer/components/datatables/users-datatable/usersDatatableController.js new file mode 100644 index 000000000..8b8a60605 --- /dev/null +++ b/app/portainer/components/datatables/users-datatable/usersDatatableController.js @@ -0,0 +1,15 @@ +export default class UsersDatatableController { + /* @ngInject*/ + constructor($controller, $scope) { + const allowSelection = this.allowSelection; + angular.extend(this, $controller('GenericDatatableController', { $scope })); + this.allowSelection = allowSelection.bind(this); + } + + /** + * Override this method to allow/deny selection + */ + allowSelection(item) { + return item.Id !== 1; + } +} diff --git a/app/portainer/views/users/edit/user.html b/app/portainer/views/users/edit/user.html index bfc01dca4..f8b339336 100644 --- a/app/portainer/views/users/edit/user.html +++ b/app/portainer/views/users/edit/user.html @@ -34,7 +34,9 @@
- +
diff --git a/app/portainer/views/users/edit/userController.js b/app/portainer/views/users/edit/userController.js index 93b3e6d9d..12aead65d 100644 --- a/app/portainer/views/users/edit/userController.js +++ b/app/portainer/views/users/edit/userController.js @@ -91,6 +91,12 @@ angular.module('portainer.app').controller('UserController', [ return user && (user.Username !== formValues.username || (formValues.Administrator && user.Role !== 1) || (!formValues.Administrator && user.Role === 1)); } + $scope.isDeleteDisabled = isDeleteDisabled; + function isDeleteDisabled() { + const { user } = $scope; + return user && user.Id === 1; + } + function initView() { $scope.isAdmin = Authentication.isAdmin(); From 89fb3c8daebcdc4466b0c997b5cdb1e3dfea8f73 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 15 Jun 2020 13:31:28 +1200 Subject: [PATCH 032/195] feat(pulldog): configure expiry --- pull-dog.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pull-dog.json b/pull-dog.json index eaeed2e08..18d496f89 100644 --- a/pull-dog.json +++ b/pull-dog.json @@ -1,4 +1,5 @@ { "dockerComposeYmlFilePaths": ["docker-compose.pull-dog.yml"], - "isLazy": true + "isLazy": true, + "expiry": "1.00:00:00" } From 5d7ba0baba5c059f934a94bf22f9da1b2079018c Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 16 Jun 2020 10:55:45 +0300 Subject: [PATCH 033/195] feat(edge-compute): add flag to auto enable Edge compute features (#3922) --- api/cli/cli.go | 43 ++++++++++++++++++++------------------- api/cmd/portainer/main.go | 1 + api/portainer.go | 43 ++++++++++++++++++++------------------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/api/cli/cli.go b/api/cli/cli.go index 13e20e94b..fed9b46d0 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -27,27 +27,28 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { kingpin.Version(version) flags := &portainer.CLIFlags{ - Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(), - TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(), - TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).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(), - EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), - NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(), - TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), - TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(), - TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(), - TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(), - TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(), - SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(), - 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(), - SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(), - AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(), - AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), - 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 definitions.").Short('t').String(), + Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(), + TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(), + TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).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(), + EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), + EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(), + NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(), + TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), + TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(), + TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(), + TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(), + TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(), + SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(), + 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(), + SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(), + AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(), + AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), + 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 definitions.").Short('t').String(), } kingpin.Parse() diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 0598b0c6a..55f65bb6b 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -203,6 +203,7 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI settings.LogoURL = *flags.Logo settings.SnapshotInterval = *flags.SnapshotInterval + settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures if *flags.Templates != "" { settings.TemplatesURL = *flags.Templates diff --git a/api/portainer.go b/api/portainer.go index 0f3b3fed0..4182ce66d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -36,27 +36,28 @@ type ( // CLIFlags represents the available flags on the CLI CLIFlags struct { - Addr *string - TunnelAddr *string - TunnelPort *string - AdminPassword *string - AdminPasswordFile *string - Assets *string - Data *string - EndpointURL *string - Labels *[]Pair - Logo *string - NoAnalytics *bool - Templates *string - TLS *bool - TLSSkipVerify *bool - TLSCacert *string - TLSCert *string - TLSKey *string - SSL *bool - SSLCert *string - SSLKey *string - SnapshotInterval *string + Addr *string + TunnelAddr *string + TunnelPort *string + AdminPassword *string + AdminPasswordFile *string + Assets *string + Data *string + EnableEdgeComputeFeatures *bool + EndpointURL *string + Labels *[]Pair + Logo *string + NoAnalytics *bool + Templates *string + TLS *bool + TLSSkipVerify *bool + TLSCacert *string + TLSCert *string + TLSKey *string + SSL *bool + SSLCert *string + SSLKey *string + SnapshotInterval *string } // CLIService represents a service for managing CLI From 7c3b83f6e5e8074a5dc20e4db40951fad2852735 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 16 Jun 2020 10:58:16 +0300 Subject: [PATCH 034/195] refactor(portainer): introduce internal package (#3924) * refactor(auth): move auth helpers to internal package * refactor(edge-compute): move edge helpers to internal package * refactor(tags): move tags helper to internal package * style(portainer): sort imports --- api/authorizations.go | 774 ----------------- api/bolt/datastore.go | 3 +- api/bolt/init.go | 13 +- api/bolt/migrator/migrate_dbversion20.go | 15 +- api/bolt/migrator/migrator.go | 5 +- api/cmd/portainer/main.go | 3 +- api/edgegroup.go | 54 -- api/http/handler/auth/authenticate.go | 3 +- api/http/handler/auth/authenticate_oauth.go | 3 +- api/http/handler/auth/handler.go | 3 +- .../handler/edgegroups/edgegroup_update.go | 9 +- .../handler/edgestacks/edgestack_create.go | 3 +- .../handler/edgestacks/edgestack_delete.go | 3 +- .../handler/edgestacks/edgestack_update.go | 5 +- .../endpointgroups/endpointgroup_update.go | 11 +- api/http/handler/endpointgroups/endpoints.go | 7 +- api/http/handler/endpointgroups/handler.go | 3 +- api/http/handler/endpoints/endpoint_create.go | 3 +- api/http/handler/endpoints/endpoint_update.go | 14 +- api/http/handler/endpoints/handler.go | 3 +- api/http/handler/extensions/handler.go | 3 +- api/http/handler/settings/handler.go | 3 +- api/http/handler/stacks/handler.go | 3 +- api/http/handler/stacks/stack_create.go | 3 +- api/http/handler/stacks/stack_list.go | 5 +- api/http/handler/tags/tag_delete.go | 3 +- api/http/handler/teammemberships/handler.go | 3 +- api/http/handler/teams/handler.go | 3 +- api/http/handler/users/admin_init.go | 3 +- api/http/handler/users/handler.go | 3 +- api/http/handler/users/user_create.go | 3 +- .../proxy/factory/docker/access_control.go | 19 +- api/http/proxy/factory/docker/configs.go | 3 +- api/http/proxy/factory/docker/containers.go | 7 +- api/http/proxy/factory/docker/networks.go | 5 +- api/http/proxy/factory/docker/secrets.go | 3 +- api/http/proxy/factory/docker/services.go | 3 +- api/http/proxy/factory/docker/transport.go | 12 +- api/http/proxy/factory/docker/volumes.go | 5 +- api/http/server.go | 3 +- .../authorization}/access_control.go | 60 +- api/internal/authorization/authorizations.go | 776 ++++++++++++++++++ api/internal/edge/edgegroup.go | 59 ++ api/{ => internal/edge}/edgestack.go | 13 +- api/{ => internal/edge}/endpoint.go | 10 +- api/{ => internal/tag}/tag.go | 28 +- 46 files changed, 1019 insertions(+), 959 deletions(-) delete mode 100644 api/authorizations.go delete mode 100644 api/edgegroup.go rename api/{ => internal/authorization}/access_control.go (66%) create mode 100644 api/internal/authorization/authorizations.go create mode 100644 api/internal/edge/edgegroup.go rename api/{ => internal/edge}/edgestack.go (56%) rename api/{ => internal/edge}/endpoint.go (58%) rename api/{ => internal/tag}/tag.go (54%) diff --git a/api/authorizations.go b/api/authorizations.go deleted file mode 100644 index 78b84b6bf..000000000 --- a/api/authorizations.go +++ /dev/null @@ -1,774 +0,0 @@ -package portainer - -// AuthorizationService represents a service used to -// update authorizations associated to a user or team. -type AuthorizationService struct { - dataStore DataStore -} - -// NewAuthorizationService returns a point to a new AuthorizationService instance. -func NewAuthorizationService(dataStore DataStore) *AuthorizationService { - return &AuthorizationService{ - dataStore: dataStore, - } -} - -// DefaultEndpointAuthorizationsForEndpointAdministratorRole returns the default endpoint authorizations -// associated to the endpoint administrator role. -func DefaultEndpointAuthorizationsForEndpointAdministratorRole() Authorizations { - return map[Authorization]bool{ - OperationDockerContainerArchiveInfo: true, - OperationDockerContainerList: true, - OperationDockerContainerExport: true, - OperationDockerContainerChanges: true, - OperationDockerContainerInspect: true, - OperationDockerContainerTop: true, - OperationDockerContainerLogs: true, - OperationDockerContainerStats: true, - OperationDockerContainerAttachWebsocket: true, - OperationDockerContainerArchive: true, - OperationDockerContainerCreate: true, - OperationDockerContainerPrune: true, - OperationDockerContainerKill: true, - OperationDockerContainerPause: true, - OperationDockerContainerUnpause: true, - OperationDockerContainerRestart: true, - OperationDockerContainerStart: true, - OperationDockerContainerStop: true, - OperationDockerContainerWait: true, - OperationDockerContainerResize: true, - OperationDockerContainerAttach: true, - OperationDockerContainerExec: true, - OperationDockerContainerRename: true, - OperationDockerContainerUpdate: true, - OperationDockerContainerPutContainerArchive: true, - OperationDockerContainerDelete: true, - OperationDockerImageList: true, - OperationDockerImageSearch: true, - OperationDockerImageGetAll: true, - OperationDockerImageGet: true, - OperationDockerImageHistory: true, - OperationDockerImageInspect: true, - OperationDockerImageLoad: true, - OperationDockerImageCreate: true, - OperationDockerImagePrune: true, - OperationDockerImagePush: true, - OperationDockerImageTag: true, - OperationDockerImageDelete: true, - OperationDockerImageCommit: true, - OperationDockerImageBuild: true, - OperationDockerNetworkList: true, - OperationDockerNetworkInspect: true, - OperationDockerNetworkCreate: true, - OperationDockerNetworkConnect: true, - OperationDockerNetworkDisconnect: true, - OperationDockerNetworkPrune: true, - OperationDockerNetworkDelete: true, - OperationDockerVolumeList: true, - OperationDockerVolumeInspect: true, - OperationDockerVolumeCreate: true, - OperationDockerVolumePrune: true, - OperationDockerVolumeDelete: true, - OperationDockerExecInspect: true, - OperationDockerExecStart: true, - OperationDockerExecResize: true, - OperationDockerSwarmInspect: true, - OperationDockerSwarmUnlockKey: true, - OperationDockerSwarmInit: true, - OperationDockerSwarmJoin: true, - OperationDockerSwarmLeave: true, - OperationDockerSwarmUpdate: true, - OperationDockerSwarmUnlock: true, - OperationDockerNodeList: true, - OperationDockerNodeInspect: true, - OperationDockerNodeUpdate: true, - OperationDockerNodeDelete: true, - OperationDockerServiceList: true, - OperationDockerServiceInspect: true, - OperationDockerServiceLogs: true, - OperationDockerServiceCreate: true, - OperationDockerServiceUpdate: true, - OperationDockerServiceDelete: true, - OperationDockerSecretList: true, - OperationDockerSecretInspect: true, - OperationDockerSecretCreate: true, - OperationDockerSecretUpdate: true, - OperationDockerSecretDelete: true, - OperationDockerConfigList: true, - OperationDockerConfigInspect: true, - OperationDockerConfigCreate: true, - OperationDockerConfigUpdate: true, - OperationDockerConfigDelete: true, - OperationDockerTaskList: true, - OperationDockerTaskInspect: true, - OperationDockerTaskLogs: true, - OperationDockerPluginList: true, - OperationDockerPluginPrivileges: true, - OperationDockerPluginInspect: true, - OperationDockerPluginPull: true, - OperationDockerPluginCreate: true, - OperationDockerPluginEnable: true, - OperationDockerPluginDisable: true, - OperationDockerPluginPush: true, - OperationDockerPluginUpgrade: true, - OperationDockerPluginSet: true, - OperationDockerPluginDelete: true, - OperationDockerSessionStart: true, - OperationDockerDistributionInspect: true, - OperationDockerBuildPrune: true, - OperationDockerBuildCancel: true, - OperationDockerPing: true, - OperationDockerInfo: true, - OperationDockerVersion: true, - OperationDockerEvents: true, - OperationDockerSystem: true, - OperationDockerUndefined: true, - OperationDockerAgentPing: true, - OperationDockerAgentList: true, - OperationDockerAgentHostInfo: true, - OperationDockerAgentBrowseDelete: true, - OperationDockerAgentBrowseGet: true, - OperationDockerAgentBrowseList: true, - OperationDockerAgentBrowsePut: true, - OperationDockerAgentBrowseRename: true, - OperationDockerAgentUndefined: true, - OperationPortainerResourceControlCreate: true, - OperationPortainerResourceControlUpdate: true, - OperationPortainerStackList: true, - OperationPortainerStackInspect: true, - OperationPortainerStackFile: true, - OperationPortainerStackCreate: true, - OperationPortainerStackMigrate: true, - OperationPortainerStackUpdate: true, - OperationPortainerStackDelete: true, - OperationPortainerWebsocketExec: true, - OperationPortainerWebhookList: true, - OperationPortainerWebhookCreate: true, - OperationPortainerWebhookDelete: true, - OperationIntegrationStoridgeAdmin: true, - EndpointResourcesAccess: true, - } -} - -// DefaultEndpointAuthorizationsForHelpDeskRole returns the default endpoint authorizations -// associated to the helpdesk role. -func DefaultEndpointAuthorizationsForHelpDeskRole(volumeBrowsingAuthorizations bool) Authorizations { - authorizations := map[Authorization]bool{ - OperationDockerContainerArchiveInfo: true, - OperationDockerContainerList: true, - OperationDockerContainerChanges: true, - OperationDockerContainerInspect: true, - OperationDockerContainerTop: true, - OperationDockerContainerLogs: true, - OperationDockerContainerStats: true, - OperationDockerImageList: true, - OperationDockerImageSearch: true, - OperationDockerImageGetAll: true, - OperationDockerImageGet: true, - OperationDockerImageHistory: true, - OperationDockerImageInspect: true, - OperationDockerNetworkList: true, - OperationDockerNetworkInspect: true, - OperationDockerVolumeList: true, - OperationDockerVolumeInspect: true, - OperationDockerSwarmInspect: true, - OperationDockerNodeList: true, - OperationDockerNodeInspect: true, - OperationDockerServiceList: true, - OperationDockerServiceInspect: true, - OperationDockerServiceLogs: true, - OperationDockerSecretList: true, - OperationDockerSecretInspect: true, - OperationDockerConfigList: true, - OperationDockerConfigInspect: true, - OperationDockerTaskList: true, - OperationDockerTaskInspect: true, - OperationDockerTaskLogs: true, - OperationDockerPluginList: true, - OperationDockerDistributionInspect: true, - OperationDockerPing: true, - OperationDockerInfo: true, - OperationDockerVersion: true, - OperationDockerEvents: true, - OperationDockerSystem: true, - OperationDockerAgentPing: true, - OperationDockerAgentList: true, - OperationDockerAgentHostInfo: true, - OperationPortainerStackList: true, - OperationPortainerStackInspect: true, - OperationPortainerStackFile: true, - OperationPortainerWebhookList: true, - EndpointResourcesAccess: true, - } - - if volumeBrowsingAuthorizations { - authorizations[OperationDockerAgentBrowseGet] = true - authorizations[OperationDockerAgentBrowseList] = true - } - - return authorizations -} - -// DefaultEndpointAuthorizationsForStandardUserRole returns the default endpoint authorizations -// associated to the standard user role. -func DefaultEndpointAuthorizationsForStandardUserRole(volumeBrowsingAuthorizations bool) Authorizations { - authorizations := map[Authorization]bool{ - OperationDockerContainerArchiveInfo: true, - OperationDockerContainerList: true, - OperationDockerContainerExport: true, - OperationDockerContainerChanges: true, - OperationDockerContainerInspect: true, - OperationDockerContainerTop: true, - OperationDockerContainerLogs: true, - OperationDockerContainerStats: true, - OperationDockerContainerAttachWebsocket: true, - OperationDockerContainerArchive: true, - OperationDockerContainerCreate: true, - OperationDockerContainerKill: true, - OperationDockerContainerPause: true, - OperationDockerContainerUnpause: true, - OperationDockerContainerRestart: true, - OperationDockerContainerStart: true, - OperationDockerContainerStop: true, - OperationDockerContainerWait: true, - OperationDockerContainerResize: true, - OperationDockerContainerAttach: true, - OperationDockerContainerExec: true, - OperationDockerContainerRename: true, - OperationDockerContainerUpdate: true, - OperationDockerContainerPutContainerArchive: true, - OperationDockerContainerDelete: true, - OperationDockerImageList: true, - OperationDockerImageSearch: true, - OperationDockerImageGetAll: true, - OperationDockerImageGet: true, - OperationDockerImageHistory: true, - OperationDockerImageInspect: true, - OperationDockerImageLoad: true, - OperationDockerImageCreate: true, - OperationDockerImagePush: true, - OperationDockerImageTag: true, - OperationDockerImageDelete: true, - OperationDockerImageCommit: true, - OperationDockerImageBuild: true, - OperationDockerNetworkList: true, - OperationDockerNetworkInspect: true, - OperationDockerNetworkCreate: true, - OperationDockerNetworkConnect: true, - OperationDockerNetworkDisconnect: true, - OperationDockerNetworkDelete: true, - OperationDockerVolumeList: true, - OperationDockerVolumeInspect: true, - OperationDockerVolumeCreate: true, - OperationDockerVolumeDelete: true, - OperationDockerExecInspect: true, - OperationDockerExecStart: true, - OperationDockerExecResize: true, - OperationDockerSwarmInspect: true, - OperationDockerSwarmUnlockKey: true, - OperationDockerSwarmInit: true, - OperationDockerSwarmJoin: true, - OperationDockerSwarmLeave: true, - OperationDockerSwarmUpdate: true, - OperationDockerSwarmUnlock: true, - OperationDockerNodeList: true, - OperationDockerNodeInspect: true, - OperationDockerNodeUpdate: true, - OperationDockerNodeDelete: true, - OperationDockerServiceList: true, - OperationDockerServiceInspect: true, - OperationDockerServiceLogs: true, - OperationDockerServiceCreate: true, - OperationDockerServiceUpdate: true, - OperationDockerServiceDelete: true, - OperationDockerSecretList: true, - OperationDockerSecretInspect: true, - OperationDockerSecretCreate: true, - OperationDockerSecretUpdate: true, - OperationDockerSecretDelete: true, - OperationDockerConfigList: true, - OperationDockerConfigInspect: true, - OperationDockerConfigCreate: true, - OperationDockerConfigUpdate: true, - OperationDockerConfigDelete: true, - OperationDockerTaskList: true, - OperationDockerTaskInspect: true, - OperationDockerTaskLogs: true, - OperationDockerPluginList: true, - OperationDockerPluginPrivileges: true, - OperationDockerPluginInspect: true, - OperationDockerPluginPull: true, - OperationDockerPluginCreate: true, - OperationDockerPluginEnable: true, - OperationDockerPluginDisable: true, - OperationDockerPluginPush: true, - OperationDockerPluginUpgrade: true, - OperationDockerPluginSet: true, - OperationDockerPluginDelete: true, - OperationDockerSessionStart: true, - OperationDockerDistributionInspect: true, - OperationDockerBuildPrune: true, - OperationDockerBuildCancel: true, - OperationDockerPing: true, - OperationDockerInfo: true, - OperationDockerVersion: true, - OperationDockerEvents: true, - OperationDockerSystem: true, - OperationDockerUndefined: true, - OperationDockerAgentPing: true, - OperationDockerAgentList: true, - OperationDockerAgentHostInfo: true, - OperationDockerAgentUndefined: true, - OperationPortainerResourceControlUpdate: true, - OperationPortainerStackList: true, - OperationPortainerStackInspect: true, - OperationPortainerStackFile: true, - OperationPortainerStackCreate: true, - OperationPortainerStackMigrate: true, - OperationPortainerStackUpdate: true, - OperationPortainerStackDelete: true, - OperationPortainerWebsocketExec: true, - OperationPortainerWebhookList: true, - OperationPortainerWebhookCreate: true, - } - - if volumeBrowsingAuthorizations { - authorizations[OperationDockerAgentBrowseGet] = true - authorizations[OperationDockerAgentBrowseList] = true - authorizations[OperationDockerAgentBrowseDelete] = true - authorizations[OperationDockerAgentBrowsePut] = true - authorizations[OperationDockerAgentBrowseRename] = true - } - - return authorizations -} - -// DefaultEndpointAuthorizationsForReadOnlyUserRole returns the default endpoint authorizations -// associated to the readonly user role. -func DefaultEndpointAuthorizationsForReadOnlyUserRole(volumeBrowsingAuthorizations bool) Authorizations { - authorizations := map[Authorization]bool{ - OperationDockerContainerArchiveInfo: true, - OperationDockerContainerList: true, - OperationDockerContainerChanges: true, - OperationDockerContainerInspect: true, - OperationDockerContainerTop: true, - OperationDockerContainerLogs: true, - OperationDockerContainerStats: true, - OperationDockerImageList: true, - OperationDockerImageSearch: true, - OperationDockerImageGetAll: true, - OperationDockerImageGet: true, - OperationDockerImageHistory: true, - OperationDockerImageInspect: true, - OperationDockerNetworkList: true, - OperationDockerNetworkInspect: true, - OperationDockerVolumeList: true, - OperationDockerVolumeInspect: true, - OperationDockerSwarmInspect: true, - OperationDockerNodeList: true, - OperationDockerNodeInspect: true, - OperationDockerServiceList: true, - OperationDockerServiceInspect: true, - OperationDockerServiceLogs: true, - OperationDockerSecretList: true, - OperationDockerSecretInspect: true, - OperationDockerConfigList: true, - OperationDockerConfigInspect: true, - OperationDockerTaskList: true, - OperationDockerTaskInspect: true, - OperationDockerTaskLogs: true, - OperationDockerPluginList: true, - OperationDockerDistributionInspect: true, - OperationDockerPing: true, - OperationDockerInfo: true, - OperationDockerVersion: true, - OperationDockerEvents: true, - OperationDockerSystem: true, - OperationDockerAgentPing: true, - OperationDockerAgentList: true, - OperationDockerAgentHostInfo: true, - OperationPortainerStackList: true, - OperationPortainerStackInspect: true, - OperationPortainerStackFile: true, - OperationPortainerWebhookList: true, - } - - if volumeBrowsingAuthorizations { - authorizations[OperationDockerAgentBrowseGet] = true - authorizations[OperationDockerAgentBrowseList] = true - } - - return authorizations -} - -// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users. -func DefaultPortainerAuthorizations() Authorizations { - return map[Authorization]bool{ - OperationPortainerDockerHubInspect: true, - OperationPortainerEndpointGroupList: true, - OperationPortainerEndpointList: true, - OperationPortainerEndpointInspect: true, - OperationPortainerEndpointExtensionAdd: true, - OperationPortainerEndpointExtensionRemove: true, - OperationPortainerExtensionList: true, - OperationPortainerMOTD: true, - OperationPortainerRegistryList: true, - OperationPortainerRegistryInspect: true, - OperationPortainerTeamList: true, - OperationPortainerTemplateList: true, - OperationPortainerTemplateInspect: true, - OperationPortainerUserList: true, - OperationPortainerUserInspect: true, - OperationPortainerUserMemberships: true, - } -} - -// UpdateVolumeBrowsingAuthorizations will update all the volume browsing authorizations for each role (except endpoint administrator) -// based on the specified removeAuthorizations parameter. If removeAuthorizations is set to true, all -// the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations -// will be reset based for each role. -func (service AuthorizationService) UpdateVolumeBrowsingAuthorizations(remove bool) error { - roles, err := service.dataStore.Role().Roles() - if err != nil { - return err - } - - for _, role := range roles { - // all roles except endpoint administrator - if role.ID != RoleID(1) { - updateRoleVolumeBrowsingAuthorizations(&role, remove) - - err := service.dataStore.Role().UpdateRole(role.ID, &role) - if err != nil { - return err - } - } - } - - return nil -} - -func updateRoleVolumeBrowsingAuthorizations(role *Role, removeAuthorizations bool) { - if !removeAuthorizations { - delete(role.Authorizations, OperationDockerAgentBrowseDelete) - delete(role.Authorizations, OperationDockerAgentBrowseGet) - delete(role.Authorizations, OperationDockerAgentBrowseList) - delete(role.Authorizations, OperationDockerAgentBrowsePut) - delete(role.Authorizations, OperationDockerAgentBrowseRename) - return - } - - role.Authorizations[OperationDockerAgentBrowseGet] = true - role.Authorizations[OperationDockerAgentBrowseList] = true - - // Standard-user - if role.ID == RoleID(3) { - role.Authorizations[OperationDockerAgentBrowseDelete] = true - role.Authorizations[OperationDockerAgentBrowsePut] = true - role.Authorizations[OperationDockerAgentBrowseRename] = true - } -} - -// RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team -func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) error { - endpoints, err := service.dataStore.Endpoint().Endpoints() - if err != nil { - return err - } - - for _, endpoint := range endpoints { - for policyTeamID := range endpoint.TeamAccessPolicies { - if policyTeamID == teamID { - delete(endpoint.TeamAccessPolicies, policyTeamID) - - err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) - if err != nil { - return err - } - - break - } - } - } - - endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() - if err != nil { - return err - } - - for _, endpointGroup := range endpointGroups { - for policyTeamID := range endpointGroup.TeamAccessPolicies { - if policyTeamID == teamID { - delete(endpointGroup.TeamAccessPolicies, policyTeamID) - - err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) - if err != nil { - return err - } - - break - } - } - } - - registries, err := service.dataStore.Registry().Registries() - if err != nil { - return err - } - - for _, registry := range registries { - for policyTeamID := range registry.TeamAccessPolicies { - if policyTeamID == teamID { - delete(registry.TeamAccessPolicies, policyTeamID) - - err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry) - if err != nil { - return err - } - - break - } - } - } - - return service.UpdateUsersAuthorizations() -} - -// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user -func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) error { - endpoints, err := service.dataStore.Endpoint().Endpoints() - if err != nil { - return err - } - - for _, endpoint := range endpoints { - for policyUserID := range endpoint.UserAccessPolicies { - if policyUserID == userID { - delete(endpoint.UserAccessPolicies, policyUserID) - - err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) - if err != nil { - return err - } - - break - } - } - } - - endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() - if err != nil { - return err - } - - for _, endpointGroup := range endpointGroups { - for policyUserID := range endpointGroup.UserAccessPolicies { - if policyUserID == userID { - delete(endpointGroup.UserAccessPolicies, policyUserID) - - err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) - if err != nil { - return err - } - - break - } - } - } - - registries, err := service.dataStore.Registry().Registries() - if err != nil { - return err - } - - for _, registry := range registries { - for policyUserID := range registry.UserAccessPolicies { - if policyUserID == userID { - delete(registry.UserAccessPolicies, policyUserID) - - err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry) - if err != nil { - return err - } - - break - } - } - } - - return nil -} - -// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users. -func (service *AuthorizationService) UpdateUsersAuthorizations() error { - users, err := service.dataStore.User().Users() - if err != nil { - return err - } - - for _, user := range users { - err := service.updateUserAuthorizations(user.ID) - if err != nil { - return err - } - } - - return nil -} - -func (service *AuthorizationService) updateUserAuthorizations(userID UserID) error { - user, err := service.dataStore.User().User(userID) - if err != nil { - return err - } - - endpointAuthorizations, err := service.getAuthorizations(user) - if err != nil { - return err - } - - user.EndpointAuthorizations = endpointAuthorizations - - return service.dataStore.User().UpdateUser(userID, user) -} - -func (service *AuthorizationService) getAuthorizations(user *User) (EndpointAuthorizations, error) { - endpointAuthorizations := EndpointAuthorizations{} - if user.Role == AdministratorRole { - return endpointAuthorizations, nil - } - - userMemberships, err := service.dataStore.TeamMembership().TeamMembershipsByUserID(user.ID) - if err != nil { - return endpointAuthorizations, err - } - - endpoints, err := service.dataStore.Endpoint().Endpoints() - if err != nil { - return endpointAuthorizations, err - } - - endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() - if err != nil { - return endpointAuthorizations, err - } - - roles, err := service.dataStore.Role().Roles() - if err != nil { - return endpointAuthorizations, err - } - - endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships) - - return endpointAuthorizations, nil -} - -func getUserEndpointAuthorizations(user *User, endpoints []Endpoint, endpointGroups []EndpointGroup, roles []Role, userMemberships []TeamMembership) EndpointAuthorizations { - endpointAuthorizations := make(EndpointAuthorizations) - - groupUserAccessPolicies := map[EndpointGroupID]UserAccessPolicies{} - groupTeamAccessPolicies := map[EndpointGroupID]TeamAccessPolicies{} - for _, endpointGroup := range endpointGroups { - groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies - groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies - } - - for _, endpoint := range endpoints { - authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles) - if len(authorizations) > 0 { - endpointAuthorizations[endpoint.ID] = authorizations - continue - } - - authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies) - if len(authorizations) > 0 { - endpointAuthorizations[endpoint.ID] = authorizations - continue - } - - authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles) - if len(authorizations) > 0 { - endpointAuthorizations[endpoint.ID] = authorizations - continue - } - - authorizations = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies) - if len(authorizations) > 0 { - endpointAuthorizations[endpoint.ID] = authorizations - } - } - - return endpointAuthorizations -} - -func getAuthorizationsFromUserEndpointPolicy(user *User, endpoint *Endpoint, roles []Role) Authorizations { - policyRoles := make([]RoleID, 0) - - policy, ok := endpoint.UserAccessPolicies[user.ID] - if ok { - policyRoles = append(policyRoles, policy.RoleID) - } - - return getAuthorizationsFromRoles(policyRoles, roles) -} - -func getAuthorizationsFromUserEndpointGroupPolicy(user *User, endpoint *Endpoint, roles []Role, groupAccessPolicies map[EndpointGroupID]UserAccessPolicies) Authorizations { - policyRoles := make([]RoleID, 0) - - policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID] - if ok { - policyRoles = append(policyRoles, policy.RoleID) - } - - return getAuthorizationsFromRoles(policyRoles, roles) -} - -func getAuthorizationsFromTeamEndpointPolicies(memberships []TeamMembership, endpoint *Endpoint, roles []Role) Authorizations { - policyRoles := make([]RoleID, 0) - - for _, membership := range memberships { - policy, ok := endpoint.TeamAccessPolicies[membership.TeamID] - if ok { - policyRoles = append(policyRoles, policy.RoleID) - } - } - - return getAuthorizationsFromRoles(policyRoles, roles) -} - -func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []TeamMembership, endpoint *Endpoint, roles []Role, groupAccessPolicies map[EndpointGroupID]TeamAccessPolicies) Authorizations { - policyRoles := make([]RoleID, 0) - - for _, membership := range memberships { - policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID] - if ok { - policyRoles = append(policyRoles, policy.RoleID) - } - } - - return getAuthorizationsFromRoles(policyRoles, roles) -} - -func getAuthorizationsFromRoles(roleIdentifiers []RoleID, roles []Role) Authorizations { - var associatedRoles []Role - - for _, id := range roleIdentifiers { - for _, role := range roles { - if role.ID == id { - associatedRoles = append(associatedRoles, role) - break - } - } - } - - var authorizations Authorizations - highestPriority := 0 - for _, role := range associatedRoles { - if role.Priority > highestPriority { - highestPriority = role.Priority - authorizations = role.Authorizations - } - } - - return authorizations -} diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 476082604..05ec10d10 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -28,6 +28,7 @@ import ( "github.com/portainer/portainer/api/bolt/user" "github.com/portainer/portainer/api/bolt/version" "github.com/portainer/portainer/api/bolt/webhook" + "github.com/portainer/portainer/api/internal/authorization" ) const ( @@ -143,7 +144,7 @@ func (store *Store) MigrateData() error { UserService: store.UserService, VersionService: store.VersionService, FileService: store.fileService, - AuthorizationService: portainer.NewAuthorizationService(store), + AuthorizationService: authorization.NewService(store), } migrator := migrator.NewMigrator(migratorParams) diff --git a/api/bolt/init.go b/api/bolt/init.go index 53fb28044..df6277ed8 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -1,6 +1,9 @@ package bolt -import portainer "github.com/portainer/portainer/api" +import ( + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/authorization" +) // Init creates the default data set. func (store *Store) Init() error { @@ -85,7 +88,7 @@ func (store *Store) Init() error { Name: "Endpoint administrator", Description: "Full control of all resources in an endpoint", Priority: 1, - Authorizations: portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole(), + Authorizations: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(), } err = store.RoleService.CreateRole(environmentAdministratorRole) @@ -97,7 +100,7 @@ func (store *Store) Init() error { Name: "Helpdesk", Description: "Read-only access of all resources in an endpoint", Priority: 2, - Authorizations: portainer.DefaultEndpointAuthorizationsForHelpDeskRole(false), + Authorizations: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(false), } err = store.RoleService.CreateRole(environmentReadOnlyUserRole) @@ -109,7 +112,7 @@ func (store *Store) Init() error { Name: "Standard user", Description: "Full control of assigned resources in an endpoint", Priority: 3, - Authorizations: portainer.DefaultEndpointAuthorizationsForStandardUserRole(false), + Authorizations: authorization.DefaultEndpointAuthorizationsForStandardUserRole(false), } err = store.RoleService.CreateRole(standardUserRole) @@ -121,7 +124,7 @@ func (store *Store) Init() error { Name: "Read-only user", Description: "Read-only access of assigned resources in an endpoint", Priority: 4, - Authorizations: portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(false), + Authorizations: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(false), } err = store.RoleService.CreateRole(readOnlyUserRole) diff --git a/api/bolt/migrator/migrate_dbversion20.go b/api/bolt/migrator/migrate_dbversion20.go index df272905a..23ddd82ab 100644 --- a/api/bolt/migrator/migrate_dbversion20.go +++ b/api/bolt/migrator/migrate_dbversion20.go @@ -1,6 +1,9 @@ package migrator -import portainer "github.com/portainer/portainer/api" +import ( + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/authorization" +) func (m *Migrator) updateResourceControlsToDBVersion22() error { legacyResourceControls, err := m.resourceControlService.ResourceControls() @@ -32,7 +35,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error { } for _, user := range legacyUsers { - user.PortainerAuthorizations = portainer.DefaultPortainerAuthorizations() + user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations() err = m.userService.UpdateUser(user.ID, &user) if err != nil { return err @@ -44,7 +47,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error { return err } endpointAdministratorRole.Priority = 1 - endpointAdministratorRole.Authorizations = portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole() + endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole() err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole) @@ -53,7 +56,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error { return err } helpDeskRole.Priority = 2 - helpDeskRole.Authorizations = portainer.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers) + helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers) err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole) @@ -62,7 +65,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error { return err } standardUserRole.Priority = 3 - standardUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers) + standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers) err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole) @@ -71,7 +74,7 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error { return err } readOnlyUserRole.Priority = 4 - readOnlyUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers) + readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers) err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole) if err != nil { diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index ed84b52e5..a933b6519 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -17,6 +17,7 @@ import ( "github.com/portainer/portainer/api/bolt/teammembership" "github.com/portainer/portainer/api/bolt/user" "github.com/portainer/portainer/api/bolt/version" + "github.com/portainer/portainer/api/internal/authorization" ) type ( @@ -39,7 +40,7 @@ type ( userService *user.Service versionService *version.Service fileService portainer.FileService - authorizationService *portainer.AuthorizationService + authorizationService *authorization.Service } // Parameters represents the required parameters to create a new Migrator instance. @@ -61,7 +62,7 @@ type ( UserService *user.Service VersionService *version.Service FileService portainer.FileService - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service } ) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 55f65bb6b..3ec42000c 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -7,6 +7,7 @@ import ( "time" "github.com/portainer/portainer/api/chisel" + "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt" @@ -486,7 +487,7 @@ func main() { Username: "admin", Role: portainer.AdministratorRole, Password: adminPasswordHash, - PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), + PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(), } err := dataStore.User().CreateUser(user) if err != nil { diff --git a/api/edgegroup.go b/api/edgegroup.go deleted file mode 100644 index d68ee7a9b..000000000 --- a/api/edgegroup.go +++ /dev/null @@ -1,54 +0,0 @@ -package portainer - -// EdgeGroupRelatedEndpoints returns a list of endpoints related to this Edge group -func EdgeGroupRelatedEndpoints(edgeGroup *EdgeGroup, endpoints []Endpoint, endpointGroups []EndpointGroup) []EndpointID { - if !edgeGroup.Dynamic { - return edgeGroup.Endpoints - } - - endpointIDs := []EndpointID{} - for _, endpoint := range endpoints { - if endpoint.Type != EdgeAgentEnvironment { - continue - } - - var endpointGroup EndpointGroup - for _, group := range endpointGroups { - if endpoint.GroupID == group.ID { - endpointGroup = group - break - } - } - - if edgeGroupRelatedToEndpoint(edgeGroup, &endpoint, &endpointGroup) { - endpointIDs = append(endpointIDs, endpoint.ID) - } - } - - return endpointIDs -} - -// edgeGroupRelatedToEndpoint returns true is edgeGroup is associated with endpoint -func edgeGroupRelatedToEndpoint(edgeGroup *EdgeGroup, endpoint *Endpoint, endpointGroup *EndpointGroup) bool { - if !edgeGroup.Dynamic { - for _, endpointID := range edgeGroup.Endpoints { - if endpoint.ID == endpointID { - return true - } - } - return false - } - - endpointTags := TagSet(endpoint.TagIDs) - if endpointGroup.TagIDs != nil { - endpointTags = TagUnion(endpointTags, TagSet(endpointGroup.TagIDs)) - } - edgeGroupTags := TagSet(edgeGroup.TagIDs) - - if edgeGroup.PartialMatch { - intersection := TagIntersection(endpointTags, edgeGroupTags) - return len(intersection) != 0 - } - - return TagContains(edgeGroupTags, endpointTags) -} diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index 6a1d47308..eb9592566 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -10,6 +10,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/authorization" ) type authenticatePayload struct { @@ -101,7 +102,7 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use user := &portainer.User{ Username: username, Role: portainer.StandardUserRole, - PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), + PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(), } err = handler.DataStore.User().CreateUser(user) diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index 757f3fd76..68f998495 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -2,6 +2,7 @@ package auth import ( "encoding/json" + "github.com/portainer/portainer/api/internal/authorization" "io/ioutil" "log" "net/http" @@ -113,7 +114,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h user = &portainer.User{ Username: username, Role: portainer.StandardUserRole, - PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), + PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(), } err = handler.DataStore.User().CreateUser(user) diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index bfb33c6f9..1f8415597 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) // Handler is the HTTP handler used to handle authentication operations. @@ -18,7 +19,7 @@ type Handler struct { JWTService portainer.JWTService LDAPService portainer.LDAPService ProxyManager *proxy.Manager - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service } // NewHandler creates a handler to manage authentication operations. diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index 6c227b11b..c2c4e36d2 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -7,7 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/edge" ) type edgeGroupUpdatePayload struct { @@ -73,7 +74,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err} } - oldRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups) + oldRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups) edgeGroup.Dynamic = payload.Dynamic if edgeGroup.Dynamic { @@ -102,7 +103,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge group changes inside the database", err} } - newRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups) + newRelatedEndpoints := edge.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups) endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...) for _, endpointID := range endpointsToUpdate { @@ -143,7 +144,7 @@ func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error { edgeStackSet := map[portainer.EdgeStackID]bool{} - endpointEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) + endpointEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) for _, edgeStackID := range endpointEdgeStacks { edgeStackSet[edgeStackID] = true } diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index 27f7ce87b..e6e96f02d 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -13,6 +13,7 @@ import ( "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/internal/edge" ) // POST request on /api/endpoint_groups @@ -42,7 +43,7 @@ func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} } - relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups) + relatedEndpoints, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups) for _, endpointID := range relatedEndpoints { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) diff --git a/api/http/handler/edgestacks/edgestack_delete.go b/api/http/handler/edgestacks/edgestack_delete.go index cec2a3c82..641d2d66e 100644 --- a/api/http/handler/edgestacks/edgestack_delete.go +++ b/api/http/handler/edgestacks/edgestack_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/edge" ) func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { @@ -42,7 +43,7 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} } - relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups) + relatedEndpoints, err := edge.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups) for _, endpointID := range relatedEndpoints { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpointID) diff --git a/api/http/handler/edgestacks/edgestack_update.go b/api/http/handler/edgestacks/edgestack_update.go index 3d7ac1ddc..39edc92e1 100644 --- a/api/http/handler/edgestacks/edgestack_update.go +++ b/api/http/handler/edgestacks/edgestack_update.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/edge" ) type updateEdgeStackPayload struct { @@ -63,12 +64,12 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err} } - oldRelated, err := portainer.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups) + oldRelated, err := edge.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err} } - newRelated, err := portainer.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups) + newRelated, err := edge.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err} } diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index e7d00d41c..c03215518 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/tag" ) type endpointGroupUpdatePayload struct { @@ -52,14 +53,14 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque tagsChanged := false if payload.TagIDs != nil { - payloadTagSet := portainer.TagSet(payload.TagIDs) - endpointGroupTagSet := portainer.TagSet((endpointGroup.TagIDs)) - union := portainer.TagUnion(payloadTagSet, endpointGroupTagSet) - intersection := portainer.TagIntersection(payloadTagSet, endpointGroupTagSet) + payloadTagSet := tag.Set(payload.TagIDs) + endpointGroupTagSet := tag.Set((endpointGroup.TagIDs)) + union := tag.Union(payloadTagSet, endpointGroupTagSet) + intersection := tag.Intersection(payloadTagSet, endpointGroupTagSet) tagsChanged = len(union) > len(intersection) if tagsChanged { - removeTags := portainer.TagDifference(endpointGroupTagSet, payloadTagSet) + removeTags := tag.Difference(endpointGroupTagSet, payloadTagSet) for tagID := range removeTags { tag, err := handler.DataStore.Tag().Tag(tagID) diff --git a/api/http/handler/endpointgroups/endpoints.go b/api/http/handler/endpointgroups/endpoints.go index 3ff3096cb..e854ca635 100644 --- a/api/http/handler/endpointgroups/endpoints.go +++ b/api/http/handler/endpointgroups/endpoints.go @@ -1,6 +1,9 @@ package endpointgroups -import portainer "github.com/portainer/portainer/api" +import ( + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/edge" +) func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) error { if endpoint.Type != portainer.EdgeAgentEnvironment { @@ -31,7 +34,7 @@ func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, en return err } - endpointStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) + endpointStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) stacksSet := map[portainer.EdgeStackID]bool{} for _, edgeStackID := range endpointStacks { stacksSet[edgeStackID] = true diff --git a/api/http/handler/endpointgroups/handler.go b/api/http/handler/endpointgroups/handler.go index 9730e875d..8a6586cb9 100644 --- a/api/http/handler/endpointgroups/handler.go +++ b/api/http/handler/endpointgroups/handler.go @@ -7,13 +7,14 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) // Handler is the HTTP handler used to handle endpoint group operations. type Handler struct { *mux.Router DataStore portainer.DataStore - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service } // NewHandler creates a handler to manage endpoint group operations. diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 677ce6222..a2b90c1cd 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -15,6 +15,7 @@ import ( "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/client" + "github.com/portainer/portainer/api/internal/edge" ) type endpointCreatePayload struct { @@ -167,7 +168,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * } if endpoint.Type == portainer.EdgeAgentEnvironment { - relatedEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) + relatedEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) for _, stackID := range relatedEdgeStacks { relationObject.EdgeStacks[stackID] = true } diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 766c09207..bf73d9320 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -10,6 +10,8 @@ import ( "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/client" + "github.com/portainer/portainer/api/internal/edge" + "github.com/portainer/portainer/api/internal/tag" ) type endpointUpdatePayload struct { @@ -79,14 +81,14 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * tagsChanged := false if payload.TagIDs != nil { - payloadTagSet := portainer.TagSet(payload.TagIDs) - endpointTagSet := portainer.TagSet((endpoint.TagIDs)) - union := portainer.TagUnion(payloadTagSet, endpointTagSet) - intersection := portainer.TagIntersection(payloadTagSet, endpointTagSet) + payloadTagSet := tag.Set(payload.TagIDs) + endpointTagSet := tag.Set((endpoint.TagIDs)) + union := tag.Union(payloadTagSet, endpointTagSet) + intersection := tag.Intersection(payloadTagSet, endpointTagSet) tagsChanged = len(union) > len(intersection) if tagsChanged { - removeTags := portainer.TagDifference(endpointTagSet, payloadTagSet) + removeTags := tag.Difference(endpointTagSet, payloadTagSet) for tagID := range removeTags { tag, err := handler.DataStore.Tag().Tag(tagID) @@ -248,7 +250,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * edgeStackSet := map[portainer.EdgeStackID]bool{} - endpointEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) + endpointEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) for _, edgeStackID := range endpointEdgeStacks { edgeStackSet[edgeStackID] = true } diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 0fb6e5b06..1d9ca9f03 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -5,6 +5,7 @@ import ( "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" "net/http" @@ -23,7 +24,7 @@ type Handler struct { *mux.Router requestBouncer *security.RequestBouncer DataStore portainer.DataStore - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service FileService portainer.FileService JobService portainer.JobService ProxyManager *proxy.Manager diff --git a/api/http/handler/extensions/handler.go b/api/http/handler/extensions/handler.go index cba6b354c..b33c23881 100644 --- a/api/http/handler/extensions/handler.go +++ b/api/http/handler/extensions/handler.go @@ -9,6 +9,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) // Handler is the HTTP handler used to handle extension operations. @@ -16,7 +17,7 @@ type Handler struct { *mux.Router DataStore portainer.DataStore ExtensionManager portainer.ExtensionManager - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service } // NewHandler creates a handler to manage extension operations. diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index 9bbe1cf52..8534b1867 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -1,6 +1,7 @@ package settings import ( + "github.com/portainer/portainer/api/internal/authorization" "net/http" "github.com/gorilla/mux" @@ -17,7 +18,7 @@ func hideFields(settings *portainer.Settings) { // Handler is the HTTP handler used to handle settings operations. type Handler struct { *mux.Router - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service DataStore portainer.DataStore FileService portainer.FileService JobScheduler portainer.JobScheduler diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index 6722feaa8..f0a6fc0bf 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -8,6 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) // Handler is the HTTP handler used to handle stack operations. @@ -58,7 +59,7 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR userTeamIDs = append(userTeamIDs, membership.TeamID) } - if resourceControl != nil && portainer.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) { + if resourceControl != nil && authorization.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) { return true, nil } diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 7f3244520..c57464c2c 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -12,6 +12,7 @@ import ( "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error { @@ -133,7 +134,7 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) } func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError { - resourceControl := portainer.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID) + resourceControl := authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID) err := handler.DataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { diff --git a/api/http/handler/stacks/stack_list.go b/api/http/handler/stacks/stack_list.go index eab7d7ca1..047740397 100644 --- a/api/http/handler/stacks/stack_list.go +++ b/api/http/handler/stacks/stack_list.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) type stackListOperationFilters struct { @@ -39,7 +40,7 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} } - stacks = portainer.DecorateStacks(stacks, resourceControls) + stacks = authorization.DecorateStacks(stacks, resourceControls) if !securityContext.IsAdmin { rbacExtensionEnabled := true @@ -60,7 +61,7 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe userTeamIDs = append(userTeamIDs, membership.TeamID) } - stacks = portainer.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled) + stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled) } return response.JSON(w, stacks) diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index 52dac6a9c..ce6a3840b 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/edge" ) // DELETE request on /api/tags/:id @@ -111,7 +112,7 @@ func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edg return err } - endpointStacks := portainer.EndpointRelatedEdgeStacks(&endpoint, endpointGroup, edgeGroups, edgeStacks) + endpointStacks := edge.EndpointRelatedEdgeStacks(&endpoint, endpointGroup, edgeGroups, edgeStacks) stacksSet := map[portainer.EdgeStackID]bool{} for _, edgeStackID := range endpointStacks { stacksSet[edgeStackID] = true diff --git a/api/http/handler/teammemberships/handler.go b/api/http/handler/teammemberships/handler.go index e9faca62d..8628234e8 100644 --- a/api/http/handler/teammemberships/handler.go +++ b/api/http/handler/teammemberships/handler.go @@ -4,6 +4,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" "net/http" @@ -14,7 +15,7 @@ import ( type Handler struct { *mux.Router DataStore portainer.DataStore - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service } // NewHandler creates a handler to manage team membership operations. diff --git a/api/http/handler/teams/handler.go b/api/http/handler/teams/handler.go index ce87e0513..d0a49e537 100644 --- a/api/http/handler/teams/handler.go +++ b/api/http/handler/teams/handler.go @@ -1,6 +1,7 @@ package teams import ( + "github.com/portainer/portainer/api/internal/authorization" "net/http" "github.com/gorilla/mux" @@ -13,7 +14,7 @@ import ( type Handler struct { *mux.Router DataStore portainer.DataStore - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service } // NewHandler creates a handler to manage team operations. diff --git a/api/http/handler/users/admin_init.go b/api/http/handler/users/admin_init.go index 67010b660..b76299199 100644 --- a/api/http/handler/users/admin_init.go +++ b/api/http/handler/users/admin_init.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/authorization" ) type adminInitPayload struct { @@ -45,7 +46,7 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe user := &portainer.User{ Username: payload.Username, Role: portainer.AdministratorRole, - PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), + PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(), } user.Password, err = handler.CryptoService.Hash(payload.Password) diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index cbdc9eaf7..3413d4bb9 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -4,6 +4,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" "net/http" @@ -19,7 +20,7 @@ type Handler struct { *mux.Router DataStore portainer.DataStore CryptoService portainer.CryptoService - AuthorizationService *portainer.AuthorizationService + AuthorizationService *authorization.Service } // NewHandler creates a handler to manage user operations. diff --git a/api/http/handler/users/user_create.go b/api/http/handler/users/user_create.go index 43bcab0e7..d39d1a9a7 100644 --- a/api/http/handler/users/user_create.go +++ b/api/http/handler/users/user_create.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) type userCreatePayload struct { @@ -60,7 +61,7 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http user = &portainer.User{ Username: payload.Username, Role: portainer.UserRole(payload.Role), - PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(), + PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(), } settings, err := handler.DataStore.Settings().Settings() diff --git a/api/http/proxy/factory/docker/access_control.go b/api/http/proxy/factory/docker/access_control.go index d3429f3b6..00b2c9693 100644 --- a/api/http/proxy/factory/docker/access_control.go +++ b/api/http/proxy/factory/docker/access_control.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api" ) @@ -30,7 +31,7 @@ type ( func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]interface{}, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) { if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil { - resourceControl := portainer.NewPublicResourceControl(resourceID, resourceType) + resourceControl := authorization.NewPublicResourceControl(resourceID, resourceType) err := transport.dataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { @@ -76,7 +77,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m userIDs = append(userIDs, user.ID) } - resourceControl := portainer.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs) + resourceControl := authorization.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs) err := transport.dataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { @@ -90,7 +91,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m } func (transport *Transport) createPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) (*portainer.ResourceControl, error) { - resourceControl := portainer.NewPrivateResourceControl(resourceIdentifier, resourceType, userID) + resourceControl := authorization.NewPrivateResourceControl(resourceIdentifier, resourceType, userID) err := transport.dataStore.ResourceControl().CreateResourceControl(resourceControl) if err != nil { @@ -158,7 +159,7 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe return responseutils.RewriteResponse(response, responseObject, http.StatusOK) } - if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || (resourceControl != nil && portainer.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || (resourceControl != nil && authorization.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) { responseObject = decorateObject(responseObject, resourceControl) return responseutils.RewriteResponse(response, responseObject, http.StatusOK) } @@ -246,7 +247,7 @@ func (transport *Transport) filterResourceList(parameters *resourceOperationPara continue } - if context.isAdmin || context.endpointResourceAccess || portainer.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) { + if context.isAdmin || context.endpointResourceAccess || authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) { resourceObject = decorateObject(resourceObject, resourceControl) filteredResourceData = append(filteredResourceData, resourceObject) } @@ -256,7 +257,7 @@ func (transport *Transport) filterResourceList(parameters *resourceOperationPara } func (transport *Transport) findResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, resourceLabelsObject map[string]interface{}, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { - resourceControl := portainer.GetResourceControlByResourceIDAndType(resourceIdentifier, resourceType, resourceControls) + resourceControl := authorization.GetResourceControlByResourceIDAndType(resourceIdentifier, resourceType, resourceControls) if resourceControl != nil { return resourceControl, nil } @@ -264,7 +265,7 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou if resourceLabelsObject != nil { if resourceLabelsObject[resourceLabelForDockerServiceID] != nil { inheritedServiceIdentifier := resourceLabelsObject[resourceLabelForDockerServiceID].(string) - resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedServiceIdentifier, portainer.ServiceResourceControl, resourceControls) + resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedServiceIdentifier, portainer.ServiceResourceControl, resourceControls) if resourceControl != nil { return resourceControl, nil @@ -273,7 +274,7 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil { inheritedSwarmStackIdentifier := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string) - resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls) + resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls) if resourceControl != nil { return resourceControl, nil @@ -282,7 +283,7 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil { inheritedComposeStackIdentifier := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string) - resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls) + resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls) if resourceControl != nil { return resourceControl, nil diff --git a/api/http/proxy/factory/docker/configs.go b/api/http/proxy/factory/docker/configs.go index 540bbdef9..0fee44bbc 100644 --- a/api/http/proxy/factory/docker/configs.go +++ b/api/http/proxy/factory/docker/configs.go @@ -8,6 +8,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/internal/authorization" ) const ( @@ -22,7 +23,7 @@ func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, co swarmStackName := config.Spec.Labels[resourceLabelForDockerSwarmStackName] if swarmStackName != "" { - return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil } return nil, nil diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index c0587d9c8..dfda0b04a 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/client" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/internal/authorization" ) const ( @@ -21,7 +22,7 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, serviceName := container.Config.Labels[resourceLabelForDockerServiceID] if serviceName != "" { - serviceResourceControl := portainer.GetResourceControlByResourceIDAndType(serviceName, portainer.ServiceResourceControl, resourceControls) + serviceResourceControl := authorization.GetResourceControlByResourceIDAndType(serviceName, portainer.ServiceResourceControl, resourceControls) if serviceResourceControl != nil { return serviceResourceControl, nil } @@ -29,12 +30,12 @@ func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName] if swarmStackName != "" { - return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil } composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName] if composeStackName != "" { - return portainer.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil + return authorization.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil } return nil, nil diff --git a/api/http/proxy/factory/docker/networks.go b/api/http/proxy/factory/docker/networks.go index c50b716e2..4c3c06d48 100644 --- a/api/http/proxy/factory/docker/networks.go +++ b/api/http/proxy/factory/docker/networks.go @@ -10,6 +10,7 @@ import ( "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/internal/authorization" ) const ( @@ -25,7 +26,7 @@ func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, n swarmStackName := network.Labels[resourceLabelForDockerSwarmStackName] if swarmStackName != "" { - return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil } return nil, nil @@ -85,7 +86,7 @@ func findSystemNetworkResourceControl(networkObject map[string]interface{}) *por networkName := networkObject[networkObjectName].(string) if networkName == "bridge" || networkName == "host" || networkName == "none" { - return portainer.NewSystemResourceControl(networkID, portainer.NetworkResourceControl) + return authorization.NewSystemResourceControl(networkID, portainer.NetworkResourceControl) } return nil diff --git a/api/http/proxy/factory/docker/secrets.go b/api/http/proxy/factory/docker/secrets.go index 522597bdb..b57627d8d 100644 --- a/api/http/proxy/factory/docker/secrets.go +++ b/api/http/proxy/factory/docker/secrets.go @@ -8,6 +8,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/internal/authorization" ) const ( @@ -22,7 +23,7 @@ func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, se swarmStackName := secret.Spec.Labels[resourceLabelForDockerSwarmStackName] if swarmStackName != "" { - return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil } return nil, nil diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go index eb1aa6e80..8863ea3fd 100644 --- a/api/http/proxy/factory/docker/services.go +++ b/api/http/proxy/factory/docker/services.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/internal/authorization" ) const ( @@ -23,7 +24,7 @@ func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, s swarmStackName := service.Spec.Labels[resourceLabelForDockerSwarmStackName] if swarmStackName != "" { - return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil } return nil, nil diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 2456cc860..4c12fb93b 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -10,12 +10,12 @@ import ( "regexp" "strings" - "github.com/portainer/portainer/api/docker" - "github.com/docker/docker/client" - portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`) @@ -462,7 +462,7 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r return nil, err } - resourceControl := portainer.GetResourceControlByResourceIDAndType(resourceID, resourceType, resourceControls) + resourceControl := authorization.GetResourceControlByResourceIDAndType(resourceID, resourceType, resourceControls) if resourceControl == nil { agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader) @@ -473,12 +473,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r return nil, err } - if inheritedResourceControl == nil || !portainer.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) { + if inheritedResourceControl == nil || !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) { return responseutils.WriteAccessDeniedResponse() } } - if resourceControl != nil && !portainer.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) { + if resourceControl != nil && !authorization.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) { return responseutils.WriteAccessDeniedResponse() } } diff --git a/api/http/proxy/factory/docker/volumes.go b/api/http/proxy/factory/docker/volumes.go index 5727d0157..a4b5002ef 100644 --- a/api/http/proxy/factory/docker/volumes.go +++ b/api/http/proxy/factory/docker/volumes.go @@ -7,9 +7,10 @@ import ( "github.com/docker/docker/client" - portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" ) const ( @@ -24,7 +25,7 @@ func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, vo swarmStackName := volume.Labels[resourceLabelForDockerSwarmStackName] if swarmStackName != "" { - return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + return authorization.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil } return nil, nil diff --git a/api/http/server.go b/api/http/server.go index 1834c4100..11987901d 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -38,6 +38,7 @@ import ( "github.com/portainer/portainer/api/http/handler/websocket" "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" "net/http" "path/filepath" @@ -73,7 +74,7 @@ type Server struct { func (server *Server) Start() error { proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory) - authorizationService := portainer.NewAuthorizationService(server.DataStore) + authorizationService := authorization.NewService(server.DataStore) rbacExtensionURL := proxyManager.GetExtensionURL(portainer.RBACExtension) requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService, rbacExtensionURL) diff --git a/api/access_control.go b/api/internal/authorization/access_control.go similarity index 66% rename from api/access_control.go rename to api/internal/authorization/access_control.go index 7c767ce03..9879799ac 100644 --- a/api/access_control.go +++ b/api/internal/authorization/access_control.go @@ -1,19 +1,21 @@ -package portainer +package authorization + +import "github.com/portainer/portainer/api" // NewPrivateResourceControl will create a new private resource control associated to the resource specified by the // identifier and type parameters. It automatically assigns it to the user specified by the userID parameter. -func NewPrivateResourceControl(resourceIdentifier string, resourceType ResourceControlType, userID UserID) *ResourceControl { - return &ResourceControl{ +func NewPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) *portainer.ResourceControl { + return &portainer.ResourceControl{ Type: resourceType, ResourceID: resourceIdentifier, SubResourceIDs: []string{}, - UserAccesses: []UserResourceAccess{ + UserAccesses: []portainer.UserResourceAccess{ { UserID: userID, - AccessLevel: ReadWriteAccessLevel, + AccessLevel: portainer.ReadWriteAccessLevel, }, }, - TeamAccesses: []TeamResourceAccess{}, + TeamAccesses: []portainer.TeamResourceAccess{}, AdministratorsOnly: false, Public: false, System: false, @@ -22,13 +24,13 @@ func NewPrivateResourceControl(resourceIdentifier string, resourceType ResourceC // NewSystemResourceControl will create a new public resource control with the System flag set to true. // These kind of resource control are not persisted and are created on the fly by the Portainer API. -func NewSystemResourceControl(resourceIdentifier string, resourceType ResourceControlType) *ResourceControl { - return &ResourceControl{ +func NewSystemResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { + return &portainer.ResourceControl{ Type: resourceType, ResourceID: resourceIdentifier, SubResourceIDs: []string{}, - UserAccesses: []UserResourceAccess{}, - TeamAccesses: []TeamResourceAccess{}, + UserAccesses: []portainer.UserResourceAccess{}, + TeamAccesses: []portainer.TeamResourceAccess{}, AdministratorsOnly: false, Public: true, System: true, @@ -36,13 +38,13 @@ func NewSystemResourceControl(resourceIdentifier string, resourceType ResourceCo } // NewPublicResourceControl will create a new public resource control. -func NewPublicResourceControl(resourceIdentifier string, resourceType ResourceControlType) *ResourceControl { - return &ResourceControl{ +func NewPublicResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType) *portainer.ResourceControl { + return &portainer.ResourceControl{ Type: resourceType, ResourceID: resourceIdentifier, SubResourceIDs: []string{}, - UserAccesses: []UserResourceAccess{}, - TeamAccesses: []TeamResourceAccess{}, + UserAccesses: []portainer.UserResourceAccess{}, + TeamAccesses: []portainer.TeamResourceAccess{}, AdministratorsOnly: false, Public: true, System: false, @@ -50,29 +52,29 @@ func NewPublicResourceControl(resourceIdentifier string, resourceType ResourceCo } // NewRestrictedResourceControl will create a new resource control with user and team accesses restrictions. -func NewRestrictedResourceControl(resourceIdentifier string, resourceType ResourceControlType, userIDs []UserID, teamIDs []TeamID) *ResourceControl { - userAccesses := make([]UserResourceAccess, 0) - teamAccesses := make([]TeamResourceAccess, 0) +func NewRestrictedResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userIDs []portainer.UserID, teamIDs []portainer.TeamID) *portainer.ResourceControl { + userAccesses := make([]portainer.UserResourceAccess, 0) + teamAccesses := make([]portainer.TeamResourceAccess, 0) for _, id := range userIDs { - access := UserResourceAccess{ + access := portainer.UserResourceAccess{ UserID: id, - AccessLevel: ReadWriteAccessLevel, + AccessLevel: portainer.ReadWriteAccessLevel, } userAccesses = append(userAccesses, access) } for _, id := range teamIDs { - access := TeamResourceAccess{ + access := portainer.TeamResourceAccess{ TeamID: id, - AccessLevel: ReadWriteAccessLevel, + AccessLevel: portainer.ReadWriteAccessLevel, } teamAccesses = append(teamAccesses, access) } - return &ResourceControl{ + return &portainer.ResourceControl{ Type: resourceType, ResourceID: resourceIdentifier, SubResourceIDs: []string{}, @@ -86,10 +88,10 @@ func NewRestrictedResourceControl(resourceIdentifier string, resourceType Resour // DecorateStacks will iterate through a list of stacks, check for an associated resource control for each // stack and decorate the stack element if a resource control is found. -func DecorateStacks(stacks []Stack, resourceControls []ResourceControl) []Stack { +func DecorateStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl) []portainer.Stack { for idx, stack := range stacks { - resourceControl := GetResourceControlByResourceIDAndType(stack.Name, StackResourceControl, resourceControls) + resourceControl := GetResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl, resourceControls) if resourceControl != nil { stacks[idx].ResourceControl = resourceControl } @@ -99,11 +101,11 @@ func DecorateStacks(stacks []Stack, resourceControls []ResourceControl) []Stack } // FilterAuthorizedStacks returns a list of decorated stacks filtered through resource control access checks. -func FilterAuthorizedStacks(stacks []Stack, user *User, userTeamIDs []TeamID, rbacEnabled bool) []Stack { - authorizedStacks := make([]Stack, 0) +func FilterAuthorizedStacks(stacks []portainer.Stack, user *portainer.User, userTeamIDs []portainer.TeamID, rbacEnabled bool) []portainer.Stack { + authorizedStacks := make([]portainer.Stack, 0) for _, stack := range stacks { - _, ok := user.EndpointAuthorizations[stack.EndpointID][EndpointResourcesAccess] + _, ok := user.EndpointAuthorizations[stack.EndpointID][portainer.EndpointResourcesAccess] if rbacEnabled && ok { authorizedStacks = append(authorizedStacks, stack) continue @@ -119,7 +121,7 @@ func FilterAuthorizedStacks(stacks []Stack, user *User, userTeamIDs []TeamID, rb // UserCanAccessResource will valide that a user has permissions defined in the specified resource control // based on its identifier and the team(s) he is part of. -func UserCanAccessResource(userID UserID, userTeamIDs []TeamID, resourceControl *ResourceControl) bool { +func UserCanAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool { for _, authorizedUserAccess := range resourceControl.UserAccesses { if userID == authorizedUserAccess.UserID { return true @@ -139,7 +141,7 @@ func UserCanAccessResource(userID UserID, userTeamIDs []TeamID, resourceControl // GetResourceControlByResourceIDAndType retrieves the first matching resource control in a set of resource controls // based on the specified id and resource type parameters. -func GetResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType, resourceControls []ResourceControl) *ResourceControl { +func GetResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) *portainer.ResourceControl { for _, resourceControl := range resourceControls { if resourceID == resourceControl.ResourceID && resourceType == resourceControl.Type { return &resourceControl diff --git a/api/internal/authorization/authorizations.go b/api/internal/authorization/authorizations.go new file mode 100644 index 000000000..682f50a56 --- /dev/null +++ b/api/internal/authorization/authorizations.go @@ -0,0 +1,776 @@ +package authorization + +import "github.com/portainer/portainer/api" + +// Service represents a service used to +// update authorizations associated to a user or team. +type Service struct { + dataStore portainer.DataStore +} + +// NewService returns a point to a new Service instance. +func NewService(dataStore portainer.DataStore) *Service { + return &Service{ + dataStore: dataStore, + } +} + +// DefaultEndpointAuthorizationsForEndpointAdministratorRole returns the default endpoint authorizations +// associated to the endpoint administrator role. +func DefaultEndpointAuthorizationsForEndpointAdministratorRole() portainer.Authorizations { + return map[portainer.Authorization]bool{ + portainer.OperationDockerContainerArchiveInfo: true, + portainer.OperationDockerContainerList: true, + portainer.OperationDockerContainerExport: true, + portainer.OperationDockerContainerChanges: true, + portainer.OperationDockerContainerInspect: true, + portainer.OperationDockerContainerTop: true, + portainer.OperationDockerContainerLogs: true, + portainer.OperationDockerContainerStats: true, + portainer.OperationDockerContainerAttachWebsocket: true, + portainer.OperationDockerContainerArchive: true, + portainer.OperationDockerContainerCreate: true, + portainer.OperationDockerContainerPrune: true, + portainer.OperationDockerContainerKill: true, + portainer.OperationDockerContainerPause: true, + portainer.OperationDockerContainerUnpause: true, + portainer.OperationDockerContainerRestart: true, + portainer.OperationDockerContainerStart: true, + portainer.OperationDockerContainerStop: true, + portainer.OperationDockerContainerWait: true, + portainer.OperationDockerContainerResize: true, + portainer.OperationDockerContainerAttach: true, + portainer.OperationDockerContainerExec: true, + portainer.OperationDockerContainerRename: true, + portainer.OperationDockerContainerUpdate: true, + portainer.OperationDockerContainerPutContainerArchive: true, + portainer.OperationDockerContainerDelete: true, + portainer.OperationDockerImageList: true, + portainer.OperationDockerImageSearch: true, + portainer.OperationDockerImageGetAll: true, + portainer.OperationDockerImageGet: true, + portainer.OperationDockerImageHistory: true, + portainer.OperationDockerImageInspect: true, + portainer.OperationDockerImageLoad: true, + portainer.OperationDockerImageCreate: true, + portainer.OperationDockerImagePrune: true, + portainer.OperationDockerImagePush: true, + portainer.OperationDockerImageTag: true, + portainer.OperationDockerImageDelete: true, + portainer.OperationDockerImageCommit: true, + portainer.OperationDockerImageBuild: true, + portainer.OperationDockerNetworkList: true, + portainer.OperationDockerNetworkInspect: true, + portainer.OperationDockerNetworkCreate: true, + portainer.OperationDockerNetworkConnect: true, + portainer.OperationDockerNetworkDisconnect: true, + portainer.OperationDockerNetworkPrune: true, + portainer.OperationDockerNetworkDelete: true, + portainer.OperationDockerVolumeList: true, + portainer.OperationDockerVolumeInspect: true, + portainer.OperationDockerVolumeCreate: true, + portainer.OperationDockerVolumePrune: true, + portainer.OperationDockerVolumeDelete: true, + portainer.OperationDockerExecInspect: true, + portainer.OperationDockerExecStart: true, + portainer.OperationDockerExecResize: true, + portainer.OperationDockerSwarmInspect: true, + portainer.OperationDockerSwarmUnlockKey: true, + portainer.OperationDockerSwarmInit: true, + portainer.OperationDockerSwarmJoin: true, + portainer.OperationDockerSwarmLeave: true, + portainer.OperationDockerSwarmUpdate: true, + portainer.OperationDockerSwarmUnlock: true, + portainer.OperationDockerNodeList: true, + portainer.OperationDockerNodeInspect: true, + portainer.OperationDockerNodeUpdate: true, + portainer.OperationDockerNodeDelete: true, + portainer.OperationDockerServiceList: true, + portainer.OperationDockerServiceInspect: true, + portainer.OperationDockerServiceLogs: true, + portainer.OperationDockerServiceCreate: true, + portainer.OperationDockerServiceUpdate: true, + portainer.OperationDockerServiceDelete: true, + portainer.OperationDockerSecretList: true, + portainer.OperationDockerSecretInspect: true, + portainer.OperationDockerSecretCreate: true, + portainer.OperationDockerSecretUpdate: true, + portainer.OperationDockerSecretDelete: true, + portainer.OperationDockerConfigList: true, + portainer.OperationDockerConfigInspect: true, + portainer.OperationDockerConfigCreate: true, + portainer.OperationDockerConfigUpdate: true, + portainer.OperationDockerConfigDelete: true, + portainer.OperationDockerTaskList: true, + portainer.OperationDockerTaskInspect: true, + portainer.OperationDockerTaskLogs: true, + portainer.OperationDockerPluginList: true, + portainer.OperationDockerPluginPrivileges: true, + portainer.OperationDockerPluginInspect: true, + portainer.OperationDockerPluginPull: true, + portainer.OperationDockerPluginCreate: true, + portainer.OperationDockerPluginEnable: true, + portainer.OperationDockerPluginDisable: true, + portainer.OperationDockerPluginPush: true, + portainer.OperationDockerPluginUpgrade: true, + portainer.OperationDockerPluginSet: true, + portainer.OperationDockerPluginDelete: true, + portainer.OperationDockerSessionStart: true, + portainer.OperationDockerDistributionInspect: true, + portainer.OperationDockerBuildPrune: true, + portainer.OperationDockerBuildCancel: true, + portainer.OperationDockerPing: true, + portainer.OperationDockerInfo: true, + portainer.OperationDockerVersion: true, + portainer.OperationDockerEvents: true, + portainer.OperationDockerSystem: true, + portainer.OperationDockerUndefined: true, + portainer.OperationDockerAgentPing: true, + portainer.OperationDockerAgentList: true, + portainer.OperationDockerAgentHostInfo: true, + portainer.OperationDockerAgentBrowseDelete: true, + portainer.OperationDockerAgentBrowseGet: true, + portainer.OperationDockerAgentBrowseList: true, + portainer.OperationDockerAgentBrowsePut: true, + portainer.OperationDockerAgentBrowseRename: true, + portainer.OperationDockerAgentUndefined: true, + portainer.OperationPortainerResourceControlCreate: true, + portainer.OperationPortainerResourceControlUpdate: true, + portainer.OperationPortainerStackList: true, + portainer.OperationPortainerStackInspect: true, + portainer.OperationPortainerStackFile: true, + portainer.OperationPortainerStackCreate: true, + portainer.OperationPortainerStackMigrate: true, + portainer.OperationPortainerStackUpdate: true, + portainer.OperationPortainerStackDelete: true, + portainer.OperationPortainerWebsocketExec: true, + portainer.OperationPortainerWebhookList: true, + portainer.OperationPortainerWebhookCreate: true, + portainer.OperationPortainerWebhookDelete: true, + portainer.OperationIntegrationStoridgeAdmin: true, + portainer.EndpointResourcesAccess: true, + } +} + +// DefaultEndpointAuthorizationsForHelpDeskRole returns the default endpoint authorizations +// associated to the helpdesk role. +func DefaultEndpointAuthorizationsForHelpDeskRole(volumeBrowsingAuthorizations bool) portainer.Authorizations { + authorizations := map[portainer.Authorization]bool{ + portainer.OperationDockerContainerArchiveInfo: true, + portainer.OperationDockerContainerList: true, + portainer.OperationDockerContainerChanges: true, + portainer.OperationDockerContainerInspect: true, + portainer.OperationDockerContainerTop: true, + portainer.OperationDockerContainerLogs: true, + portainer.OperationDockerContainerStats: true, + portainer.OperationDockerImageList: true, + portainer.OperationDockerImageSearch: true, + portainer.OperationDockerImageGetAll: true, + portainer.OperationDockerImageGet: true, + portainer.OperationDockerImageHistory: true, + portainer.OperationDockerImageInspect: true, + portainer.OperationDockerNetworkList: true, + portainer.OperationDockerNetworkInspect: true, + portainer.OperationDockerVolumeList: true, + portainer.OperationDockerVolumeInspect: true, + portainer.OperationDockerSwarmInspect: true, + portainer.OperationDockerNodeList: true, + portainer.OperationDockerNodeInspect: true, + portainer.OperationDockerServiceList: true, + portainer.OperationDockerServiceInspect: true, + portainer.OperationDockerServiceLogs: true, + portainer.OperationDockerSecretList: true, + portainer.OperationDockerSecretInspect: true, + portainer.OperationDockerConfigList: true, + portainer.OperationDockerConfigInspect: true, + portainer.OperationDockerTaskList: true, + portainer.OperationDockerTaskInspect: true, + portainer.OperationDockerTaskLogs: true, + portainer.OperationDockerPluginList: true, + portainer.OperationDockerDistributionInspect: true, + portainer.OperationDockerPing: true, + portainer.OperationDockerInfo: true, + portainer.OperationDockerVersion: true, + portainer.OperationDockerEvents: true, + portainer.OperationDockerSystem: true, + portainer.OperationDockerAgentPing: true, + portainer.OperationDockerAgentList: true, + portainer.OperationDockerAgentHostInfo: true, + portainer.OperationPortainerStackList: true, + portainer.OperationPortainerStackInspect: true, + portainer.OperationPortainerStackFile: true, + portainer.OperationPortainerWebhookList: true, + portainer.EndpointResourcesAccess: true, + } + + if volumeBrowsingAuthorizations { + authorizations[portainer.OperationDockerAgentBrowseGet] = true + authorizations[portainer.OperationDockerAgentBrowseList] = true + } + + return authorizations +} + +// DefaultEndpointAuthorizationsForStandardUserRole returns the default endpoint authorizations +// associated to the standard user role. +func DefaultEndpointAuthorizationsForStandardUserRole(volumeBrowsingAuthorizations bool) portainer.Authorizations { + authorizations := map[portainer.Authorization]bool{ + portainer.OperationDockerContainerArchiveInfo: true, + portainer.OperationDockerContainerList: true, + portainer.OperationDockerContainerExport: true, + portainer.OperationDockerContainerChanges: true, + portainer.OperationDockerContainerInspect: true, + portainer.OperationDockerContainerTop: true, + portainer.OperationDockerContainerLogs: true, + portainer.OperationDockerContainerStats: true, + portainer.OperationDockerContainerAttachWebsocket: true, + portainer.OperationDockerContainerArchive: true, + portainer.OperationDockerContainerCreate: true, + portainer.OperationDockerContainerKill: true, + portainer.OperationDockerContainerPause: true, + portainer.OperationDockerContainerUnpause: true, + portainer.OperationDockerContainerRestart: true, + portainer.OperationDockerContainerStart: true, + portainer.OperationDockerContainerStop: true, + portainer.OperationDockerContainerWait: true, + portainer.OperationDockerContainerResize: true, + portainer.OperationDockerContainerAttach: true, + portainer.OperationDockerContainerExec: true, + portainer.OperationDockerContainerRename: true, + portainer.OperationDockerContainerUpdate: true, + portainer.OperationDockerContainerPutContainerArchive: true, + portainer.OperationDockerContainerDelete: true, + portainer.OperationDockerImageList: true, + portainer.OperationDockerImageSearch: true, + portainer.OperationDockerImageGetAll: true, + portainer.OperationDockerImageGet: true, + portainer.OperationDockerImageHistory: true, + portainer.OperationDockerImageInspect: true, + portainer.OperationDockerImageLoad: true, + portainer.OperationDockerImageCreate: true, + portainer.OperationDockerImagePush: true, + portainer.OperationDockerImageTag: true, + portainer.OperationDockerImageDelete: true, + portainer.OperationDockerImageCommit: true, + portainer.OperationDockerImageBuild: true, + portainer.OperationDockerNetworkList: true, + portainer.OperationDockerNetworkInspect: true, + portainer.OperationDockerNetworkCreate: true, + portainer.OperationDockerNetworkConnect: true, + portainer.OperationDockerNetworkDisconnect: true, + portainer.OperationDockerNetworkDelete: true, + portainer.OperationDockerVolumeList: true, + portainer.OperationDockerVolumeInspect: true, + portainer.OperationDockerVolumeCreate: true, + portainer.OperationDockerVolumeDelete: true, + portainer.OperationDockerExecInspect: true, + portainer.OperationDockerExecStart: true, + portainer.OperationDockerExecResize: true, + portainer.OperationDockerSwarmInspect: true, + portainer.OperationDockerSwarmUnlockKey: true, + portainer.OperationDockerSwarmInit: true, + portainer.OperationDockerSwarmJoin: true, + portainer.OperationDockerSwarmLeave: true, + portainer.OperationDockerSwarmUpdate: true, + portainer.OperationDockerSwarmUnlock: true, + portainer.OperationDockerNodeList: true, + portainer.OperationDockerNodeInspect: true, + portainer.OperationDockerNodeUpdate: true, + portainer.OperationDockerNodeDelete: true, + portainer.OperationDockerServiceList: true, + portainer.OperationDockerServiceInspect: true, + portainer.OperationDockerServiceLogs: true, + portainer.OperationDockerServiceCreate: true, + portainer.OperationDockerServiceUpdate: true, + portainer.OperationDockerServiceDelete: true, + portainer.OperationDockerSecretList: true, + portainer.OperationDockerSecretInspect: true, + portainer.OperationDockerSecretCreate: true, + portainer.OperationDockerSecretUpdate: true, + portainer.OperationDockerSecretDelete: true, + portainer.OperationDockerConfigList: true, + portainer.OperationDockerConfigInspect: true, + portainer.OperationDockerConfigCreate: true, + portainer.OperationDockerConfigUpdate: true, + portainer.OperationDockerConfigDelete: true, + portainer.OperationDockerTaskList: true, + portainer.OperationDockerTaskInspect: true, + portainer.OperationDockerTaskLogs: true, + portainer.OperationDockerPluginList: true, + portainer.OperationDockerPluginPrivileges: true, + portainer.OperationDockerPluginInspect: true, + portainer.OperationDockerPluginPull: true, + portainer.OperationDockerPluginCreate: true, + portainer.OperationDockerPluginEnable: true, + portainer.OperationDockerPluginDisable: true, + portainer.OperationDockerPluginPush: true, + portainer.OperationDockerPluginUpgrade: true, + portainer.OperationDockerPluginSet: true, + portainer.OperationDockerPluginDelete: true, + portainer.OperationDockerSessionStart: true, + portainer.OperationDockerDistributionInspect: true, + portainer.OperationDockerBuildPrune: true, + portainer.OperationDockerBuildCancel: true, + portainer.OperationDockerPing: true, + portainer.OperationDockerInfo: true, + portainer.OperationDockerVersion: true, + portainer.OperationDockerEvents: true, + portainer.OperationDockerSystem: true, + portainer.OperationDockerUndefined: true, + portainer.OperationDockerAgentPing: true, + portainer.OperationDockerAgentList: true, + portainer.OperationDockerAgentHostInfo: true, + portainer.OperationDockerAgentUndefined: true, + portainer.OperationPortainerResourceControlUpdate: true, + portainer.OperationPortainerStackList: true, + portainer.OperationPortainerStackInspect: true, + portainer.OperationPortainerStackFile: true, + portainer.OperationPortainerStackCreate: true, + portainer.OperationPortainerStackMigrate: true, + portainer.OperationPortainerStackUpdate: true, + portainer.OperationPortainerStackDelete: true, + portainer.OperationPortainerWebsocketExec: true, + portainer.OperationPortainerWebhookList: true, + portainer.OperationPortainerWebhookCreate: true, + } + + if volumeBrowsingAuthorizations { + authorizations[portainer.OperationDockerAgentBrowseGet] = true + authorizations[portainer.OperationDockerAgentBrowseList] = true + authorizations[portainer.OperationDockerAgentBrowseDelete] = true + authorizations[portainer.OperationDockerAgentBrowsePut] = true + authorizations[portainer.OperationDockerAgentBrowseRename] = true + } + + return authorizations +} + +// DefaultEndpointAuthorizationsForReadOnlyUserRole returns the default endpoint authorizations +// associated to the readonly user role. +func DefaultEndpointAuthorizationsForReadOnlyUserRole(volumeBrowsingAuthorizations bool) portainer.Authorizations { + authorizations := map[portainer.Authorization]bool{ + portainer.OperationDockerContainerArchiveInfo: true, + portainer.OperationDockerContainerList: true, + portainer.OperationDockerContainerChanges: true, + portainer.OperationDockerContainerInspect: true, + portainer.OperationDockerContainerTop: true, + portainer.OperationDockerContainerLogs: true, + portainer.OperationDockerContainerStats: true, + portainer.OperationDockerImageList: true, + portainer.OperationDockerImageSearch: true, + portainer.OperationDockerImageGetAll: true, + portainer.OperationDockerImageGet: true, + portainer.OperationDockerImageHistory: true, + portainer.OperationDockerImageInspect: true, + portainer.OperationDockerNetworkList: true, + portainer.OperationDockerNetworkInspect: true, + portainer.OperationDockerVolumeList: true, + portainer.OperationDockerVolumeInspect: true, + portainer.OperationDockerSwarmInspect: true, + portainer.OperationDockerNodeList: true, + portainer.OperationDockerNodeInspect: true, + portainer.OperationDockerServiceList: true, + portainer.OperationDockerServiceInspect: true, + portainer.OperationDockerServiceLogs: true, + portainer.OperationDockerSecretList: true, + portainer.OperationDockerSecretInspect: true, + portainer.OperationDockerConfigList: true, + portainer.OperationDockerConfigInspect: true, + portainer.OperationDockerTaskList: true, + portainer.OperationDockerTaskInspect: true, + portainer.OperationDockerTaskLogs: true, + portainer.OperationDockerPluginList: true, + portainer.OperationDockerDistributionInspect: true, + portainer.OperationDockerPing: true, + portainer.OperationDockerInfo: true, + portainer.OperationDockerVersion: true, + portainer.OperationDockerEvents: true, + portainer.OperationDockerSystem: true, + portainer.OperationDockerAgentPing: true, + portainer.OperationDockerAgentList: true, + portainer.OperationDockerAgentHostInfo: true, + portainer.OperationPortainerStackList: true, + portainer.OperationPortainerStackInspect: true, + portainer.OperationPortainerStackFile: true, + portainer.OperationPortainerWebhookList: true, + } + + if volumeBrowsingAuthorizations { + authorizations[portainer.OperationDockerAgentBrowseGet] = true + authorizations[portainer.OperationDockerAgentBrowseList] = true + } + + return authorizations +} + +// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users. +func DefaultPortainerAuthorizations() portainer.Authorizations { + return map[portainer.Authorization]bool{ + portainer.OperationPortainerDockerHubInspect: true, + portainer.OperationPortainerEndpointGroupList: true, + portainer.OperationPortainerEndpointList: true, + portainer.OperationPortainerEndpointInspect: true, + portainer.OperationPortainerEndpointExtensionAdd: true, + portainer.OperationPortainerEndpointExtensionRemove: true, + portainer.OperationPortainerExtensionList: true, + portainer.OperationPortainerMOTD: true, + portainer.OperationPortainerRegistryList: true, + portainer.OperationPortainerRegistryInspect: true, + portainer.OperationPortainerTeamList: true, + portainer.OperationPortainerTemplateList: true, + portainer.OperationPortainerTemplateInspect: true, + portainer.OperationPortainerUserList: true, + portainer.OperationPortainerUserInspect: true, + portainer.OperationPortainerUserMemberships: true, + } +} + +// UpdateVolumeBrowsingAuthorizations will update all the volume browsing authorizations for each role (except endpoint administrator) +// based on the specified removeAuthorizations parameter. If removeAuthorizations is set to true, all +// the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations +// will be reset based for each role. +func (service Service) UpdateVolumeBrowsingAuthorizations(remove bool) error { + roles, err := service.dataStore.Role().Roles() + if err != nil { + return err + } + + for _, role := range roles { + // all roles except endpoint administrator + if role.ID != portainer.RoleID(1) { + updateRoleVolumeBrowsingAuthorizations(&role, remove) + + err := service.dataStore.Role().UpdateRole(role.ID, &role) + if err != nil { + return err + } + } + } + + return nil +} + +func updateRoleVolumeBrowsingAuthorizations(role *portainer.Role, removeAuthorizations bool) { + if !removeAuthorizations { + delete(role.Authorizations, portainer.OperationDockerAgentBrowseDelete) + delete(role.Authorizations, portainer.OperationDockerAgentBrowseGet) + delete(role.Authorizations, portainer.OperationDockerAgentBrowseList) + delete(role.Authorizations, portainer.OperationDockerAgentBrowsePut) + delete(role.Authorizations, portainer.OperationDockerAgentBrowseRename) + return + } + + role.Authorizations[portainer.OperationDockerAgentBrowseGet] = true + role.Authorizations[portainer.OperationDockerAgentBrowseList] = true + + // Standard-user + if role.ID == portainer.RoleID(3) { + role.Authorizations[portainer.OperationDockerAgentBrowseDelete] = true + role.Authorizations[portainer.OperationDockerAgentBrowsePut] = true + role.Authorizations[portainer.OperationDockerAgentBrowseRename] = true + } +} + +// RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team +func (service *Service) RemoveTeamAccessPolicies(teamID portainer.TeamID) error { + endpoints, err := service.dataStore.Endpoint().Endpoints() + if err != nil { + return err + } + + for _, endpoint := range endpoints { + for policyTeamID := range endpoint.TeamAccessPolicies { + if policyTeamID == teamID { + delete(endpoint.TeamAccessPolicies, policyTeamID) + + err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + + break + } + } + } + + endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() + if err != nil { + return err + } + + for _, endpointGroup := range endpointGroups { + for policyTeamID := range endpointGroup.TeamAccessPolicies { + if policyTeamID == teamID { + delete(endpointGroup.TeamAccessPolicies, policyTeamID) + + err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + if err != nil { + return err + } + + break + } + } + } + + registries, err := service.dataStore.Registry().Registries() + if err != nil { + return err + } + + for _, registry := range registries { + for policyTeamID := range registry.TeamAccessPolicies { + if policyTeamID == teamID { + delete(registry.TeamAccessPolicies, policyTeamID) + + err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry) + if err != nil { + return err + } + + break + } + } + } + + return service.UpdateUsersAuthorizations() +} + +// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user +func (service *Service) RemoveUserAccessPolicies(userID portainer.UserID) error { + endpoints, err := service.dataStore.Endpoint().Endpoints() + if err != nil { + return err + } + + for _, endpoint := range endpoints { + for policyUserID := range endpoint.UserAccessPolicies { + if policyUserID == userID { + delete(endpoint.UserAccessPolicies, policyUserID) + + err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + + break + } + } + } + + endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() + if err != nil { + return err + } + + for _, endpointGroup := range endpointGroups { + for policyUserID := range endpointGroup.UserAccessPolicies { + if policyUserID == userID { + delete(endpointGroup.UserAccessPolicies, policyUserID) + + err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + if err != nil { + return err + } + + break + } + } + } + + registries, err := service.dataStore.Registry().Registries() + if err != nil { + return err + } + + for _, registry := range registries { + for policyUserID := range registry.UserAccessPolicies { + if policyUserID == userID { + delete(registry.UserAccessPolicies, policyUserID) + + err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry) + if err != nil { + return err + } + + break + } + } + } + + return nil +} + +// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users. +func (service *Service) UpdateUsersAuthorizations() error { + users, err := service.dataStore.User().Users() + if err != nil { + return err + } + + for _, user := range users { + err := service.updateUserAuthorizations(user.ID) + if err != nil { + return err + } + } + + return nil +} + +func (service *Service) updateUserAuthorizations(userID portainer.UserID) error { + user, err := service.dataStore.User().User(userID) + if err != nil { + return err + } + + endpointAuthorizations, err := service.getAuthorizations(user) + if err != nil { + return err + } + + user.EndpointAuthorizations = endpointAuthorizations + + return service.dataStore.User().UpdateUser(userID, user) +} + +func (service *Service) getAuthorizations(user *portainer.User) (portainer.EndpointAuthorizations, error) { + endpointAuthorizations := portainer.EndpointAuthorizations{} + if user.Role == portainer.AdministratorRole { + return endpointAuthorizations, nil + } + + userMemberships, err := service.dataStore.TeamMembership().TeamMembershipsByUserID(user.ID) + if err != nil { + return endpointAuthorizations, err + } + + endpoints, err := service.dataStore.Endpoint().Endpoints() + if err != nil { + return endpointAuthorizations, err + } + + endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups() + if err != nil { + return endpointAuthorizations, err + } + + roles, err := service.dataStore.Role().Roles() + if err != nil { + return endpointAuthorizations, err + } + + endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships) + + return endpointAuthorizations, nil +} + +func getUserEndpointAuthorizations(user *portainer.User, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, roles []portainer.Role, userMemberships []portainer.TeamMembership) portainer.EndpointAuthorizations { + endpointAuthorizations := make(portainer.EndpointAuthorizations) + + groupUserAccessPolicies := map[portainer.EndpointGroupID]portainer.UserAccessPolicies{} + groupTeamAccessPolicies := map[portainer.EndpointGroupID]portainer.TeamAccessPolicies{} + for _, endpointGroup := range endpointGroups { + groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies + groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies + } + + for _, endpoint := range endpoints { + authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles) + if len(authorizations) > 0 { + endpointAuthorizations[endpoint.ID] = authorizations + continue + } + + authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies) + if len(authorizations) > 0 { + endpointAuthorizations[endpoint.ID] = authorizations + continue + } + + authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles) + if len(authorizations) > 0 { + endpointAuthorizations[endpoint.ID] = authorizations + continue + } + + authorizations = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies) + if len(authorizations) > 0 { + endpointAuthorizations[endpoint.ID] = authorizations + } + } + + return endpointAuthorizations +} + +func getAuthorizationsFromUserEndpointPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations { + policyRoles := make([]portainer.RoleID, 0) + + policy, ok := endpoint.UserAccessPolicies[user.ID] + if ok { + policyRoles = append(policyRoles, policy.RoleID) + } + + return getAuthorizationsFromRoles(policyRoles, roles) +} + +func getAuthorizationsFromUserEndpointGroupPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.UserAccessPolicies) portainer.Authorizations { + policyRoles := make([]portainer.RoleID, 0) + + policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID] + if ok { + policyRoles = append(policyRoles, policy.RoleID) + } + + return getAuthorizationsFromRoles(policyRoles, roles) +} + +func getAuthorizationsFromTeamEndpointPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations { + policyRoles := make([]portainer.RoleID, 0) + + for _, membership := range memberships { + policy, ok := endpoint.TeamAccessPolicies[membership.TeamID] + if ok { + policyRoles = append(policyRoles, policy.RoleID) + } + } + + return getAuthorizationsFromRoles(policyRoles, roles) +} + +func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.TeamAccessPolicies) portainer.Authorizations { + policyRoles := make([]portainer.RoleID, 0) + + for _, membership := range memberships { + policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID] + if ok { + policyRoles = append(policyRoles, policy.RoleID) + } + } + + return getAuthorizationsFromRoles(policyRoles, roles) +} + +func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []portainer.Role) portainer.Authorizations { + var associatedRoles []portainer.Role + + for _, id := range roleIdentifiers { + for _, role := range roles { + if role.ID == id { + associatedRoles = append(associatedRoles, role) + break + } + } + } + + var authorizations portainer.Authorizations + highestPriority := 0 + for _, role := range associatedRoles { + if role.Priority > highestPriority { + highestPriority = role.Priority + authorizations = role.Authorizations + } + } + + return authorizations +} diff --git a/api/internal/edge/edgegroup.go b/api/internal/edge/edgegroup.go new file mode 100644 index 000000000..968d5cff2 --- /dev/null +++ b/api/internal/edge/edgegroup.go @@ -0,0 +1,59 @@ +package edge + +import ( + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/tag" +) + +// EdgeGroupRelatedEndpoints returns a list of endpoints related to this Edge group +func EdgeGroupRelatedEndpoints(edgeGroup *portainer.EdgeGroup, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup) []portainer.EndpointID { + if !edgeGroup.Dynamic { + return edgeGroup.Endpoints + } + + endpointIDs := []portainer.EndpointID{} + for _, endpoint := range endpoints { + if endpoint.Type != portainer.EdgeAgentEnvironment { + continue + } + + var endpointGroup portainer.EndpointGroup + for _, group := range endpointGroups { + if endpoint.GroupID == group.ID { + endpointGroup = group + break + } + } + + if edgeGroupRelatedToEndpoint(edgeGroup, &endpoint, &endpointGroup) { + endpointIDs = append(endpointIDs, endpoint.ID) + } + } + + return endpointIDs +} + +// edgeGroupRelatedToEndpoint returns true is edgeGroup is associated with endpoint +func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) bool { + if !edgeGroup.Dynamic { + for _, endpointID := range edgeGroup.Endpoints { + if endpoint.ID == endpointID { + return true + } + } + return false + } + + endpointTags := tag.Set(endpoint.TagIDs) + if endpointGroup.TagIDs != nil { + endpointTags = tag.Union(endpointTags, tag.Set(endpointGroup.TagIDs)) + } + edgeGroupTags := tag.Set(edgeGroup.TagIDs) + + if edgeGroup.PartialMatch { + intersection := tag.Intersection(endpointTags, edgeGroupTags) + return len(intersection) != 0 + } + + return tag.Contains(edgeGroupTags, endpointTags) +} diff --git a/api/edgestack.go b/api/internal/edge/edgestack.go similarity index 56% rename from api/edgestack.go rename to api/internal/edge/edgestack.go index 7a3019c5d..6f4094e9d 100644 --- a/api/edgestack.go +++ b/api/internal/edge/edgestack.go @@ -1,13 +1,16 @@ -package portainer +package edge -import "errors" +import ( + "errors" + "github.com/portainer/portainer/api" +) // EdgeStackRelatedEndpoints returns a list of endpoints related to this Edge stack -func EdgeStackRelatedEndpoints(edgeGroupIDs []EdgeGroupID, endpoints []Endpoint, endpointGroups []EndpointGroup, edgeGroups []EdgeGroup) ([]EndpointID, error) { - edgeStackEndpoints := []EndpointID{} +func EdgeStackRelatedEndpoints(edgeGroupIDs []portainer.EdgeGroupID, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, edgeGroups []portainer.EdgeGroup) ([]portainer.EndpointID, error) { + edgeStackEndpoints := []portainer.EndpointID{} for _, edgeGroupID := range edgeGroupIDs { - var edgeGroup *EdgeGroup + var edgeGroup *portainer.EdgeGroup for _, group := range edgeGroups { if group.ID == edgeGroupID { diff --git a/api/endpoint.go b/api/internal/edge/endpoint.go similarity index 58% rename from api/endpoint.go rename to api/internal/edge/endpoint.go index 661818da3..99d12bf60 100644 --- a/api/endpoint.go +++ b/api/internal/edge/endpoint.go @@ -1,8 +1,10 @@ -package portainer +package edge + +import "github.com/portainer/portainer/api" // EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Endpoint -func EndpointRelatedEdgeStacks(endpoint *Endpoint, endpointGroup *EndpointGroup, edgeGroups []EdgeGroup, edgeStacks []EdgeStack) []EdgeStackID { - relatedEdgeGroupsSet := map[EdgeGroupID]bool{} +func EndpointRelatedEdgeStacks(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) []portainer.EdgeStackID { + relatedEdgeGroupsSet := map[portainer.EdgeGroupID]bool{} for _, edgeGroup := range edgeGroups { if edgeGroupRelatedToEndpoint(&edgeGroup, endpoint, endpointGroup) { @@ -10,7 +12,7 @@ func EndpointRelatedEdgeStacks(endpoint *Endpoint, endpointGroup *EndpointGroup, } } - relatedEdgeStacks := []EdgeStackID{} + relatedEdgeStacks := []portainer.EdgeStackID{} for _, edgeStack := range edgeStacks { for _, edgeGroupID := range edgeStack.EdgeGroups { if relatedEdgeGroupsSet[edgeGroupID] { diff --git a/api/tag.go b/api/internal/tag/tag.go similarity index 54% rename from api/tag.go rename to api/internal/tag/tag.go index f93c6b547..8dd5ad58b 100644 --- a/api/tag.go +++ b/api/internal/tag/tag.go @@ -1,18 +1,20 @@ -package portainer +package tag -type tagSet map[TagID]bool +import "github.com/portainer/portainer/api" -// TagSet converts an array of ids to a set -func TagSet(tagIDs []TagID) tagSet { - set := map[TagID]bool{} +type tagSet map[portainer.TagID]bool + +// Set converts an array of ids to a set +func Set(tagIDs []portainer.TagID) tagSet { + set := map[portainer.TagID]bool{} for _, tagID := range tagIDs { set[tagID] = true } return set } -// TagIntersection returns a set intersection of the provided sets -func TagIntersection(sets ...tagSet) tagSet { +// Intersection returns a set intersection of the provided sets +func Intersection(sets ...tagSet) tagSet { intersection := tagSet{} if len(sets) == 0 { return intersection @@ -35,8 +37,8 @@ func TagIntersection(sets ...tagSet) tagSet { return intersection } -// TagUnion returns a set union of provided sets -func TagUnion(sets ...tagSet) tagSet { +// Union returns a set union of provided sets +func Union(sets ...tagSet) tagSet { union := tagSet{} for _, set := range sets { for tag := range set { @@ -46,8 +48,8 @@ func TagUnion(sets ...tagSet) tagSet { return union } -// TagContains return true if setA contains setB -func TagContains(setA tagSet, setB tagSet) bool { +// Contains return true if setA contains setB +func Contains(setA tagSet, setB tagSet) bool { containedTags := 0 for tag := range setB { if setA[tag] { @@ -57,8 +59,8 @@ func TagContains(setA tagSet, setB tagSet) bool { return containedTags == len(setA) } -// TagDifference returns the set difference tagsA - tagsB -func TagDifference(setA tagSet, setB tagSet) tagSet { +// Difference returns the set difference tagsA - tagsB +func Difference(setA tagSet, setB tagSet) tagSet { set := tagSet{} for tag := range setA { From 0b6dbec305acecd988266fdecb218aa7b9f677cd Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 17 Jun 2020 02:41:39 +0300 Subject: [PATCH 035/195] refactor(auth): remove login retry with $sanitize (#3923) * refactor(auth): remove update-password view * refactor(auth): remove auth retry with $sanitize --- app/index.html | 4 +- app/portainer/__module.js | 13 --- app/portainer/views/auth/authController.js | 20 +---- .../views/update-password/updatePassword.html | 83 ------------------- .../updatePasswordController.js | 44 ---------- 5 files changed, 3 insertions(+), 161 deletions(-) delete mode 100644 app/portainer/views/update-password/updatePassword.html delete mode 100644 app/portainer/views/update-password/updatePasswordController.js diff --git a/app/index.html b/app/index.html index 3765fb0bb..ef7137d15 100644 --- a/app/index.html +++ b/app/index.html @@ -25,8 +25,8 @@
diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 7a7df77e9..04dfa7235 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -452,18 +452,6 @@ angular.module('portainer.app', []).config([ }, }; - var updatePassword = { - name: 'portainer.updatePassword', - url: '/update-password', - views: { - 'content@': { - templateUrl: './views/update-password/updatePassword.html', - controller: 'UpdatePasswordController', - }, - 'sidebar@': {}, - }, - }; - var users = { name: 'portainer.users', url: '/users', @@ -565,7 +553,6 @@ angular.module('portainer.app', []).config([ $stateRegistryProvider.register(support); $stateRegistryProvider.register(supportProduct); $stateRegistryProvider.register(tags); - $stateRegistryProvider.register(updatePassword); $stateRegistryProvider.register(users); $stateRegistryProvider.register(user); $stateRegistryProvider.register(teams); diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index a0eb03acc..a2acfc0be 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -8,7 +8,6 @@ class AuthenticationController { $scope, $state, $stateParams, - $sanitize, $window, Authentication, UserService, @@ -26,7 +25,6 @@ class AuthenticationController { this.$state = $state; this.$stateParams = $stateParams; this.$window = $window; - this.$sanitize = $sanitize; this.Authentication = Authentication; this.UserService = UserService; this.EndpointService = EndpointService; @@ -55,7 +53,6 @@ class AuthenticationController { this.postLoginSteps = this.postLoginSteps.bind(this); this.oAuthLoginAsync = this.oAuthLoginAsync.bind(this); - this.retryLoginSanitizeAsync = this.retryLoginSanitizeAsync.bind(this); this.internalLoginAsync = this.internalLoginAsync.bind(this); this.authenticateUserAsync = this.authenticateUserAsync.bind(this); @@ -182,15 +179,6 @@ class AuthenticationController { } } - async retryLoginSanitizeAsync(username, password) { - try { - await this.internalLoginAsync(this.$sanitize(username), this.$sanitize(password)); - this.$state.go('portainer.updatePassword'); - } catch (err) { - this.error(err, 'Invalid credentials'); - } - } - async internalLoginAsync(username, password) { await this.Authentication.login(username, password); await this.postLoginSteps(); @@ -211,13 +199,7 @@ class AuthenticationController { this.state.loginInProgress = true; await this.internalLoginAsync(username, password); } catch (err) { - if (this.state.permissionsError) { - return; - } - // This login retry is necessary to avoid conflicts with databases - // containing users created before Portainer 1.19.2 - // See https://github.com/portainer/portainer/issues/2199 for more info - await this.retryLoginSanitizeAsync(username, password); + this.error(err, 'Unable to login'); } } diff --git a/app/portainer/views/update-password/updatePassword.html b/app/portainer/views/update-password/updatePassword.html deleted file mode 100644 index 1070ccb99..000000000 --- a/app/portainer/views/update-password/updatePassword.html +++ /dev/null @@ -1,83 +0,0 @@ -
- -
-
- -
-
-
- -
-
- - Your password must be updated. - -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
-
- - -
-
-
- - -
-
- - - The password must be at least 8 characters long - -
-
- - -
-
- -
-
- -
-
-
- -
-
- -
diff --git a/app/portainer/views/update-password/updatePasswordController.js b/app/portainer/views/update-password/updatePasswordController.js deleted file mode 100644 index ebf229aa6..000000000 --- a/app/portainer/views/update-password/updatePasswordController.js +++ /dev/null @@ -1,44 +0,0 @@ -angular.module('portainer.app').controller('UpdatePasswordController', [ - '$scope', - '$state', - '$transition$', - '$sanitize', - 'UserService', - 'Authentication', - 'Notifications', - function UpdatePasswordController($scope, $state, $transition$, $sanitize, UserService, Authentication, Notifications) { - $scope.formValues = { - CurrentPassword: '', - Password: '', - ConfirmPassword: '', - }; - - $scope.state = { - actionInProgress: false, - }; - - $scope.updatePassword = function () { - var userId = Authentication.getUserDetails().ID; - - $scope.state.actionInProgress = true; - UserService.updateUserPassword(userId, $sanitize($scope.formValues.CurrentPassword), $scope.formValues.Password) - .then(function success() { - $state.go('portainer.home'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update password'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; - - function initView() { - if (!Authentication.isAuthenticated()) { - $state.go('portainer.auth'); - } - } - - initView(); - }, -]); From 7bd3d6e44af826b9944f404b09333d4e091b2ede Mon Sep 17 00:00:00 2001 From: itsconquest Date: Wed, 17 Jun 2020 13:36:11 +1200 Subject: [PATCH 036/195] feat(project): introduce toolkit for containerized dev (#3863) * feat(project): introduce toolkit for containerized dev * feat(project): clean up localserver shell cmd * feat(project): add install of yarn deps to grunt * feat(project): update gruntfile.js * Introduce an ARG statement for the GO_VERSION Co-authored-by: Anthony Lapenna --- build/linux/toolkit.Dockerfile | 39 +++ gruntfile.js | 16 +- package.json | 13 +- yarn.lock | 522 +++++++++++++++++++++++++++++++-- 4 files changed, 560 insertions(+), 30 deletions(-) create mode 100644 build/linux/toolkit.Dockerfile diff --git a/build/linux/toolkit.Dockerfile b/build/linux/toolkit.Dockerfile new file mode 100644 index 000000000..1f1ac4376 --- /dev/null +++ b/build/linux/toolkit.Dockerfile @@ -0,0 +1,39 @@ +FROM ubuntu + +# Expose port for the Portainer UI +EXPOSE 9000 + +WORKDIR /src/portainer + +# Set TERM as noninteractive to suppress debconf errors +RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections + +# Set default go version +ARG GO_VERSION=go1.13.11.linux-amd64 + +# Install packages +RUN apt-get update --fix-missing && apt-get install -qq \ + dialog \ + apt-utils \ + curl \ + build-essential \ + nodejs \ + git \ + wget + +# Install Yarn +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ + && apt-get update && apt-get -y install yarn + +# Install Golang +RUN cd /tmp \ + && wget -q https://dl.google.com/go/${GO_VERSION}.tar.gz \ + && tar -xf ${GO_VERSION}.tar.gz \ + && mv go /usr/local + +# Configure Go +ENV PATH "$PATH:/usr/local/go/bin" + +# Confirm installation +RUN go version && node -v && yarn -v \ No newline at end of file diff --git a/gruntfile.js b/gruntfile.js index 95fc0cb66..bd7855302 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -38,10 +38,14 @@ module.exports = function (grunt) { grunt.registerTask('start:server', ['build:server', 'copy:assets', 'shell:run_container']); - grunt.registerTask('start:client', ['config:dev', 'env:dev', 'webpack:devWatch']); + grunt.registerTask('start:localserver', ['shell:build_binary:linux:' + arch, 'shell:run_localserver']); + + grunt.registerTask('start:client', ['shell:install_yarndeps', 'config:dev', 'env:dev', 'webpack:devWatch']); grunt.registerTask('start', ['start:server', 'start:client']); + grunt.registerTask('start:toolkit', ['start:localserver', 'start:client']); + grunt.task.registerTask('release', 'release::', function (p = 'linux', a = arch) { grunt.task.run(['config:prod', 'env:prod', 'clean:all', 'copy:assets', 'shell:build_binary:' + p + ':' + a, 'shell:download_docker_binary:' + p + ':' + a, 'webpack:prod']); }); @@ -119,6 +123,8 @@ gruntfile_cfg.shell = { build_binary_azuredevops: { command: shell_build_binary_azuredevops }, download_docker_binary: { command: shell_download_docker_binary }, run_container: { command: shell_run_container }, + run_localserver: { command: shell_run_localserver, options: { async: true } }, + install_yarndeps: { command: shell_install_yarndeps }, }; function shell_build_binary(p, a) { @@ -153,6 +159,14 @@ function shell_run_container() { ].join(';'); } +function shell_run_localserver() { + return './dist/portainer --no-analytics'; +} + +function shell_install_yarndeps() { + return 'yarn'; +} + function shell_download_docker_binary(p, a) { var ps = { windows: 'win', darwin: 'mac' }; var as = { amd64: 'x86_64', arm: 'armhf', arm64: 'aarch64' }; diff --git a/package.json b/package.json index 579626fd9..f663fd108 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,11 @@ "build:client": "grunt clean:client && grunt build:client", "clean": "grunt clean:all", "start": "grunt clean:all && grunt start", + "start:localserver": "grunt start:localserver", "start:server": "grunt clean:server && grunt start:server", "start:client": "grunt clean:client && grunt start:client", "dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js", + "start:toolkit": "grunt start:toolkit", "build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer", "clean:all": "grunt clean:all", "format": "prettier --loglevel warn --write \"**/*.{js,css,html}\"" @@ -37,8 +39,9 @@ "build:server": "Build the backend", "build:client": "Build the frontend (development mode)", "clean": "Clean the entire dist folder", - "start": "Build the entire app (backend/frontend) in development mode, run it inside a container locally and start a watch process for the frontend files", + "start": "Build the entire app (backend/frontend) in development mode, run backend inside a container and start a watch process locally for the frontend files", "start:server": "Build the backend and run it inside a container", + "start:toolkit": "Build the entire app (backend/frontend) in development mode, run backend locally and start a watch process for the frontend files", "clean:all": "Deprecated. Use the clean script instead", "build:server:offline": "Deprecated. Use the build:server script instead", "format": "Should be removed before pr is merged" @@ -108,7 +111,13 @@ "grunt-contrib-clean": "^2.0.0", "grunt-contrib-copy": "^1.0.0", "grunt-env": "^0.4.4", - "grunt-shell": "^1.1.2", + "grunt-filerev": "^2.3.1", + "grunt-html2js": "~0.1.0", + "grunt-karma": "~0.4.4", + "grunt-postcss": "^0.8.0", + "grunt-replace": "^1.0.1", + "grunt-shell-spawn": "^0.4.0", + "grunt-usemin": "^3.1.1", "grunt-webpack": "^3.1.3", "gruntify-eslint": "^3.1.0", "html-loader": "^0.5.5", diff --git a/yarn.lock b/yarn.lock index cdcde4adc..d46f49040 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1038,11 +1038,23 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +LiveScript@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/LiveScript/-/LiveScript-1.0.1.tgz#fd314990e5d75010425789f525a67531b5a0b143" + integrity sha1-/TFJkOXXUBBCV4n1JaZ1MbWgsUM= + dependencies: + prelude-ls ">= 0.6.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abbrev@1.0.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" + integrity sha1-kbR5JYinc4wl813W9jdSovh3YTU= + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -1078,6 +1090,13 @@ acorn@^6.0.7, acorn@^6.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== +active-x-obfuscator@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/active-x-obfuscator/-/active-x-obfuscator-0.0.1.tgz#089b89b37145ff1d9ec74af6530be5526cae1f1a" + integrity sha1-CJuJs3FF/x2ex0r2UwvlUmyuHxo= + dependencies: + zeparser "0.0.5" + aggregate-error@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" @@ -1124,6 +1143,11 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= + angular-clipboard@^1.6.2: version "1.7.0" resolved "https://registry.yarnpkg.com/angular-clipboard/-/angular-clipboard-1.7.0.tgz#9621a6ce66eab1ea9549aa8bfb3b71352307554f" @@ -1323,6 +1347,15 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +applause@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/applause/-/applause-1.2.2.tgz#a8468579e81f67397bb5634c29953bedcd0f56c0" + integrity sha1-qEaFeegfZzl7tWNMKZU77c0PVsA= + dependencies: + cson-parser "^1.1.0" + js-yaml "^3.3.0" + lodash "^3.10.0" + aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -1478,6 +1511,11 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async@0.2.x, async@~0.2.6: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E= + async@^2.6.1, async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -1592,6 +1630,11 @@ base64-js@^1.0.2: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== +base64id@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-0.1.0.tgz#02ce0fdeee0cef4f40080e1e73e834f0b1bfce3f" + integrity sha1-As4P3u4M709ACA4ec+g08LG/zj8= + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -2186,6 +2229,11 @@ chokidar@^2.0.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@~0.6: + version "0.6.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-0.6.3.tgz#e85968fa235f21773d388c617af085bf2104425a" + integrity sha1-6Flo+iNfIXc9OIxhevCFvyEEQlo= + chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -2348,6 +2396,16 @@ codemirror@~5.30.0: resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.30.0.tgz#86e57dd5ea5535acbcf9c720797b4cefe05b5a70" integrity sha512-pfJV/7fLAUUenuGK3iANkQu1AxNLuWpeF7HV6YFDjSBMp53F8FTa2F6oPs9NKAHFweT2m08usmXUIA+7sohdew== +coffee-script@^1.10.0: + version "1.12.7" + resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" + integrity sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw== + +coffee-script@~1.6: + version "1.6.3" + resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.6.3.tgz#6355d32cf1b04cdff6b484e5e711782b2f0c39be" + integrity sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4= + coffeescript@~1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-1.10.0.tgz#e7aa8301917ef621b35d8a39f348dcdd1db7e33e" @@ -2410,6 +2468,11 @@ colormin@^1.0.5: css-color-names "0.0.4" has "^1.0.1" +colors@0.6.0-1: + version "0.6.0-1" + resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.0-1.tgz#6dbb68ceb8bc60f2b313dcc5ce1599f06d19e67a" + integrity sha1-bbtozri8YPKzE9zFzhWZ8G0Z5no= + colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -2430,6 +2493,11 @@ commander@^4.0.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781" + integrity sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E= + commander@~2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -2545,7 +2613,7 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -convert-source-map@^1.7.0: +convert-source-map@^1.0.0, convert-source-map@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== @@ -2700,6 +2768,13 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" +cson-parser@^1.1.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/cson-parser/-/cson-parser-1.3.5.tgz#7ec675e039145533bf2a6a856073f1599d9c2d24" + integrity sha1-fsZ14DkUVTO/KmqFYHPxWZ2cLSQ= + dependencies: + coffee-script "^1.10.0" + css-color-names@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" @@ -2875,6 +2950,11 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== +dateformat@1.0.2-1.2.3: + version "1.0.2-1.2.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.2-1.2.3.tgz#b0220c02de98617433b72851cf47de3df2cdbee9" + integrity sha1-sCIMAt6YYXQztyhRz0fePfLNvuk= + dateformat@~1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" @@ -2883,7 +2963,7 @@ dateformat@~1.0.12: get-stdin "^4.0.1" meow "^3.3.0" -debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: +debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -3107,6 +3187,11 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== +diff@^2.0.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" + integrity sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k= + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3275,6 +3360,11 @@ duplexify@^3.4.2, duplexify@^3.6.0: readable-stream "^2.0.0" stream-shift "^1.0.0" +each-async@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/each-async/-/each-async-0.1.3.tgz#b436025b08da2f86608025519e3096763dedfca3" + integrity sha1-tDYCWwjaL4ZggCVRnjCWdj3t/KM= + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3488,6 +3578,16 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escodegen@0.0.23: + version "0.0.23" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-0.0.23.tgz#9acf978164368e42276571f18839c823b3a844df" + integrity sha1-ms+XgWQ2jkInZXHxiDnII7OoRN8= + dependencies: + esprima "~1.0.2" + estraverse "~0.0.4" + optionalDependencies: + source-map ">= 0.1.2" + escope@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" @@ -3670,6 +3770,11 @@ espree@^5.0.1: acorn-jsx "^5.0.0" eslint-visitor-keys "^1.0.0" +esprima@1.0.x, esprima@~1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.0.4.tgz#9f557e08fc3b4d26ece9dd34f8fbf476b62585ad" + integrity sha1-n1V+CPw7TSbs6d00+Pv0drYlha0= + esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" @@ -3704,6 +3809,11 @@ estraverse@^5.0.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.0.0.tgz#ac81750b482c11cca26e4b07e83ed8f75fbcdc22" integrity sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A== +estraverse@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-0.0.4.tgz#01a0932dfee574684a598af5a67c3bf9b6428db2" + integrity sha1-AaCTLf7ldGhKWYr1pnw7+bZCjbI= + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -4125,6 +4235,14 @@ filenamify@^2.0.0: strip-outer "^1.0.0" trim-repeated "^1.0.0" +fileset@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/fileset/-/fileset-0.1.8.tgz#506b91a9396eaa7e32fb42a84077c7a0c736b741" + integrity sha1-UGuRqTluqn4y+0KoQHfHoMc2t0E= + dependencies: + glob "3.x" + minimatch "0.x" + filesize@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.3.0.tgz#53149ea3460e3b2e024962a51648aa572cf98122" @@ -4485,6 +4603,14 @@ glob-parent@^5.1.0: dependencies: is-glob "^4.0.1" +glob@3.x: + version "3.2.11" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.2.11.tgz#4a973f635b9190f715d10987d5c00fd2815ebe3d" + integrity sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0= + dependencies: + inherits "2" + minimatch "0.3" + glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -4497,6 +4623,15 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" +glob@~3.1.21: + version "3.1.21" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" + integrity sha1-0p4KBV3qUTj00H7UDomC6DwgZs0= + dependencies: + graceful-fs "~1.2.0" + inherits "1" + minimatch "~0.2.11" + glob@~5.0.0: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -4639,6 +4774,11 @@ graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1. resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== +graceful-fs@~1, graceful-fs@~1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" + integrity sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q= + "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" @@ -4649,6 +4789,11 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= +growly@~1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.1.1.tgz#eb434a0e56f0241d82864cbfd4112c70911242fa" + integrity sha1-60NKDlbwJB2Chky/1BEscJESQvo= + grunt-cli@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/grunt-cli/-/grunt-cli-1.3.2.tgz#60f12d12c1b5aae94ae3469c6b5fe24e960014e8" @@ -4701,6 +4846,27 @@ grunt-env@^0.4.4: ini "~1.3.0" lodash "~2.4.1" +grunt-filerev@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/grunt-filerev/-/grunt-filerev-2.3.1.tgz#2990210f0b5a9edc5e7198987fd1c029c6d5f4df" + integrity sha1-KZAhDwtantxecZiYf9HAKcbV9N8= + dependencies: + chalk "^1.0.0" + convert-source-map "^1.0.0" + each-async "^0.1.3" + +grunt-html2js@~0.1.0: + version "0.1.9" + resolved "https://registry.yarnpkg.com/grunt-html2js/-/grunt-html2js-0.1.9.tgz#0de0139ffabebc25f51828e8b5dcf6c654b984be" + integrity sha1-DeATn/q+vCX1GCjotdz2xlS5hL4= + +grunt-karma@~0.4.4: + version "0.4.6" + resolved "https://registry.yarnpkg.com/grunt-karma/-/grunt-karma-0.4.6.tgz#60736c14968fddf4060afe2da816e5261cab2d90" + integrity sha1-YHNsFJaP3fQGCv4tqBblJhyrLZA= + dependencies: + karma "~0.8.5" + grunt-known-options@~1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/grunt-known-options/-/grunt-known-options-1.1.1.tgz#6cc088107bd0219dc5d3e57d91923f469059804d" @@ -4737,14 +4903,41 @@ grunt-legacy-util@~1.1.1: underscore.string "~3.3.4" which "~1.3.0" -grunt-shell@^1.1.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/grunt-shell/-/grunt-shell-1.3.1.tgz#5e2beecd05d5d3787fa401028d5733d5d43b9bd1" - integrity sha1-XivuzQXV03h/pAECjVcz1dQ7m9E= +grunt-postcss@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/grunt-postcss/-/grunt-postcss-0.8.0.tgz#8f30a8af607903ce0c45f01f0be42c60e31ceb0e" + integrity sha1-jzCor2B5A84MRfAfC+QsYOMc6w4= dependencies: chalk "^1.0.0" - npm-run-path "^1.0.0" - object-assign "^4.0.0" + diff "^2.0.2" + postcss "^5.0.0" + +grunt-replace@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/grunt-replace/-/grunt-replace-1.0.1.tgz#90a79532fb89041fe427c87d425238b0f886651a" + integrity sha1-kKeVMvuJBB/kJ8h9QlI4sPiGZRo= + dependencies: + applause "1.2.2" + chalk "^1.1.0" + file-sync-cmp "^0.1.0" + lodash "^4.11.0" + +grunt-shell-spawn@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/grunt-shell-spawn/-/grunt-shell-spawn-0.4.0.tgz#3d34ebe22a20ec6bb946ae12c2f80dd38a2937ab" + integrity sha512-lfYvEQjbO1Wv+1Fk3d3XlcEpuQjyXiErZMkiz/i/tDQeMHHGF1LziqA4ZcietBAo/bM2RHdEEUJfnNWt1VRMwQ== + dependencies: + grunt ">=0.4.x" + +grunt-usemin@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/grunt-usemin/-/grunt-usemin-3.1.1.tgz#5ab679510d672cea566cc717abe8b8a009f641c2" + integrity sha1-WrZ5UQ1nLOpWbMcXq+i4oAn2QcI= + dependencies: + chalk "^1.1.1" + debug "^2.1.3" + lodash "^3.6.0" + path-exists "^1.0.0" grunt-webpack@^3.1.3: version "3.1.3" @@ -4754,7 +4947,7 @@ grunt-webpack@^3.1.3: deep-for-each "^2.0.2" lodash "^4.7.0" -grunt@^1.1.0: +grunt@>=0.4.x, grunt@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/grunt/-/grunt-1.1.0.tgz#97dc6e6add901459774a988e4f454a12e24c9d3d" integrity sha512-+NGod0grmviZ7Nzdi9am7vuRS/h76PcWDsV635mEXF0PEQMUV6Kb+OjTdsVxbi0PZmfQOjCMKb3w8CVZcqsn1g== @@ -4789,6 +4982,14 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== +handlebars@1.0.x: + version "1.0.12" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-1.0.12.tgz#18c6d3440c35e91b19b3ff582b9151ab4985d4fc" + integrity sha1-GMbTRAw16RsZs/9YK5FRq0mF1Pw= + dependencies: + optimist "~0.3" + uglify-js "~2.3" + handlebars@^4.4.3: version "4.7.3" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.3.tgz#8ece2797826886cf8082d1726ff21d2a022550ee" @@ -5058,7 +5259,7 @@ http-proxy-middleware@0.19.1: lodash "^4.17.11" micromatch "^3.1.10" -http-proxy@^1.17.0, http-proxy@^1.18.1: +http-proxy@^1.17.0, http-proxy@^1.18.1, http-proxy@~0.10: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== @@ -5301,6 +5502,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" +inherits@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" + integrity sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js= + inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" @@ -5806,6 +6012,11 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -5833,6 +6044,23 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= +istanbul@~0.1.40: + version "0.1.46" + resolved "https://registry.yarnpkg.com/istanbul/-/istanbul-0.1.46.tgz#cefeb1c787d125a6db23bd0f63b0eb9390b0b40d" + integrity sha1-zv6xx4fRJabbI70PY7Drk5CwtA0= + dependencies: + abbrev "1.0.x" + async "0.2.x" + escodegen "0.0.23" + esprima "1.0.x" + fileset "0.1.x" + handlebars "1.0.x" + mkdirp "0.3.x" + nopt "2.1.x" + resolve "0.5.x" + which "1.0.x" + wordwrap "0.0.x" + isurl@^1.0.0-alpha5: version "1.0.0" resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" @@ -5861,7 +6089,7 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.5.1, js-yaml@~3.13.1, js-yaml@~3.7.0: +js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.3.0, js-yaml@^3.5.1, js-yaml@~3.13.1, js-yaml@~3.7.0: version "3.14.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== @@ -5940,6 +6168,31 @@ jsonpointer@^4.0.0: resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk= +karma@~0.8.5: + version "0.8.8" + resolved "https://registry.yarnpkg.com/karma/-/karma-0.8.8.tgz#b7fbb2aad0d43001f7d38820c1d9cc749af2e8a0" + integrity sha1-t/uyqtDUMAH304ggwdnMdJry6KA= + dependencies: + LiveScript "1.0.1" + chokidar "~0.6" + coffee-script "~1.6" + colors "0.6.0-1" + dateformat "1.0.2-1.2.3" + glob "~3.1.21" + growly "~1.1" + http-proxy "~0.10" + istanbul "~0.1.40" + lodash "~1.1" + log4js "~0.6.3" + mime "~1.2" + minimatch "~0.2" + optimist "0.3.5" + pause "0.0.1" + q "~0.9" + rimraf "~2.1" + socket.io "~0.9.13" + xmlbuilder "0.4.2" + keyv@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" @@ -6212,7 +6465,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.7.0, lodash@~2.4.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@^3.10.0, lodash@^3.6.0, lodash@^4.0.0, lodash@^4.11.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.7.0, lodash@~1.1, lodash@~2.4.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -6247,6 +6500,14 @@ log-update@^2.3.0: cli-cursor "^2.0.0" wrap-ansi "^3.0.1" +log4js@~0.6.3: + version "0.6.38" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-0.6.38.tgz#2c494116695d6fb25480943d3fc872e662a522fd" + integrity sha1-LElBFmldb7JUgJQ9P8hy5mKlIv0= + dependencies: + readable-stream "~1.0.2" + semver "~4.3.3" + logalot@^2.0.0, logalot@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/logalot/-/logalot-2.1.0.tgz#5f8e8c90d304edf12530951a5554abb8c5e3f552" @@ -6312,6 +6573,11 @@ lpad-align@^1.0.1: longest "^1.0.0" meow "^3.3.0" +lru-cache@2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" @@ -6520,6 +6786,11 @@ mime@^2.0.3, mime@^2.4.4: resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== +mime@~1.2: + version "1.2.11" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.2.11.tgz#58203eed86e3a5ef17aed2b7d9ebd47f0a60dd10" + integrity sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA= + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -6554,6 +6825,22 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= +minimatch@0.3: + version "0.3.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd" + integrity sha1-J12O2qxPG7MyZHIInnlJyDlGmd0= + dependencies: + lru-cache "2" + sigmund "~1.0.0" + +minimatch@0.x: + version "0.4.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.4.0.tgz#bd2c7d060d2c8c8fd7cde7f1f2ed2d5b270fdb1b" + integrity sha1-vSx9Bg0sjI/Xzefx8u0tWycP2xs= + dependencies: + lru-cache "2" + sigmund "~1.0.0" + "minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -6561,6 +6848,14 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" +minimatch@~0.2, minimatch@~0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" + integrity sha1-x054BXT2PG+aCQ6Q775u9TpqdWo= + dependencies: + lru-cache "2" + sigmund "~1.0.0" + minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~0.0.1: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" @@ -6590,6 +6885,11 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mkdirp@0.3.x: + version "0.3.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" + integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= + mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" @@ -6686,6 +6986,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== +nan@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-1.0.0.tgz#ae24f8850818d662fcab5acf7f3b95bfaa2ccf38" + integrity sha1-riT4hQgY1mL8q1rPfzuVv6oszzg= + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -6816,6 +7121,13 @@ node-releases@^1.1.52: dependencies: semver "^6.3.0" +nopt@2.1.x: + version "2.1.2" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.1.2.tgz#6cccd977b80132a07731d6e8ce58c2c8303cf9af" + integrity sha1-bMzZd7gBMqB3MdbozljCyDA8+a8= + dependencies: + abbrev "1" + nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -6885,13 +7197,6 @@ npm-conf@^1.1.0: config-chain "^1.1.11" pify "^3.0.0" -npm-run-path@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-1.0.0.tgz#f5c32bf595fe81ae927daec52e82f8b000ac3c8f" - integrity sha1-9cMr9ZX+ga6Sfa7FLoL4sACsPI8= - dependencies: - path-key "^1.0.0" - npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -6923,7 +7228,7 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= -object-assign@^4.0.0, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -7072,6 +7377,13 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" +optimist@0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.5.tgz#03654b52417030312d109f39b159825b60309304" + integrity sha1-A2VLUkFwMDEtEJ85sVmCW2AwkwQ= + dependencies: + wordwrap "~0.0.2" + optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -7080,6 +7392,13 @@ optimist@^0.6.1: minimist "~0.0.1" wordwrap "~0.0.2" +optimist@~0.3, optimist@~0.3.5: + version "0.3.7" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" + integrity sha1-yQlBrVnkJzMokjB00s8ufLxuwNk= + dependencies: + wordwrap "~0.0.2" + optionator@^0.8.2: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -7092,6 +7411,11 @@ optionator@^0.8.2: type-check "~0.3.2" word-wrap "~1.2.3" +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + integrity sha1-7CLTEoBrtT5zF3Pnza788cZDEo8= + optipng-bin@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/optipng-bin/-/optipng-bin-5.1.0.tgz#a7c7ab600a3ab5a177dae2f94c2d800aa386b5a9" @@ -7417,6 +7741,11 @@ path-dirname@^1.0.0: resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= +path-exists@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081" + integrity sha1-1aiZjrce83p0w06w2eum6HjuoIE= + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -7444,11 +7773,6 @@ path-is-inside@^1.0.1, path-is-inside@^1.0.2: resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= -path-key@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-1.0.0.tgz#5d53d578019646c0d68800db4e146e6bdc2ac7af" - integrity sha1-XVPVeAGWRsDWiADbThRua9wqx68= - path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -7502,6 +7826,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -7628,6 +7957,11 @@ pngquant-bin@^5.0.0: execa "^0.10.0" logalot "^2.0.0" +policyfile@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/policyfile/-/policyfile-0.0.4.tgz#d6b82ead98ae79ebe228e2daf5903311ec982e4d" + integrity sha1-1rgurZiueeviKOLa9ZAzEeyYLk0= + popper.js@^1.16.0: version "1.16.1" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" @@ -7925,7 +8259,7 @@ postcss-zindex@^2.0.1: postcss "^5.0.4" uniqs "^2.0.0" -postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.2.16: +postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.8, postcss@^5.2.16: version "5.2.18" resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" integrity sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg== @@ -7953,6 +8287,11 @@ postcss@^7.0.0: source-map "^0.6.1" supports-color "^6.1.0" +"prelude-ls@>= 0.6.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -8091,6 +8430,11 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +q@~0.9: + version "0.9.7" + resolved "https://registry.yarnpkg.com/q/-/q-0.9.7.tgz#4de2e6cb3b29088c9e4cbc03bf9d42fb96ce2f75" + integrity sha1-TeLmyzspCIyeTLwDv51C+5bOL3U= + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -8214,6 +8558,16 @@ readable-stream@^3.0.6, readable-stream@^3.1.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@~1.0.2: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -8257,6 +8611,11 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" +redis@0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/redis/-/redis-0.7.3.tgz#ee57b7a44d25ec1594e44365d8165fa7d1d4811a" + integrity sha1-7le3pE0l7BWU5ENl2BZfp9HUgRo= + reduce-css-calc@^1.2.6: version "1.3.0" resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" @@ -8461,6 +8820,11 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +resolve@0.5.x: + version "0.5.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.5.1.tgz#15e4a222c4236bcd4cf85454412c2d0fb6524576" + integrity sha1-FeSiIsQja81M+FRUQSwtD7ZSRXY= + resolve@^1.1.6, resolve@^1.1.7, resolve@~1.1.0: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" @@ -8547,6 +8911,13 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" +rimraf@~2.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.1.4.tgz#5a6eb62eeda068f51ede50f29b3e5cd22f3d9bb2" + integrity sha1-Wm62Lu2gaPUe3lDymz5c0i89m7I= + optionalDependencies: + graceful-fs "~1" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -8703,6 +9074,11 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@~4.3.3: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + integrity sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto= + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -8834,6 +9210,11 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -8900,6 +9281,27 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +socket.io-client@0.9.16: + version "0.9.16" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-0.9.16.tgz#4da7515c5e773041d1b423970415bcc430f35fc6" + integrity sha1-TadRXF53MEHRtCOXBBW8xDDzX8Y= + dependencies: + active-x-obfuscator "0.0.1" + uglify-js "1.2.5" + ws "0.4.x" + xmlhttprequest "1.4.2" + +socket.io@~0.9.13: + version "0.9.19" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-0.9.19.tgz#490bb5fd0dc54cf002ee04e67fadfc43b848a38f" + integrity sha1-SQu1/Q3FTPAC7gTmf638Q7hIo48= + dependencies: + base64id "0.1.0" + policyfile "0.0.4" + socket.io-client "0.9.16" + optionalDependencies: + redis "0.7.3" + sockjs-client@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.4.0.tgz#c9f2568e19c8fd8173b4997ea3420e0bb306c7d5" @@ -8971,6 +9373,11 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= +"source-map@>= 0.1.2": + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -8981,6 +9388,13 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +source-map@~0.1.7: + version "0.1.43" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" + integrity sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y= + dependencies: + amdefine ">=0.0.4" + spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" @@ -9199,6 +9613,11 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -9482,6 +9901,11 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +tinycolor@0.x: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tinycolor/-/tinycolor-0.0.1.tgz#320b5a52d83abb5978d81a3e887d4aefb15a6164" + integrity sha1-MgtaUtg6u1l42Bo+iH1K77FaYWQ= + title-case@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/title-case/-/title-case-2.1.1.tgz#3e127216da58d2bc5becf137ab91dae3a7cd8faa" @@ -9625,6 +10049,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +uglify-js@1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-1.2.5.tgz#b542c2c76f78efb34b200b20177634330ff702b6" + integrity sha1-tULCx29477NLIAsgF3Y0Mw/3ArY= + uglify-js@3.4.x: version "3.4.10" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" @@ -9641,6 +10070,15 @@ uglify-js@^3.1.4: commander "~2.20.3" source-map "~0.6.1" +uglify-js@~2.3: + version "2.3.6" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.3.6.tgz#fa0984770b428b7a9b2a8058f46355d14fef211a" + integrity sha1-+gmEdwtCi3qbKoBY9GNV0U/vIRo= + dependencies: + async "~0.2.6" + optimist "~0.3.5" + source-map "~0.1.7" + ui-select@^0.19.8: version "0.19.8" resolved "https://registry.yarnpkg.com/ui-select/-/ui-select-0.19.8.tgz#74860848a7fd8bc494d9856d2f62776ea98637c1" @@ -10118,6 +10556,11 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= +which@1.0.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/which/-/which-1.0.9.tgz#460c1da0f810103d0321a9b633af9e575e64486f" + integrity sha1-RgwdoPgQED0DIam2M6+eV15kSG8= + which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1, which@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -10137,7 +10580,7 @@ word-wrap@~1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@~0.0.2: +wordwrap@0.0.x, wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= @@ -10185,6 +10628,16 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" +ws@0.4.x: + version "0.4.32" + resolved "https://registry.yarnpkg.com/ws/-/ws-0.4.32.tgz#787a6154414f3c99ed83c5772153b20feb0cec32" + integrity sha1-eHphVEFPPJntg8V3IVOyD+sM7DI= + dependencies: + commander "~2.1.0" + nan "~1.0.0" + options ">=0.0.5" + tinycolor "0.x" + ws@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" @@ -10192,6 +10645,16 @@ ws@^6.2.1: dependencies: async-limiter "~1.0.0" +xmlbuilder@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.2.tgz#1776d65f3fdbad470a08d8604cdeb1c4e540ff83" + integrity sha1-F3bWXz/brUcKCNhgTN6xxOVA/4M= + +xmlhttprequest@1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.4.2.tgz#01453a1d9bed1e8f172f6495bbf4c8c426321500" + integrity sha1-AUU6HZvtHo8XL2SVu/TIxCYyFQA= + xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -10272,3 +10735,8 @@ yauzl@^2.4.2: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" + +zeparser@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/zeparser/-/zeparser-0.0.5.tgz#03726561bc268f2e5444f54c665b7fd4a8c029e2" + integrity sha1-A3JlYbwmjy5URPVMZlt/1KjAKeI= From 576064897010791d96032670a1a875837f48889c Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 22 Jun 2020 10:38:22 +0300 Subject: [PATCH 037/195] chore(plop): fix controller import in template (#3948) --- plop-templates/component.js.hbs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plop-templates/component.js.hbs b/plop-templates/component.js.hbs index f4fb2adbd..45237ce14 100644 --- a/plop-templates/component.js.hbs +++ b/plop-templates/component.js.hbs @@ -1,6 +1,6 @@ -import {{properCase name}}Controller from './{{dashCase name}}/{{camelCase name}}Controller.js' +import {{properCase name}}Controller from './{{camelCase name}}Controller.js' angular.module('portainer.{{module}}').component('{{camelCase name}}', { - templateUrl: './{{camelCase name}}.html', - controller: {{properCase name}}Controller, -}); +templateUrl: './{{camelCase name}}.html', +controller: {{properCase name}}Controller, +}); \ No newline at end of file From 40f9078d800359f2e5993c8871087ec2f60b66b0 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 23 Jun 2020 02:46:56 +0300 Subject: [PATCH 038/195] style(docker): replace icons for containers and volumes (#3950) * fix(sidebar): replace icons for containers and volumes * fix(icons): replace icons for containers and volumes --- .../components/azure-sidebar-content/azureSidebarContent.html | 2 +- app/azure/views/containerinstances/containerinstances.html | 2 +- .../components/dockerSidebarContent/dockerSidebarContent.html | 4 ++-- app/docker/views/containers/containers.html | 2 +- app/docker/views/containers/edit/container.html | 2 +- app/docker/views/dashboard/dashboard.html | 4 ++-- app/docker/views/volumes/edit/volume.html | 2 +- app/docker/views/volumes/volumes.html | 2 +- .../components/endpoint-list/endpoint-item/endpointItem.html | 4 ++-- .../components/forms/template-form/templateForm.html | 2 +- app/portainer/views/stacks/edit/stack.html | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/azure/components/azure-sidebar-content/azureSidebarContent.html b/app/azure/components/azure-sidebar-content/azureSidebarContent.html index 01986e8e7..811ab7837 100644 --- a/app/azure/components/azure-sidebar-content/azureSidebarContent.html +++ b/app/azure/components/azure-sidebar-content/azureSidebarContent.html @@ -2,5 +2,5 @@ Dashboard diff --git a/app/azure/views/containerinstances/containerinstances.html b/app/azure/views/containerinstances/containerinstances.html index 6c0223852..cfa2337c9 100644 --- a/app/azure/views/containerinstances/containerinstances.html +++ b/app/azure/views/containerinstances/containerinstances.html @@ -11,7 +11,7 @@
Services
-
- You can select which endpoint should be part of this group by moving them to the associated endpoints table. Simply click on any endpoint entry to move it from one table - to the other. -
-
- -
-
Available endpoints
-
- -
-
- - -
-
Associated endpoints
-
- -
-
- +
@@ -155,9 +115,9 @@ loaded="$ctrl.loaded" page-type="$ctrl.pageType" table-type="associated" - retrieve-page="$ctrl.getPaginatedEndpoints" - dataset="$ctrl.endpoints.associated" - pagination-state="$ctrl.state.associated" + retrieve-page="$ctrl.getDynamicEndpoints" + dataset="$ctrl.endpoints.value" + pagination-state="$ctrl.endpoints.state" empty-dataset-message="No associated endpoint" tags="$ctrl.tags" show-tags="true" diff --git a/app/edge/components/group-form/groupFormController.js b/app/edge/components/group-form/groupFormController.js index 413a988e9..8b9c325bc 100644 --- a/app/edge/components/group-form/groupFormController.js +++ b/app/edge/components/group-form/groupFormController.js @@ -7,35 +7,27 @@ class EdgeGroupFormController { this.EndpointService = EndpointService; this.$async = $async; - this.state = { - available: { - limit: '10', - filter: '', - pageNumber: 1, - totalCount: 0, - }, - associated: { - limit: '10', - filter: '', - pageNumber: 1, - totalCount: 0, - }, - }; - this.endpoints = { - associated: [], - available: null, + state: { + limit: '10', + filter: '', + pageNumber: 1, + totalCount: 0, + }, + value: null, }; this.associateEndpoint = this.associateEndpoint.bind(this); this.dissociateEndpoint = this.dissociateEndpoint.bind(this); - this.getPaginatedEndpointsAsync = this.getPaginatedEndpointsAsync.bind(this); - this.getPaginatedEndpoints = this.getPaginatedEndpoints.bind(this); + this.getDynamicEndpointsAsync = this.getDynamicEndpointsAsync.bind(this); + this.getDynamicEndpoints = this.getDynamicEndpoints.bind(this); $scope.$watch( () => this.model, () => { - this.getPaginatedEndpoints(this.pageType, 'associated'); + if (this.model.Dynamic) { + this.getDynamicEndpoints(); + } }, true ); @@ -43,50 +35,28 @@ class EdgeGroupFormController { associateEndpoint(endpoint) { if (!_.includes(this.model.Endpoints, endpoint.Id)) { - this.endpoints.associated.push(endpoint); - this.model.Endpoints.push(endpoint.Id); - _.remove(this.endpoints.available, { Id: endpoint.Id }); + this.model.Endpoints = [...this.model.Endpoints, endpoint.Id]; } } dissociateEndpoint(endpoint) { - _.remove(this.endpoints.associated, { Id: endpoint.Id }); - _.remove(this.model.Endpoints, (id) => id === endpoint.Id); - this.endpoints.available.push(endpoint); + this.model.Endpoints = _.filter(this.model.Endpoints, (id) => id !== endpoint.Id); } - getPaginatedEndpoints(pageType, tableType) { - return this.$async(this.getPaginatedEndpointsAsync, pageType, tableType); + getDynamicEndpoints() { + return this.$async(this.getDynamicEndpointsAsync); } - async getPaginatedEndpointsAsync(pageType, tableType) { - const { pageNumber, limit, search } = this.state[tableType]; + async getDynamicEndpointsAsync() { + const { pageNumber, limit, search } = this.endpoints.state; const start = (pageNumber - 1) * limit + 1; - const query = { search, type: 4 }; - if (tableType === 'associated') { - if (this.model.Dynamic) { - query.tagIds = this.model.TagIds; - query.tagsPartialMatch = this.model.PartialMatch; - } else { - query.endpointIds = this.model.Endpoints; - } - } - const response = await this.fetchEndpoints(start, limit, query); + const query = { search, type: 4, tagIds: this.model.TagIds, tagsPartialMatch: this.model.PartialMatch }; + + const response = await this.EndpointService.endpoints(start, limit, query); + const totalCount = parseInt(response.totalCount, 10); - this.endpoints[tableType] = response.value; - this.state[tableType].totalCount = totalCount; - - if (tableType === 'available') { - this.noEndpoints = totalCount === 0; - this.endpoints[tableType] = _.filter(response.value, (endpoint) => !_.includes(this.model.Endpoints, endpoint.Id)); - } - } - - fetchEndpoints(start, limit, query) { - if (query.tagIds && !query.tagIds.length) { - return { value: [], totalCount: 0 }; - } - return this.EndpointService.endpoints(start, limit, query); + this.endpoints.value = response.value; + this.endpoints.state.totalCount = totalCount; } } diff --git a/app/edge/rest/edge-job-results.js b/app/edge/rest/edge-job-results.js new file mode 100644 index 000000000..3fb3e658f --- /dev/null +++ b/app/edge/rest/edge-job-results.js @@ -0,0 +1,14 @@ +angular.module('portainer.edge').factory('EdgeJobResults', EdgeJobResultsFactory); + +function EdgeJobResultsFactory($resource, API_ENDPOINT_EDGE_JOBS) { + return $resource( + API_ENDPOINT_EDGE_JOBS + '/:id/tasks/:taskId/:action', + {}, + { + query: { method: 'GET', isArray: true, params: { id: '@id' } }, + logFile: { method: 'GET', params: { id: '@id', taskId: '@taskId', action: 'logs' } }, + clearLogs: { method: 'DELETE', params: { id: '@id', taskId: '@taskId', action: 'logs' } }, + collectLogs: { method: 'POST', params: { id: '@id', taskId: '@taskId', action: 'logs' } }, + } + ); +} diff --git a/app/edge/rest/edge-jobs.js b/app/edge/rest/edge-jobs.js new file mode 100644 index 000000000..0cd8747dc --- /dev/null +++ b/app/edge/rest/edge-jobs.js @@ -0,0 +1,17 @@ +angular.module('portainer.edge').factory('EdgeJobs', EdgeJobsFactory); + +function EdgeJobsFactory($resource, API_ENDPOINT_EDGE_JOBS) { + return $resource( + API_ENDPOINT_EDGE_JOBS + '/:id/:action', + {}, + { + create: { method: 'POST' }, + query: { method: 'GET', isArray: true }, + get: { method: 'GET', params: { id: '@id' } }, + update: { method: 'PUT', params: { id: '@id' } }, + remove: { method: 'DELETE', params: { id: '@id' } }, + file: { method: 'GET', params: { id: '@id', action: 'file' } }, + tasks: { method: 'GET', isArray: true, params: { id: '@id', action: 'tasks' } }, + } + ); +} diff --git a/app/edge/services/edge-job.js b/app/edge/services/edge-job.js new file mode 100644 index 000000000..9ed0bc755 --- /dev/null +++ b/app/edge/services/edge-job.js @@ -0,0 +1,76 @@ +import angular from 'angular'; + +import { ScheduleCreateRequest, ScheduleUpdateRequest } from 'Portainer/models/schedule'; + +function EdgeJobService(EdgeJobs, EdgeJobResults, FileUploadService) { + var service = {}; + + service.edgeJob = edgeJob; + async function edgeJob(edgeJobId) { + try { + return await EdgeJobs.get({ id: edgeJobId }).$promise; + } catch (err) { + throw { msg: 'Unable to retrieve edgeJob', err: err }; + } + } + + service.edgeJobs = edgeJobs; + async function edgeJobs() { + try { + return await EdgeJobs.query().$promise; + } catch (err) { + throw { msg: 'Unable to retrieve edgeJobs', err: err }; + } + } + + service.jobResults = jobResults; + async function jobResults(edgeJobId) { + try { + return await EdgeJobResults.query({ id: edgeJobId }).$promise; + } catch (err) { + throw { msg: 'Unable to retrieve results associated to the edgeJob', err: err }; + } + } + + service.logFile = logFile; + function logFile(id, taskId) { + return EdgeJobResults.logFile({ id, taskId }).$promise; + } + + service.collectLogs = collectLogs; + function collectLogs(id, taskId) { + return EdgeJobResults.collectLogs({ id, taskId }).$promise; + } + + service.clearLogs = clearLogs; + function clearLogs(id, taskId) { + return EdgeJobResults.clearLogs({ id, taskId }).$promise; + } + + service.createEdgeJobFromFileContent = function (model) { + var payload = new ScheduleCreateRequest(model); + return EdgeJobs.create({ method: 'string' }, payload).$promise; + }; + + service.createEdgeJobFromFileUpload = function (model) { + var payload = new ScheduleCreateRequest(model); + return FileUploadService.createSchedule(payload); + }; + + service.updateEdgeJob = function (model) { + var payload = new ScheduleUpdateRequest(model); + return EdgeJobs.update(payload).$promise; + }; + + service.remove = function (edgeJobId) { + return EdgeJobs.remove({ id: edgeJobId }).$promise; + }; + + service.getScriptFile = function (edgeJobId) { + return EdgeJobs.file({ id: edgeJobId }).$promise; + }; + + return service; +} + +angular.module('portainer.edge').factory('EdgeJobService', EdgeJobService); diff --git a/app/edge/views/edge-jobs/create/createEdgeJobView.html b/app/edge/views/edge-jobs/create/createEdgeJobView.html new file mode 100644 index 000000000..6b9935fde --- /dev/null +++ b/app/edge/views/edge-jobs/create/createEdgeJobView.html @@ -0,0 +1,21 @@ + + + Edge Jobs > Create Edge job + + +
+
+ + + + + +
+
diff --git a/app/edge/views/edge-jobs/create/createEdgeJobView.js b/app/edge/views/edge-jobs/create/createEdgeJobView.js new file mode 100644 index 000000000..4daa6dae9 --- /dev/null +++ b/app/edge/views/edge-jobs/create/createEdgeJobView.js @@ -0,0 +1,7 @@ +import angular from 'angular'; +import CreateEdgeJobViewController from './createEdgeJobViewController'; + +angular.module('portainer.edge').component('createEdgeJobView', { + templateUrl: './createEdgeJobView.html', + controller: CreateEdgeJobViewController, +}); diff --git a/app/edge/views/edge-jobs/create/createEdgeJobViewController.js b/app/edge/views/edge-jobs/create/createEdgeJobViewController.js new file mode 100644 index 000000000..155958b55 --- /dev/null +++ b/app/edge/views/edge-jobs/create/createEdgeJobViewController.js @@ -0,0 +1,68 @@ +import angular from 'angular'; + +class CreateEdgeJobController { + constructor($async, $q, $state, EdgeJobService, GroupService, Notifications, TagService) { + this.state = { + actionInProgress: false, + }; + + this.$async = $async; + this.$q = $q; + this.$state = $state; + this.Notifications = Notifications; + this.GroupService = GroupService; + this.EdgeJobService = EdgeJobService; + this.TagService = TagService; + + this.create = this.create.bind(this); + this.createEdgeJob = this.createEdgeJob.bind(this); + this.createAsync = this.createAsync.bind(this); + } + + create(method) { + return this.$async(this.createAsync, method); + } + + async createAsync(method) { + this.state.actionInProgress = true; + + try { + await this.createEdgeJob(method, this.model); + this.Notifications.success('Edge job successfully created'); + this.$state.go('edge.jobs', {}, { reload: true }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to create Edge job'); + } + + this.state.actionInProgress = false; + } + + createEdgeJob(method, model) { + if (method === 'editor') { + return this.EdgeJobService.createEdgeJobFromFileContent(model); + } + return this.EdgeJobService.createEdgeJobFromFileUpload(model); + } + + async $onInit() { + this.model = { + Name: '', + Recurring: false, + CronExpression: '', + Endpoints: [], + FileContent: '', + File: null, + }; + + try { + const [groups, tags] = await Promise.all([this.GroupService.groups(), this.TagService.tags()]); + this.groups = groups; + this.tags = tags; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve page data'); + } + } +} + +angular.module('portainer.edge').controller('CreateEdgeJobController', CreateEdgeJobController); +export default CreateEdgeJobController; diff --git a/app/edge/views/edge-jobs/edgeJobsView.html b/app/edge/views/edge-jobs/edgeJobsView.html new file mode 100644 index 000000000..c4d8b3bdc --- /dev/null +++ b/app/edge/views/edge-jobs/edgeJobsView.html @@ -0,0 +1,23 @@ + + + + + + + Edge Jobs + + + + +
+
+ +
+
diff --git a/app/edge/views/edge-jobs/edgeJobsView.js b/app/edge/views/edge-jobs/edgeJobsView.js new file mode 100644 index 000000000..c87dd4023 --- /dev/null +++ b/app/edge/views/edge-jobs/edgeJobsView.js @@ -0,0 +1,7 @@ +import angular from 'angular'; +import EdgeJobsViewController from './edgeJobsViewController'; + +angular.module('portainer.edge').component('edgeJobsView', { + templateUrl: './edgeJobsView.html', + controller: EdgeJobsViewController, +}); diff --git a/app/edge/views/edge-jobs/edgeJobsViewController.js b/app/edge/views/edge-jobs/edgeJobsViewController.js new file mode 100644 index 000000000..85a4394d1 --- /dev/null +++ b/app/edge/views/edge-jobs/edgeJobsViewController.js @@ -0,0 +1,56 @@ +import angular from 'angular'; +import _ from 'lodash-es'; + +class EdgeJobsController { + constructor($async, $state, EdgeJobService, ModalService, Notifications) { + this.$async = $async; + this.$state = $state; + this.EdgeJobService = EdgeJobService; + this.ModalService = ModalService; + this.Notifications = Notifications; + + this.removeAction = this.removeAction.bind(this); + this.deleteJobsAsync = this.deleteJobsAsync.bind(this); + this.deleteJobs = this.deleteJobs.bind(this); + } + + removeAction(selectedItems) { + this.ModalService.confirmDeletion('Do you want to remove the selected edge job(s) ?', (confirmed) => { + if (!confirmed) { + return; + } + this.deleteJobs(selectedItems); + }); + } + + deleteJobs(edgeJobs) { + return this.$async(this.deleteJobsAsync, edgeJobs); + } + + async deleteJobsAsync(edgeJobs) { + for (let edgeJob of edgeJobs) { + try { + await this.EdgeJobService.remove(edgeJob.Id); + this.Notifications.success('Stack successfully removed', edgeJob.Name); + _.remove(this.edgeJobs, edgeJob); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to remove Edge job ' + edgeJob.Name); + } + } + + this.$state.reload(); + } + + async $onInit() { + try { + const edgeJobs = await this.EdgeJobService.edgeJobs(); + this.edgeJobs = edgeJobs; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve Edge jobs'); + this.edgeJobs = []; + } + } +} + +angular.module('portainer.edge').controller('EdgeJobsController', EdgeJobsController); +export default EdgeJobsController; diff --git a/app/edge/views/edge-jobs/edit/edgeJob.html b/app/edge/views/edge-jobs/edit/edgeJob.html new file mode 100644 index 000000000..9cdb02588 --- /dev/null +++ b/app/edge/views/edge-jobs/edit/edgeJob.html @@ -0,0 +1,52 @@ + + + + + + + Edge jobs > {{ ::$ctrl.edgeJob.Name }} + + +
+
+ + + + + Configuration + + + + + + Results + + + + + + +
+
diff --git a/app/edge/views/edge-jobs/edit/edgeJob.js b/app/edge/views/edge-jobs/edit/edgeJob.js new file mode 100644 index 000000000..f2395289f --- /dev/null +++ b/app/edge/views/edge-jobs/edit/edgeJob.js @@ -0,0 +1,7 @@ +import angular from 'angular'; +import EdgeJobController from './edgeJobController'; + +angular.module('portainer.edge').component('edgeJobView', { + templateUrl: './edgeJob.html', + controller: EdgeJobController, +}); diff --git a/app/edge/views/edge-jobs/edit/edgeJobController.js b/app/edge/views/edge-jobs/edit/edgeJobController.js new file mode 100644 index 000000000..41e757f10 --- /dev/null +++ b/app/edge/views/edge-jobs/edit/edgeJobController.js @@ -0,0 +1,159 @@ +import angular from 'angular'; +import _ from 'lodash-es'; + +class EdgeJobController { + constructor($async, $q, $state, EdgeJobService, EndpointService, FileSaver, GroupService, HostBrowserService, Notifications, TagService) { + this.state = { + actionInProgress: false, + showEditorTab: false, + }; + + this.$async = $async; + this.$q = $q; + this.$state = $state; + this.EdgeJobService = EdgeJobService; + this.EndpointService = EndpointService; + this.FileSaver = FileSaver; + this.GroupService = GroupService; + this.HostBrowserService = HostBrowserService; + this.Notifications = Notifications; + this.TagService = TagService; + + this.update = this.update.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.downloadLogs = this.downloadLogs.bind(this); + this.downloadLogsAsync = this.downloadLogsAsync.bind(this); + this.collectLogs = this.collectLogs.bind(this); + this.collectLogsAsync = this.collectLogsAsync.bind(this); + this.clearLogs = this.clearLogs.bind(this); + this.clearLogsAsync = this.clearLogsAsync.bind(this); + this.refresh = this.refresh.bind(this); + this.refreshAsync = this.refreshAsync.bind(this); + this.showEditor = this.showEditor.bind(this); + } + + update() { + return this.$async(this.updateAsync); + } + + async updateAsync() { + const model = this.edgeJob; + this.state.actionInProgress = true; + + try { + await this.EdgeJobService.updateEdgeJob(model); + this.Notifications.success('Edge job successfully updated'); + this.$state.go('edge.jobs', {}, { reload: true }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to update Edge job'); + } + + this.state.actionInProgress = false; + } + + downloadLogs(endpointId) { + return this.$async(this.downloadLogsAsync, endpointId); + } + async downloadLogsAsync(endpointId) { + try { + const data = await this.EdgeJobService.logFile(this.edgeJob.Id, endpointId); + const downloadData = new Blob([data.FileContent], { + type: 'text/plain;charset=utf-8', + }); + const logFileName = `job_${this.edgeJob.Id}_task_${endpointId}.log`; + this.FileSaver.saveAs(downloadData, logFileName); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to download file'); + } + } + + associateEndpointsToResults(results, endpoints) { + return _.map(results, (result) => { + const endpoint = _.find(endpoints, (endpoint) => endpoint.Id === result.EndpointId); + result.Endpoint = endpoint; + return result; + }); + } + + collectLogs(endpointId) { + return this.$async(this.collectLogsAsync, endpointId); + } + + async collectLogsAsync(endpointId) { + try { + await this.EdgeJobService.collectLogs(this.edgeJob.Id, endpointId); + const result = _.find(this.results, (result) => result.EndpointId === endpointId); + result.LogsStatus = 2; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to collect logs'); + } + } + + clearLogs(endpointId) { + return this.$async(this.clearLogsAsync, endpointId); + } + async clearLogsAsync(endpointId) { + try { + await this.EdgeJobService.clearLogs(this.edgeJob.Id, endpointId); + const result = _.find(this.results, (result) => result.EndpointId === endpointId); + result.LogsStatus = 1; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to clear logs'); + } + } + + refresh() { + return this.$async(this.refreshAsync); + } + async refreshAsync() { + const { id } = this.$state.params; + const results = await this.EdgeJobService.jobResults(id); + if (results.length > 0) { + const endpointIds = _.map(results, (result) => result.EndpointId); + const endpoints = await this.EndpointService.endpoints(undefined, undefined, { endpointIds }); + this.results = this.associateEndpointsToResults(results, endpoints.value); + } else { + this.results = results; + } + } + + showEditor() { + this.state.showEditorTab = true; + } + + async $onInit() { + const { id, tab } = this.$state.params; + this.state.activeTab = tab; + if (!tab || tab === 0) { + this.state.showEditorTab = true; + } + + try { + const [edgeJob, file, results, groups, tags] = await Promise.all([ + this.EdgeJobService.edgeJob(id), + this.EdgeJobService.getScriptFile(id), + this.EdgeJobService.jobResults(id), + this.GroupService.groups(), + this.TagService.tags(), + ]); + + edgeJob.FileContent = file.FileContent; + this.edgeJob = edgeJob; + this.groups = groups; + this.tags = tags; + + if (results.length > 0) { + const endpointIds = _.map(results, (result) => result.EndpointId); + const endpoints = await this.EndpointService.endpoints(undefined, undefined, { endpointIds }); + this.results = this.associateEndpointsToResults(results, endpoints.value); + } else { + this.results = results; + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve endpoint list'); + } + } +} + +angular.module('portainer.edge').controller('EdgeJobController', EdgeJobController); +export default EdgeJobController; diff --git a/app/edge/views/groups/edgeGroupsViewController.js b/app/edge/views/groups/edgeGroupsViewController.js index d589f7851..1d7f6fc32 100644 --- a/app/edge/views/groups/edgeGroupsViewController.js +++ b/app/edge/views/groups/edgeGroupsViewController.js @@ -14,7 +14,12 @@ class EdgeGroupsController { } async $onInit() { - this.items = await this.EdgeGroupService.groups(); + try { + this.items = await this.EdgeGroupService.groups(); + } catch (err) { + this.items = []; + this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups'); + } } removeAction(selectedItems) { diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 04dfa7235..2a43cb0dd 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -316,39 +316,6 @@ angular.module('portainer.app', []).config([ }, }; - var schedules = { - name: 'portainer.schedules', - url: '/schedules', - views: { - 'content@': { - templateUrl: './views/schedules/schedules.html', - controller: 'SchedulesController', - }, - }, - }; - - var schedule = { - name: 'portainer.schedules.schedule', - url: '/:id', - views: { - 'content@': { - templateUrl: './views/schedules/edit/schedule.html', - controller: 'ScheduleController', - }, - }, - }; - - var scheduleCreation = { - name: 'portainer.schedules.new', - url: '/new', - views: { - 'content@': { - templateUrl: './views/schedules/create/createschedule.html', - controller: 'CreateScheduleController', - }, - }, - }; - var settings = { name: 'portainer.settings', url: '/settings', @@ -542,9 +509,6 @@ angular.module('portainer.app', []).config([ $stateRegistryProvider.register(registry); $stateRegistryProvider.register(registryAccess); $stateRegistryProvider.register(registryCreation); - $stateRegistryProvider.register(schedules); - $stateRegistryProvider.register(schedule); - $stateRegistryProvider.register(scheduleCreation); $stateRegistryProvider.register(settings); $stateRegistryProvider.register(settingsAuthentication); $stateRegistryProvider.register(stacks); diff --git a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.html b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.html new file mode 100644 index 000000000..e0d6f2e06 --- /dev/null +++ b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.html @@ -0,0 +1,50 @@ +
+ You can select which endpoint should be part of this group by moving them to the associated endpoints table. Simply click on any endpoint entry to move it from one table to the + other. +
+
+ +
+
Available endpoints
+
+ +
+
+ + +
+
Associated endpoints
+
+ +
+
+ +
diff --git a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.js b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.js new file mode 100644 index 000000000..2e0cce768 --- /dev/null +++ b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelector.js @@ -0,0 +1,16 @@ +import angular from 'angular'; +import AssociatedEndpointsSelectorController from './associatedEndpointsSelectorController'; + +angular.module('portainer.app').component('associatedEndpointsSelector', { + templateUrl: './associatedEndpointsSelector.html', + controller: AssociatedEndpointsSelectorController, + bindings: { + endpointIds: '<', + tags: '<', + groups: '<', + hasBackendPagination: '<', + + onAssociate: '<', + onDissociate: '<', + }, +}); diff --git a/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js new file mode 100644 index 000000000..e129552d5 --- /dev/null +++ b/app/portainer/components/associated-endpoints-selector/associatedEndpointsSelectorController.js @@ -0,0 +1,106 @@ +import angular from 'angular'; +import _ from 'lodash-es'; + +class AssoicatedEndpointsSelectorController { + /* @ngInject */ + constructor($async, EndpointService) { + this.$async = $async; + this.EndpointService = EndpointService; + + this.state = { + available: { + limit: '10', + filter: '', + pageNumber: 1, + totalCount: 0, + }, + associated: { + limit: '10', + filter: '', + pageNumber: 1, + totalCount: 0, + }, + }; + + this.endpoints = { + associated: [], + available: null, + }; + + this.getEndpoints = this.getEndpoints.bind(this); + this.getEndpointsAsync = this.getEndpointsAsync.bind(this); + this.getAssociatedEndpoints = this.getAssociatedEndpoints.bind(this); + this.getAssociatedEndpointsAsync = this.getAssociatedEndpointsAsync.bind(this); + this.associateEndpoint = this.associateEndpoint.bind(this); + this.dissociateEndpoint = this.dissociateEndpoint.bind(this); + } + + $onInit() { + this.loadData(); + } + + $onChanges({ endpointIds }) { + if (endpointIds && endpointIds.currentValue) { + this.loadData(); + } + } + + loadData() { + this.getAssociatedEndpoints(); + this.getEndpoints(); + } + + getEndpoints() { + return this.$async(this.getEndpointsAsync); + } + + async getEndpointsAsync() { + const { start, search, limit } = this.getPaginationData('available'); + const query = { search, type: 4 }; + + const response = await this.EndpointService.endpoints(start, limit, query); + + const endpoints = _.filter(response.value, (endpoint) => !_.includes(this.endpointIds, endpoint.Id)); + this.setTableData('available', endpoints, response.totalCount); + this.noEndpoints = this.state.available.totalCount === 0; + } + + getAssociatedEndpoints() { + return this.$async(this.getAssociatedEndpointsAsync); + } + + async getAssociatedEndpointsAsync() { + let response = { value: [], totalCount: 0 }; + if (this.endpointIds.length > 0) { + const { start, search, limit } = this.getPaginationData('associated'); + const query = { search, type: 4, endpointIds: this.endpointIds }; + + response = await this.EndpointService.endpoints(start, limit, query); + } + + this.setTableData('associated', response.value, response.totalCount); + } + + associateEndpoint(endpoint) { + this.onAssociate(endpoint); + } + + dissociateEndpoint(endpoint) { + this.onDissociate(endpoint); + } + + getPaginationData(tableType) { + const { pageNumber, limit, search } = this.state[tableType]; + const start = (pageNumber - 1) * limit + 1; + + return { start, search, limit }; + } + + setTableData(tableType, endpoints, totalCount) { + this.endpoints[tableType] = endpoints; + this.state[tableType].totalCount = parseInt(totalCount, 10); + } +} + +angular.module('portainer.app').controller('AssoicatedEndpointsSelectorController', AssoicatedEndpointsSelectorController); +export default AssoicatedEndpointsSelectorController; diff --git a/app/portainer/components/beta-panel/betaPanel.html b/app/portainer/components/beta-panel/betaPanel.html new file mode 100644 index 000000000..01d0db0f1 --- /dev/null +++ b/app/portainer/components/beta-panel/betaPanel.html @@ -0,0 +1,8 @@ + + +

+ + This is a beta feature. +

+
+
diff --git a/app/portainer/components/beta-panel/betaPanel.js b/app/portainer/components/beta-panel/betaPanel.js new file mode 100644 index 000000000..9af432eac --- /dev/null +++ b/app/portainer/components/beta-panel/betaPanel.js @@ -0,0 +1,3 @@ +angular.module('portainer.app').component('betaPanel', { + templateUrl: './betaPanel.html', +}); diff --git a/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.html b/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.html deleted file mode 100644 index ddd217bc1..000000000 --- a/app/portainer/components/datatables/schedule-tasks-datatable/scheduleTasksDatatable.html +++ /dev/null @@ -1,105 +0,0 @@ -
-
-
- - -
-
- - {{ $ctrl.titleText }} -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - -
- - - - Endpoint - - - - Id - - - - - - State - - - - - - - - Created - -
- {{ item.Endpoint.Name }} - Download logs - - {{ item.Id | truncate: 32 }} - - - - {{ item.Status }} - - - - {{ item.Created | getisodatefromtimestamp }} - - -
Loading...
No tasks available.
-
- -
-
-
-
-
diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.js b/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.js deleted file mode 100644 index 176448cb7..000000000 --- a/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('portainer.app').component('schedulesDatatable', { - templateUrl: './schedulesDatatable.html', - controller: 'SchedulesDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - removeAction: '<', - }, -}); diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js deleted file mode 100644 index d5a19c617..000000000 --- a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js +++ /dev/null @@ -1,48 +0,0 @@ -angular.module('portainer.app').controller('SchedulesDatatableController', [ - '$scope', - '$controller', - 'DatatableService', - function ($scope, $controller, DatatableService) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - - /** - * Do not allow items - */ - this.allowSelection = function (item) { - return item.JobType === 1; - }; - - this.$onInit = function () { - this.setDefaults(); - this.prepareTableFromDataset(); - - this.state.orderBy = this.orderBy; - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; - } - - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - this.onTextFilterChange(); - } - - var storedFilters = DatatableService.getDataTableFilters(this.tableKey); - if (storedFilters !== null) { - this.filters = storedFilters; - } - if (this.filters && this.filters.state) { - this.filters.state.open = false; - } - - var storedSettings = DatatableService.getDataTableSettings(this.tableKey); - if (storedSettings !== null) { - this.settings = storedSettings; - this.settings.open = false; - } - this.onSettingsRepeaterChange(); - }; - }, -]); diff --git a/app/portainer/components/forms/execute-job-form/execute-job-form-controller.js b/app/portainer/components/forms/execute-job-form/execute-job-form-controller.js deleted file mode 100644 index 667737dd0..000000000 --- a/app/portainer/components/forms/execute-job-form/execute-job-form-controller.js +++ /dev/null @@ -1,74 +0,0 @@ -angular.module('portainer.app').controller('JobFormController', [ - '$state', - 'LocalStorage', - 'EndpointService', - 'EndpointProvider', - 'Notifications', - function ($state, LocalStorage, EndpointService, EndpointProvider, Notifications) { - var ctrl = this; - - ctrl.$onInit = onInit; - ctrl.editorUpdate = editorUpdate; - ctrl.executeJob = executeJob; - - ctrl.state = { - Method: 'editor', - formValidationError: '', - actionInProgress: false, - }; - - ctrl.formValues = { - Image: 'ubuntu:latest', - JobFileContent: '', - JobFile: null, - }; - - function onInit() { - var storedImage = LocalStorage.getJobImage(); - if (storedImage) { - ctrl.formValues.Image = storedImage; - } - } - - function editorUpdate(cm) { - ctrl.formValues.JobFileContent = cm.getValue(); - } - - function createJob(image, method) { - var endpointId = EndpointProvider.endpointID(); - var nodeName = ctrl.nodeName; - - if (method === 'editor') { - var jobFileContent = ctrl.formValues.JobFileContent; - return EndpointService.executeJobFromFileContent(image, jobFileContent, endpointId, nodeName); - } - - var jobFile = ctrl.formValues.JobFile; - return EndpointService.executeJobFromFileUpload(image, jobFile, endpointId, nodeName); - } - - function executeJob() { - var method = ctrl.state.Method; - if (method === 'editor' && ctrl.formValues.JobFileContent === '') { - ctrl.state.formValidationError = 'Script file content must not be empty'; - return; - } - - var image = ctrl.formValues.Image; - LocalStorage.storeJobImage(image); - - ctrl.state.actionInProgress = true; - createJob(image, method) - .then(function success() { - Notifications.success('Job successfully created'); - $state.go('^'); - }) - .catch(function error(err) { - Notifications.error('Job execution failure', err); - }) - .finally(function final() { - ctrl.state.actionInProgress = false; - }); - } - }, -]); diff --git a/app/portainer/components/forms/execute-job-form/execute-job-form.html b/app/portainer/components/forms/execute-job-form/execute-job-form.html deleted file mode 100644 index 60cc8556b..000000000 --- a/app/portainer/components/forms/execute-job-form/execute-job-form.html +++ /dev/null @@ -1,109 +0,0 @@ -
- -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- -
- - This job will run inside a privileged container on the host. You can access the host filesystem under the - /host folder. - -
- -
- Job creation -
-
-
-
-
- - -
-
- - -
-
-
- - -
-
- Web editor -
-
-
- -
-
-
- - -
-
- Upload -
-
- - You can upload a script file from your computer. - -
-
-
- - - {{ $ctrl.formValues.JobFile.name }} - - -
-
-
- - -
- Actions -
-
-
- - - {{ $ctrl.state.formValidationError }} - -
-
- -
diff --git a/app/portainer/components/forms/execute-job-form/execute-job-form.js b/app/portainer/components/forms/execute-job-form/execute-job-form.js deleted file mode 100644 index 97a1637c7..000000000 --- a/app/portainer/components/forms/execute-job-form/execute-job-form.js +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('portainer.app').component('executeJobForm', { - templateUrl: './execute-job-form.html', - controller: 'JobFormController', - bindings: { - nodeName: '<', - }, -}); diff --git a/app/portainer/components/forms/schedule-form/schedule-form.js b/app/portainer/components/forms/schedule-form/schedule-form.js deleted file mode 100644 index 9be082a01..000000000 --- a/app/portainer/components/forms/schedule-form/schedule-form.js +++ /dev/null @@ -1,81 +0,0 @@ -import moment from 'moment'; - -angular.module('portainer.app').component('scheduleForm', { - templateUrl: './scheduleForm.html', - controller: function () { - var ctrl = this; - - ctrl.state = { - formValidationError: '', - }; - - ctrl.scheduleValues = [ - { - displayed: 'Every hour', - cron: '0 * * * *', - }, - { - displayed: 'Every 2 hours', - cron: '0 */2 * * *', - }, - { - displayed: 'Every day', - cron: '0 0 * * *', - }, - ]; - - ctrl.formValues = { - datetime: ctrl.model.CronExpression ? cronToDatetime(ctrl.model.CronExpression) : moment(), - scheduleValue: ctrl.scheduleValues[0], - cronMethod: ctrl.model.Recurring ? 'advanced' : 'basic', - }; - - function cronToDatetime(cron) { - var strings = cron.split(' '); - if (strings.length !== 5) { - return moment(); - } - return moment(cron, 's m H D M'); - } - - function datetimeToCron(datetime) { - var date = moment(datetime); - return '0 '.concat(date.minutes(), ' ', date.hours(), ' ', date.date(), ' ', date.month() + 1, ' *'); - } - - this.action = function () { - ctrl.state.formValidationError = ''; - - if (ctrl.model.Job.Method === 'editor' && ctrl.model.Job.FileContent === '') { - ctrl.state.formValidationError = 'Script file content must not be empty'; - return; - } - - if (ctrl.formValues.cronMethod === 'basic') { - if (ctrl.model.Recurring === false) { - ctrl.model.CronExpression = datetimeToCron(ctrl.formValues.datetime); - } else { - ctrl.model.CronExpression = ctrl.formValues.scheduleValue.cron; - } - } else { - ctrl.model.Recurring = true; - } - ctrl.formAction(); - }; - - this.editorUpdate = function (cm) { - ctrl.model.Job.FileContent = cm.getValue(); - }; - }, - bindings: { - model: '=', - endpoints: '<', - groups: '<', - tags: '<', - addLabelAction: '<', - removeLabelAction: '<', - formAction: '<', - formActionLabel: '@', - actionInProgress: '<', - }, -}); diff --git a/app/portainer/components/forms/schedule-form/scheduleForm.html b/app/portainer/components/forms/schedule-form/scheduleForm.html deleted file mode 100644 index e08177217..000000000 --- a/app/portainer/components/forms/schedule-form/scheduleForm.html +++ /dev/null @@ -1,303 +0,0 @@ -
-
- Information -
-
- -

- Due to how schedules behave differently on Edge endpoints and other - endpoints it is recommended to create specific schedules that will only target one type of endpoint. -

-
-
-
- Schedule configuration -
- -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- - - -
- Schedule configuration -
-
-
-
-
- - -
-
- - -
-
-
- - -
-
- -
- -
-
- -
-
- -
- -
-
-
-
-

This field is required.

-
-
-
-
-
- - -
-
- -
- -
-
-
-
-

This field is required.

-
-
-
-
-
- -
- - -
-
- -
- -
-
-
-
-
-

This field is required.

-
-
-
-
- -

- You can refer to the following documentation to get more information - about the supported cron expression format. -

-

- Edge endpoint schedules are managed by cron on the - underlying host. You need to use a valid cron expression that is different from the documentation above. -

-
-
-
- - -
- Job configuration -
-
- -

This configuration will be ignored for Edge endpoint schedules.

-
-
- -
- -
- -
-
-
-
-
-

This field is required.

-
-
-
- - -
- -
- -
- -
- -
-
- - -
-
- Job content -
-
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- Web editor -
-
- -

- This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the - /host folder. -

-

- Edge endpoint schedules are managed by cron on the - underlying host. You have full access to the filesystem without having to use the /host folder. -

-
-
-
-
- -
-
-
- - -
-
- Upload -
-
- - You can upload a script file from your computer. - -
-
-
- - - {{ $ctrl.model.Job.File.name }} - - -
-
-
- -
- Target endpoints -
- - - - -
- Actions -
-
-
- - - {{ $ctrl.state.formValidationError }} - -
-
- -
diff --git a/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js b/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js deleted file mode 100644 index 1862307f0..000000000 --- a/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js +++ /dev/null @@ -1,10 +0,0 @@ -angular.module('portainer.app').component('multiEndpointSelector', { - templateUrl: './multiEndpointSelector.html', - controller: 'MultiEndpointSelectorController', - bindings: { - model: '=', - endpoints: '<', - groups: '<', - tags: '<', - }, -}); diff --git a/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html b/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html deleted file mode 100644 index adb33e059..000000000 --- a/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - {{ $item.Name }} - - {{ $ctrl.tagIdsToTagNames($item.TagIds) | arraytostr }} - - - - - {{ endpoint.Name }} - - {{ $ctrl.tagIdsToTagNames(endpoint.TagIds) | arraytostr }} - - - diff --git a/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js b/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js deleted file mode 100644 index 608825904..000000000 --- a/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js +++ /dev/null @@ -1,48 +0,0 @@ -import _ from 'lodash-es'; -import PortainerEndpointTagHelper from 'Portainer/helpers/tagHelper'; - -import angular from 'angular'; - -class MultiEndpointSelectorController { - /* @ngInject */ - constructor() { - this.sortGroups = this.sortGroups.bind(this); - this.groupEndpoints = this.groupEndpoints.bind(this); - this.tagIdsToTagNames = this.tagIdsToTagNames.bind(this); - } - - sortGroups(groups) { - return _.sortBy(groups, ['name']); - } - - groupEndpoints(endpoint) { - for (var i = 0; i < this.availableGroups.length; i++) { - var group = this.availableGroups[i]; - - if (endpoint.GroupId === group.Id) { - return group.Name; - } - } - } - - tagIdsToTagNames(tagIds) { - return PortainerEndpointTagHelper.idsToTagNames(this.tags, tagIds); - } - - filterEmptyGroups() { - this.availableGroups = _.filter(this.groups, (group) => _.some(this.endpoints, (endpoint) => endpoint.GroupId == group.Id)); - } - - $onInit() { - this.filterEmptyGroups(); - } - - $onChanges({ endpoints, groups }) { - if (endpoints || groups) { - this.filterEmptyGroups(); - } - } -} - -export default MultiEndpointSelectorController; -angular.module('portainer.app').controller('MultiEndpointSelectorController', MultiEndpointSelectorController); diff --git a/app/portainer/models/schedule.js b/app/portainer/models/schedule.js index aaec7773d..2f8c4500a 100644 --- a/app/portainer/models/schedule.js +++ b/app/portainer/models/schedule.js @@ -1,67 +1,10 @@ -import _ from 'lodash-es'; -import { createStatus } from '../../docker/models/container'; - -export function ScheduleDefaultModel() { - this.Name = ''; - this.Recurring = false; - this.CronExpression = ''; - this.JobType = 1; - this.Job = new ScriptExecutionDefaultJobModel(); -} - -function ScriptExecutionDefaultJobModel() { - this.Image = 'ubuntu:latest'; - this.Endpoints = []; - this.FileContent = ''; - this.File = null; - this.Method = 'editor'; -} - -export function ScheduleModel(data) { - this.Id = data.Id; - this.Name = data.Name; - this.Recurring = data.Recurring; - this.JobType = data.JobType; - this.CronExpression = data.CronExpression; - this.Created = data.Created; - this.EdgeSchedule = data.EdgeSchedule; - if (this.JobType === 1) { - this.Job = new ScriptExecutionJobModel(data.ScriptExecutionJob, data.EdgeSchedule); - } -} - -function ScriptExecutionJobModel(data, edgeSchedule) { - this.Image = data.Image; - this.Endpoints = data.Endpoints; - - if (edgeSchedule !== null) { - this.Endpoints = _.concat(data.Endpoints, edgeSchedule.Endpoints); - } - - this.FileContent = ''; - this.Method = 'editor'; - this.RetryCount = data.RetryCount; - this.RetryInterval = data.RetryInterval; -} - -export function ScriptExecutionTaskModel(data) { - this.Id = data.Id; - this.EndpointId = data.EndpointId; - this.Status = createStatus(data.Status); - this.Created = data.Created; - this.Edge = data.Edge; -} - export function ScheduleCreateRequest(model) { this.Name = model.Name; this.Recurring = model.Recurring; this.CronExpression = model.CronExpression; - this.Image = model.Job.Image; - this.Endpoints = model.Job.Endpoints; - this.FileContent = model.Job.FileContent; - this.RetryCount = model.Job.RetryCount; - this.RetryInterval = model.Job.RetryInterval; - this.File = model.Job.File; + this.Endpoints = model.Endpoints; + this.FileContent = model.FileContent; + this.File = model.File; } export function ScheduleUpdateRequest(model) { @@ -69,9 +12,6 @@ export function ScheduleUpdateRequest(model) { this.Name = model.Name; this.Recurring = model.Recurring; this.CronExpression = model.CronExpression; - this.Image = model.Job.Image; - this.Endpoints = model.Job.Endpoints; - this.FileContent = model.Job.FileContent; - this.RetryCount = model.Job.RetryCount; - this.RetryInterval = model.Job.RetryInterval; + this.Endpoints = model.Endpoints; + this.FileContent = model.FileContent; } diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index 923675cbd..65a6b82d4 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -20,7 +20,6 @@ angular.module('portainer.app').factory('Endpoints', [ remove: { method: 'DELETE', params: { id: '@id' } }, snapshots: { method: 'POST', params: { action: 'snapshot' } }, snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } }, - executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } }, status: { method: 'GET', params: { id: '@id', action: 'status' } }, } ); diff --git a/app/portainer/rest/schedule.js b/app/portainer/rest/schedule.js deleted file mode 100644 index da8ded80f..000000000 --- a/app/portainer/rest/schedule.js +++ /dev/null @@ -1,20 +0,0 @@ -angular.module('portainer.app').factory('Schedules', [ - '$resource', - 'API_ENDPOINT_SCHEDULES', - function SchedulesFactory($resource, API_ENDPOINT_SCHEDULES) { - 'use strict'; - return $resource( - API_ENDPOINT_SCHEDULES + '/:id/:action', - {}, - { - create: { method: 'POST' }, - query: { method: 'GET', isArray: true }, - get: { method: 'GET', params: { id: '@id' } }, - update: { method: 'PUT', params: { id: '@id' } }, - remove: { method: 'DELETE', params: { id: '@id' } }, - file: { method: 'GET', params: { id: '@id', action: 'file' } }, - tasks: { method: 'GET', isArray: true, params: { id: '@id', action: 'tasks' } }, - } - ); - }, -]); diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 511983ade..1501e9ed8 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -11,6 +11,9 @@ angular.module('portainer.app').factory('EndpointService', [ }; service.endpoints = function (start, limit, { search, type, tagIds, endpointIds, tagsPartialMatch } = {}) { + if (tagIds && !tagIds.length) { + return Promise.resolve({ value: [], totalCount: 0 }); + } return Endpoints.query({ start, limit, search, type, tagIds: JSON.stringify(tagIds), endpointIds: JSON.stringify(endpointIds), tagsPartialMatch }).$promise; }; @@ -126,19 +129,6 @@ angular.module('portainer.app').factory('EndpointService', [ return deferred.promise; }; - service.executeJobFromFileUpload = function (image, jobFile, endpointId, nodeName) { - return FileUploadService.executeEndpointJob(image, jobFile, endpointId, nodeName); - }; - - service.executeJobFromFileContent = function (image, jobFileContent, endpointId, nodeName) { - var payload = { - Image: image, - FileContent: jobFileContent, - }; - - return Endpoints.executeJob({ id: endpointId, method: 'string', nodeName: nodeName }, payload).$promise; - }; - return service; }, ]); diff --git a/app/portainer/services/api/scheduleService.js b/app/portainer/services/api/scheduleService.js deleted file mode 100644 index e80f5517f..000000000 --- a/app/portainer/services/api/scheduleService.js +++ /dev/null @@ -1,85 +0,0 @@ -import { ScheduleModel, ScheduleCreateRequest, ScheduleUpdateRequest, ScriptExecutionTaskModel } from '../../models/schedule'; - -angular.module('portainer.app').factory('ScheduleService', [ - '$q', - 'Schedules', - 'FileUploadService', - function ScheduleService($q, Schedules, FileUploadService) { - 'use strict'; - var service = {}; - - service.schedule = function (scheduleId) { - var deferred = $q.defer(); - - Schedules.get({ id: scheduleId }) - .$promise.then(function success(data) { - var schedule = new ScheduleModel(data); - deferred.resolve(schedule); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve schedule', err: err }); - }); - - return deferred.promise; - }; - - service.schedules = function () { - var deferred = $q.defer(); - - Schedules.query() - .$promise.then(function success(data) { - var schedules = data.map(function (item) { - return new ScheduleModel(item); - }); - deferred.resolve(schedules); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve schedules', err: err }); - }); - - return deferred.promise; - }; - - service.scriptExecutionTasks = function (scheduleId) { - var deferred = $q.defer(); - - Schedules.tasks({ id: scheduleId }) - .$promise.then(function success(data) { - var tasks = data.map(function (item) { - return new ScriptExecutionTaskModel(item); - }); - deferred.resolve(tasks); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve tasks associated to the schedule', err: err }); - }); - - return deferred.promise; - }; - - service.createScheduleFromFileContent = function (model) { - var payload = new ScheduleCreateRequest(model); - return Schedules.create({ method: 'string' }, payload).$promise; - }; - - service.createScheduleFromFileUpload = function (model) { - var payload = new ScheduleCreateRequest(model); - return FileUploadService.createSchedule(payload); - }; - - service.updateSchedule = function (model) { - var payload = new ScheduleUpdateRequest(model); - return Schedules.update(payload).$promise; - }; - - service.deleteSchedule = function (scheduleId) { - return Schedules.remove({ id: scheduleId }).$promise; - }; - - service.getScriptFile = function (scheduleId) { - return Schedules.file({ id: scheduleId }).$promise; - }; - - return service; - }, -]); diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 5b2e7d67f..5300311c1 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -104,17 +104,6 @@ angular.module('portainer.app').factory('FileUploadService', [ }); }; - service.executeEndpointJob = function (imageName, file, endpointId, nodeName) { - return Upload.upload({ - url: 'api/endpoints/' + endpointId + '/job?method=file&nodeName=' + nodeName, - data: { - File: file, - Image: imageName, - }, - ignoreLoadingBar: true, - }); - }; - service.createEndpoint = function (name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, checkinInterval) { return Upload.upload({ url: 'api/endpoints', diff --git a/app/portainer/views/schedules/create/createScheduleController.js b/app/portainer/views/schedules/create/createScheduleController.js deleted file mode 100644 index c4364654e..000000000 --- a/app/portainer/views/schedules/create/createScheduleController.js +++ /dev/null @@ -1,55 +0,0 @@ -import { ScheduleDefaultModel } from '../../../models/schedule'; - -angular - .module('portainer.app') - .controller('CreateScheduleController', function CreateScheduleController($q, $scope, $state, Notifications, EndpointService, GroupService, ScheduleService, TagService) { - $scope.state = { - actionInProgress: false, - }; - - $scope.create = create; - - function create() { - var model = $scope.model; - - $scope.state.actionInProgress = true; - createSchedule(model) - .then(function success() { - Notifications.success('Schedule successfully created'); - $state.go('portainer.schedules', {}, { reload: true }); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create schedule'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - } - - function createSchedule(model) { - if (model.Job.Method === 'editor') { - return ScheduleService.createScheduleFromFileContent(model); - } - return ScheduleService.createScheduleFromFileUpload(model); - } - - function initView() { - $scope.model = new ScheduleDefaultModel(); - - $q.all({ - endpoints: EndpointService.endpoints(), - groups: GroupService.groups(), - tags: TagService.tags(), - }) - .then(function success(data) { - $scope.endpoints = data.endpoints.value; - $scope.groups = data.groups; - $scope.tags = data.tags; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoint list'); - }); - } - - initView(); - }); diff --git a/app/portainer/views/schedules/create/createschedule.html b/app/portainer/views/schedules/create/createschedule.html deleted file mode 100644 index f09165ccd..000000000 --- a/app/portainer/views/schedules/create/createschedule.html +++ /dev/null @@ -1,22 +0,0 @@ - - - Schedules > Add schedule - - -
-
- - - - - -
-
diff --git a/app/portainer/views/schedules/edit/schedule.html b/app/portainer/views/schedules/edit/schedule.html deleted file mode 100644 index 1ebefc902..000000000 --- a/app/portainer/views/schedules/edit/schedule.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - Schedules > {{ ::schedule.Name }} - - -
-
- - - - - Configuration - - - - - - Tasks - -
- Information -
-
- - Tasks are retrieved across all endpoints via snapshots. Data available in this view might not be up-to-date. - -
- -
- Tasks -
- -
-
-
-
-
-
diff --git a/app/portainer/views/schedules/edit/scheduleController.js b/app/portainer/views/schedules/edit/scheduleController.js deleted file mode 100644 index 99d06a420..000000000 --- a/app/portainer/views/schedules/edit/scheduleController.js +++ /dev/null @@ -1,113 +0,0 @@ -angular - .module('portainer.app') - .controller('ScheduleController', function ScheduleController( - $q, - $scope, - $transition$, - $state, - Notifications, - EndpointService, - GroupService, - ScheduleService, - EndpointProvider, - HostBrowserService, - FileSaver, - TagService - ) { - $scope.state = { - actionInProgress: false, - }; - - $scope.update = update; - $scope.goToContainerLogs = goToContainerLogs; - $scope.getEdgeTaskLogs = getEdgeTaskLogs; - - function update() { - var model = $scope.schedule; - - $scope.state.actionInProgress = true; - ScheduleService.updateSchedule(model) - .then(function success() { - Notifications.success('Schedule successfully updated'); - $state.go('portainer.schedules', {}, { reload: true }); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update schedule'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - } - - function goToContainerLogs(endpointId, containerId) { - EndpointProvider.setEndpointID(endpointId); - $state.go('docker.containers.container.logs', { id: containerId }); - } - - function getEdgeTaskLogs(endpointId, scheduleId) { - var currentId = EndpointProvider.endpointID(); - EndpointProvider.setEndpointID(endpointId); - - var filePath = '/host/opt/portainer/scripts/' + scheduleId + '.log'; - HostBrowserService.get(filePath) - .then(function onFileReceived(data) { - var downloadData = new Blob([data.file], { - type: 'text/plain;charset=utf-8', - }); - FileSaver.saveAs(downloadData, scheduleId + '.log'); - }) - .catch(function notifyOnError(err) { - Notifications.error('Failure', err, 'Unable to download file'); - }) - .finally(function final() { - EndpointProvider.setEndpointID(currentId); - }); - } - - function associateEndpointsToTasks(tasks, endpoints) { - for (var i = 0; i < tasks.length; i++) { - var task = tasks[i]; - - for (var j = 0; j < endpoints.length; j++) { - var endpoint = endpoints[j]; - - if (task.EndpointId === endpoint.Id) { - task.Endpoint = endpoint; - break; - } - } - } - } - - function initView() { - var id = $transition$.params().id; - - $q.all({ - schedule: ScheduleService.schedule(id), - file: ScheduleService.getScriptFile(id), - tasks: ScheduleService.scriptExecutionTasks(id), - endpoints: EndpointService.endpoints(), - groups: GroupService.groups(), - tags: TagService.tags(), - }) - .then(function success(data) { - var schedule = data.schedule; - schedule.Job.FileContent = data.file.ScheduleFileContent; - - var endpoints = data.endpoints.value; - var tasks = data.tasks; - associateEndpointsToTasks(tasks, endpoints); - - $scope.schedule = schedule; - $scope.tasks = data.tasks; - $scope.endpoints = data.endpoints.value; - $scope.groups = data.groups; - $scope.tags = data.tags; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoint list'); - }); - } - - initView(); - }); diff --git a/app/portainer/views/schedules/schedules.html b/app/portainer/views/schedules/schedules.html deleted file mode 100644 index 3ab0d7826..000000000 --- a/app/portainer/views/schedules/schedules.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Schedules - - -
-
- -
-
diff --git a/app/portainer/views/schedules/schedulesController.js b/app/portainer/views/schedules/schedulesController.js deleted file mode 100644 index 409a96099..000000000 --- a/app/portainer/views/schedules/schedulesController.js +++ /dev/null @@ -1,53 +0,0 @@ -angular.module('portainer.app').controller('SchedulesController', [ - '$scope', - '$state', - 'Notifications', - 'ModalService', - 'ScheduleService', - function ($scope, $state, Notifications, ModalService, ScheduleService) { - $scope.removeAction = removeAction; - - function removeAction(selectedItems) { - ModalService.confirmDeletion('Do you want to remove the selected schedule(s) ?', function onConfirm(confirmed) { - if (!confirmed) { - return; - } - deleteSelectedSchedules(selectedItems); - }); - } - - function deleteSelectedSchedules(schedules) { - var actionCount = schedules.length; - angular.forEach(schedules, function (schedule) { - ScheduleService.deleteSchedule(schedule.Id) - .then(function success() { - Notifications.success('Schedule successfully removed', schedule.Name); - var index = $scope.schedules.indexOf(schedule); - $scope.schedules.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove schedule ' + schedule.Name); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } - }); - }); - } - - function initView() { - ScheduleService.schedules() - .then(function success(data) { - $scope.schedules = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve schedules'); - $scope.schedules = []; - }); - } - - initView(); - }, -]); diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index d6cb80db0..07ab23533 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -101,10 +101,7 @@
- - @@ -100,6 +94,9 @@ + From af6bea5acc133a11825ce5a82d94f168abc825b1 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 6 Jul 2020 11:21:03 +1200 Subject: [PATCH 042/195] feat(kubernetes): introduce kubernetes support (#3987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(kubernetes): fix duplicate published mode * feat(kubernetes): group port mappings by applications * feat(kubernetes): updated UX * feat(kubernetes): updated UX * feat(kubernetes): new applications list view * fix(kubernetes): applications - expand ports on row click * refactor(kubernetes): applications - replace old view with new * fix(kubernetes): disable access management for default resource pool * feat(kubernetes): app creation - limit stacks suggestion to selected resource pool * feat(kubernetes): do not allow access management on system resource pools * refactor(kubernetes): refactor services * create view node detail * compute node status * compute resource reservations * resource reservation progress bar * create applications node datatable * fix(kubernetes): fix invalid method name * feat(kubernetes): minor UI changes * feat(kubernetes): update application inspect UI * feat(kubernetes): add the ability to copy load balancer IP * fix(kubernetes): minor fixes on applications view * feat(kubernetes): set usage level info on progress bars * fix(kubernetes): fix an issue with duplicate pagination controls * fix(kubernetes): fix an issue with unexpandable items * refacto(kubernetes): clean status and resource computation * fix(kubernetes): remove a bad line * feat(kubernetes): update application detail view * feat(kubernetes): change few things on view * refacto(kubernetes): Corrections relative to PR #13 * refacto(kubernetes): remove old functions * feat(kubernetes): add application pod logs * fix(kubernetes): PR #13 * feat(kubernetes): Enable quotas by default * feat(kubernetes): allow non admin to have access to ressource pool list/detail view * feat(kubernetes): UI changes * fix(kubernetes): fix resource reservation computation in node view * fix(kubernetes): pods are correctly filter by app name * fix(kubernetes): nodeapplicationsdatatable is correctly reorder by cpu and memory * fix(kubernetes): nodeapplications datatable is correctly reorder on reload * feat(kubernetes): update podService * refacto(kubernetes): rename nodeInspect as node * refaceto(kubernetes): use colspan 6 instead of colspan 3 * refacto(kubernetes): use genericdatatablecontroller and make isadmin a binding * refacto(kubernetes): remove not needed lines * refacto(kubernetes) extract usageLevelInfo as html filter * refacto(kubernetes): no line break for params * refacto(kubernetes): change on node converter and filters * refacto(kubernetes): remove bad indentations * feat(kubernetes): add plain text informations about resources limits for non admibn user * refacto(kubernetes): ES6 format * refacto(kubernetes): format * refacto(kubernetes): format * refacto(kubernetes): add refresh callback for nodeapplicationsdatatable * refacto(kubernetes): change if else structure * refactor(kubernetes): files naming and format * fix(kubernetes): remove checkbox and actions on resourcespools view for non admin * feat(kubernetes): minor UI update * fix(kubernetes): bind this on getPodsApplications to allow it to access $async * fix(kubernetes): bind this on getEvents to allow it to access $async * fix(kubernetes): format * feat(kubernetes): minor UI update * feat(kubernetes): add support for container console * fix(kubernetes): fix a merge issue * feat(kubernetes): update container console UI * fix(api): fix typo * feat(api): proxy pod websocket to agent * fix(api): fix websocket pod proxy * refactor(kubernetes): uniformize k8s merge comments * refactor(kubernetes): update consoleController * feat(kubernetes): prevent the removal of the default resource pool (#38) * feat(kubernetes): show all applications running inside the resource pool (#35) * add new datatable * feat(kubernetes): add resource pool applications datatable to resource pool detail view * refacto(kubernetes): factorise computeResourceReservation * fix(kubernetes): colspan 6 to colspan 5 * fix(kubernetes): rename resourceReservationHelper into kubernetesResourceReservationHelper * fix(kubernetes): add await to avoid double diggest cycles * feat(kubernetes): add link to application name * fix(kubernetes): change kubernetes-resource-pool-applications-datatable table key * fix(kubernetes): change wording * feat(kubernetes): add proper support for persisted folders (#36) * feat(kubernetes): persistent volume mockups * feat(kubernetes): persistent volume mockups * feat(kubernetes): update persisted folders mockups * feat(kubernetes): endpoint configure storage access policies * fix(kubernetes): restrict advanced deployment to admin * refactor(kubernetes): storageclass service / rest / model * refactor(kubernetes): params/payload/converter pattern for deployments and daemonsets * feat(kubernetes): statefulset management for applications * fix(kubernets): associate application and pods * feat(kubernetes): statefulset support for applications * refactor(kubernetes): rebase on pportainer/k8s * fix(kubernetes): app create - invalid targetPort on loadbalancer * fix(kubernetes): internal services showed as loadbalancer * fix(kubernetes): service ports creation / parsing * fix(kubernetes): remove ports on headless services + ensure nodePort is used only for Cluster publishing * fix(kubernetes): delete headless service on statefulset delete * fix(kubernetes): statefulset replicas count display * refactor(kubernetes): rebase on pportainer/k8s * refactor(kubernetes): cleanup Co-authored-by: Anthony Lapenna * fix(kubernetes): remove mockup routes * feat(kubernetes): only display applications running on node/in resource pool when there are any * feat(kubernetes): review resource reservations and leverage requests instead of limits (#40) * fix(kubernetes): filter resource reservation by app in node view (#48) * refactor(kubernetes): remove review comment * chore(version): bump version number * refactor(kubernetes): remove unused stacks view and components * feat(kubernetes): update CPU slider step to 0.1 for resource pools (#60) * feat(kubernetes): round up application CPU values (#61) * feat(kubernetes): add information about application resource reservat… (#62) * feat(kubernetes): add information about application resource reservations * feat(kubernetes): apply kubernetesApplicationCPUValue to application CPU reservation * refactor(kubernetes): services layer with models/converter/payloads (#64) * refactor(kubernetes): services layer with models/converter/payloads * refactor(kubernetes): file rename and comment update * style(kubernetes): replace strings double quotes with simple quotes Co-authored-by: Anthony Lapenna * fix(kubernetes): filter application by node in node detail view (#69) * fix(kubernetes): filter applications by node * fix(kubernetes): remove js error * refactor(kubernetes): delete resource quota deletion process when deleting a resource pool (#68) * feat(kubernetes): enforce valid resource reservations and clarify its… (#70) * feat(kubernetes): enforce valid resource reservations and clarify its usage * feat(kubernetes): update instance count input behavior * feat(kubernetes): resource pools labels (#71) * feat(kubernetes): resource pools labels * fix(kubernetes): RP/RQ/LR owner label * feat(kubernetes): confirmation popup on RP delete (#76) * feat(kubernetes): application labels (#72) * feat(kubernetes): application labels * feat(kubernetes): display application owner in details when available * style(kubernetes): revert StackName column labels * fix(kubernetes): default displayed StackName * feat(kubernetes): remove RQ query across cluster (#73) * refactor(kubernetes): routes as components (#75) * refactor(kubernetes): routes as components * refactor(kubernetes): use component lifecycle hook * refactor(kubernetes): files naming consistency * fix(kubernetes): fix invalid component name for cluster view Co-authored-by: Anthony Lapenna * feat(kubernetes): update portaineruser cluster role policy rules (#78) * refactor(kubernetes): remove unused helper * fix(kubernetes): fix invalid reload link in cluster view * feat(kubernetes): add cluster resource reservation (#77) * feat(kubernetes): add cluster resource reservation * fix(kubernetes): filter resource reservation with applications * fix(kubernetes): fix indent * refacto(kubernetes): extract megabytes value calc as resourceReservationHelper method * fix(kubernetes): remove unused import * refacto(kubernetes): add resourcereservation model * fix(kubernetes): add parenthesis on arrow functions parameters * refacto(kubernetes): getpods in applicationService getAll * fix(kubernetes): let to const * fix(kubernetes): remove unused podservice * fix(kubernetes): fix computeResourceReservation * fix(kubernetes): app.pods to app.Pods everywhere and camelcase of this.ResourceReservation * feat(kubernetes): configurations list view (#74) * feat(kubernetes): add configuration list view * feat(kubernetes): add configurations datatable * feat(kubernetes): add item selection * feat(kubernetes): allow to remove configuration * feat(kubernetes): allow non admin user to see configurations * fix(kubernetes): configurations view as component * feat(kubernetes): remove stack property for secret and configurations * fix(kubernetes): update import * fix(kubernetes): remove secret delete payload * fix(kubernetes): rename configuration model * fix(kubernetes): remove configmap delete payload * fix(Kubernetes): fix configuration getAsync * fix(kubernetes): extract params as variables * refacto(kubernetes): extract configurations used lines as helper * fix(kubernetes): add verification of _.find return value * fix(kubernetes): fix kubernetes configurations datatable callback * refacto(Kubernetes): extract find before if * fix(kubernetes): replace this by KubernetesConfigurationHelper in static method * fix(Kubernetes): fix getASync Co-authored-by: Anthony Lapenna * review(kubernetes): todo comments (#80) * feat(kubernetes): minor UI update * feat(kubernetes): round max cpu value in application creation * feat(kubernetes): minor UI update * fix(kubernetes): no-wrap resource reservation bar text (#89) * docs(kubernetes): add review for formValues to resource conversion (#91) * feat(kubernetes): configuration creation view (#82) * feat(kubernetes): create configuration view * feat(kubernetes): add advanced mode and create entry from file * fix(kubernetes): fix validation issues * fix(kubernetes): fix wording * fix(kubernetes): replace data by stringdata in secret payloads * fix(kubernetes): rename KubernetesConfigurationEntry to KubernetesConfigurationFormValuesDataEntry * refacto(kubernetes): add isSimple to formValues and change configuration creation pattern * fix(kubernetes): fix some bugs * refacto(kubernetes): renaming * fix(kubernetes): fix few bugs * fix(kubernetes): fix few bugs * review(kubernetes): refactor notices Co-authored-by: xAt0mZ * feat(kubernetes): rename codeclimate file * feat(kubernetes): re-enable codeclimate * feat(project): update codeclimate configuration * feat(project): update codeclimate configuration * feat(project): update codeclimate configuration * feat(kubernetes): minor UI update * feat(project): update codeclimate * feat(project): update codeclimate configuration * feat(project): update codeclimate configuration * feat(kubernetes): configuration details view (#93) * feat(kubernetes): configuration details view * fix(kubernetes): fix wording * fix(kubernetes): fix update button * fix(kubernetes): line indent * refacto(kubernetes): remove conversion * refacto(kubernetes): remove useless line * refacto(kubernetes): remove useless lines * fix(kubernetes): revert error handling * fix(kubernetes): fix wording * fix(kubernetes): revert line deletion * refacto(kubernetes): change data mapping * fix(kubernetes): create before delete * fix(kubernetes): fix duplicate bug * feat(kubernetes): configurations in application creation (#92) * feat(kubernetes): application configuration mockups * feat(kubernetes): update mockup * feat(kubernetes): app create - dynamic view for configurations * feat(kubernetes): app create - configuration support * refactor(kubernetes): more generic configuration conversion function Co-authored-by: Anthony Lapenna * feat(kubernetes): automatically display first entry in configuration creation * feat(kubernetes): minor UI update regarding applications and configurations * feat(kubernetes): update Cluster icon in sidebar * feat(kubernetes): volumes list view (#112) * feat(kubernetes): add a feedback panel on main views (#111) * feat(kubernetes): add a feedback panel on main views * feat(kubernetes): add feedback panel to volumes view * fix(kubernetes): isolated volumes showed as unused even when used (#116) * feat(kubernetes): remove limit range from Portainer (#119) * limits instead of requests (#121) * feat(kubernetes): volume details (#117) * feat(kubernetes): volume details * fix(kubernetes): yaml not showed * feat(kubernetes): expandable stacks list (#122) * feat(kubernetes): expandable stacks list * feat(kubernetes): minor UI update to stacks datatable Co-authored-by: Anthony Lapenna * feat(kubernetes): uibprogress font color (#129) * feat(kubernetes): minor UI update to resource reservation component * feat(kubernetes): automatically select a configuration * refactor(kubernetes): remove comment * feat(kubernetes): minor UI update * feat(kubernetes): add resource links and uniformize view headers (#133) * feat(kubernetes): prevent removal of system configurations (#128) * feat(kubernetes): prevent removal of system configurations * fix(kubernetes): KubernetesNamespaceHelper is not a function * refacto(kubernetes): change prevent removal pattern * fix(kubernetes): remove unused dependencies * fix(kubernetes): fix configuration used label (#123) * fix(kubernetes): fix used configurations * fix(kubernetes): remove console log * feat(kubernetes): rename configuration types (#127) * refacto(kubernetes): fix wording and use configMap instead of Basic in the code * feat(kubernetes): prevent the removal of system configuration * fix(kubernetes): remove feat on bad branch * fix(kubernetes): rename configuration types * refacto(kubernetes): use a numeric enum and add a filter to display the text type * refacto(kubernetes): fix wording and use configMap instead of Basic in the code * feat(kubernetes): prevent the removal of system configuration * fix(kubernetes): remove feat on bad branch * fix(kubernetes): rename configuration types * refacto(kubernetes): use a numeric enum and add a filter to display the text type * fix(kubernetes): rename file and not use default in switch case * feat(kubernetes): update advanced deployment UI/UX (#130) * feat(kubernetes): update advanced deployment UI/UX * feat(kubernetes): review HTML tags indentation * feat(kubernetes): applications stacks delete (#135) * fix(kubernetes): multinode resources reservations (#118) * fix(kubernetes): filter pods by node * fix(kubernetes): fix applications by node filter * fix(kubernetes): filter pods by node * Update app/kubernetes/views/cluster/node/nodeController.js Co-authored-by: Anthony Lapenna * feat(kubernetes): limit usage of pod console view (#136) * feat(kubernetes): add yaml and events to configuration details (#126) * feat(kubernetes): add yaml and events to configuration details * fix(kubernetes): fix errors on secret details view * fix(kubernetes): display only events related to configuration * fix(kubernetes): fix applications by node filter * fix(kubernetes): revert commit on bad branch * refacto(kubernetes): refacto configmap get yaml function * refacto(kubernetes): add yaml into converter * feat(kubernetes): improve application details (#141) * refactor(kubernetes): remove applications retrieval from volume service * feat(kubernetes): improve application details view * feat(kubernetes): update kompose binary version (#143) * feat(kubernetes): update kubectl version (#144) * refactor(kubernetes): rename portainer system namespace (#145) * feat(kubernetes): add a loading view indicator (#140) * feat(kubernetes): add an example of view loading indicator * refactor(css): remove comment * feat(kubernetes): updated loading pattern * feat(kubernetes): add loading indicator for resource pool views * feat(kubernetes): add loading indicator for deploy view * feat(kubernetes): add loading view indicator to dashboard * feat(kubernetes): add loading view indicator to configure view * feat(kubernetes): add loading indicator to configuration views * feat(kubernetes): add loading indicator to cluster views * feat(kubernetes): rebase on k8s branch * feat(kubernetes): update icon size * refactor(kubernetes): update indentation and tag format * feat(kubernetes): backend role validation for stack deployment (#147) * feat(kubernetes): show applications when volume is used * feat(kubernetes): set empty value when node is not set * feat(kubernetes): update configuration UI/UX * feat(kubernetes): update configuration UX * fix(kubernetes): Invalid value for a configuration (#139) * fix(kubernetes): Invalid value for a configuration * fix(kubernetes): remove auto JSON convertion for configMap ; apply it for RPool Accesses only * refactor(kubernetes): remove unneeded line * fix(kubernetes): remove default JSON parsing on configMap API retrieval Co-authored-by: xAt0mZ * feat(kubernetes): applications table in configuration details (#154) * feat(kubernetes): Add the ability to filter system resources (#142) * feat(kubernetes): hide system configurations * feat(kubernetes): Add the ability to filter system resources * feat(kubernetes): add the ability to hide system resources on volumes * fix(kubernetes): fix few issue in volumesDatatableController * fix(kubernetes): fix applications / ports / stacks labels * feat(kubernetes): add volumes and configurations to dashboard (#152) * feat(kubernetes): event warning indicator (#150) * feat(kubernetes): event warning indicator for applications * refactor(kubernetes): refactor events indicator logic * feat(kubernetes): add event warning indicator to all resources * feat(kubernetes): fix missing YAML panel for node (#157) * feat(kubernetes): revised application details view (#159) * feat(kubernetes): revised application details view * refactor(kubernetes): remove comment * feat(kubernetes): rebase on k8s * refactor(kubernetes): remove extra line * feat(kubernetes): update kubernetes beta feedback panel locations (#161) * feat(kubernetes): stack logs (#160) * feat(kubernetes): stack logs * fix(kubernetes): ignore starting pods * fix(kubernetes): colspan on expandable stack applications table * feat(kubernetes): add an information message about system resources (#163) * fix(kubernetes): fix empty panel being display in cluster view (#165) * fix(kubernetes): Invalid CPU unit for node (#156) * fix(kubernetes): Invalid CPU unit for node * fix(kubernetes): Invalid CPU unit for node * refacto(kubernetes): extract parseCPU function in helper * refacto(kubernetes): rewrite parseCPU function * feat(kubernetes): add the kube-node-lease namespace to system namespaces (#177) * feat(kubernetes): tag system applications on node details view (#175) * feat(kubernetes): tag system applications on node details view * fix(kubernetes): remove system resources filter * feat(kubernetes): review UI/UX around volume size unit (#178) * feat(kubernetes): updates after review (#174) * feat(kubernetes): update access user message * feat(kubernetes): relocate resource pool to a specific form section * feat(kubernetes): review responsiveness of port mappings * feat(kubernetes): clarify table settings * feat(kubernetes): add resource reservation summary message * feat(kubernetes): review wording (#182) * feat(kubernetes): application stack edit (#179) * feat(kubernetes): update UI -- update action missing * feat(kubernetes): application stack update * feat(kubernetes): change services stacks * feat(kubernetes): hide default-tokens + prevent remove (#183) * feat(kubernetes): hide default-tokens + prevent remove * feat(kubernetes): do not display unused label for system configurations * fix(kubernetes): minor fix around showing system configurations Co-authored-by: Anthony Lapenna * feat(kubernetes): rebase on k8s branch (#180) * fix(kubernetes): prevent the display of system resources in dashboard (#186) * fix(kubernetes): prevent the display of system resources in dashboard * fix(kubernetes): prevent the display of frontend filtered resource pools * feat(kubernetes): support downward API for env vars in application details (#181) * feat(kubernetes): support downward API for env vars in application details * refactor(kubernetes): remove comment * feat(kubernetes): minor UI update * feat(kubernetes): remove Docker features (#189) * chore(version): bump version number (#187) * chore(version): bump version number * feat(kubernetes): disable update notice * feat(kubernetes): minor UI update * feat(kubernetes): minor UI update * feat(kubernetes): form validation (#170) * feat(kubernetes): add published node port value check * feat(kubernetes): add a dns compliant validation * fix(kubernetes): fix port range validation * feat(kubernetes): lot of form validation * feat(kubernetes): add lot of form validation * feat(kubernetes): persisted folders size validation * feat(kubernetes): persisted folder path should be unique * fix(kubernetes): fix createResourcePool button * fix(kubernetes): change few things * fix(kubernetes): fix slider memory * fix(kubernetes): fix duplicates on dynamic field list * fix(kubernetes): remove bad validation on keys * feat(kubernetes): minor UI enhancements and validation updates * feat(kubernetes): minor UI update * fix(kubernetes): revert on slider fix * review(kubernetes): add future changes to do * fix(kubernetes): add form validation on create application memory slider Co-authored-by: Anthony Lapenna Co-authored-by: xAt0mZ * feat(kubernetes): remove Docker related content * feat(kubernetes): update build system to remove docker binary install * fix(kubernetes): fix an issue with missing user settings * feat(kubernetes): created column for apps and resource pools (#184) * feat(kubernetes): created column for apps and resource pools * feat(kubernetes): configurations and volumes owner * feat(kubernetes): rename datatables columns * fix(kubernetes): auto detect statefulset headless service name (#196) * fix(applications): display used configurations (#198) * feat(kubernetes): app details - display data access policy (#199) * feat(kubernetes): app details - display data access policy * feat(kubernetes): tooltip on data access info * feat(kubernetes): move DAP tooltip to end of line * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna * fix(kubernetes): fix an issue when updating the local endpoint (#204) * fix(kubernetes): add unique key to configuration overriden key path field (#207) * feat(kubernetes): tag applications as external (#221) * feat(kubernetes): tag applications as external first approach * feat(kubernetes): tag applications as external * feat(kubernetes): Use ibytes as the default volume size unit sent to the Kubernetes API (#222) * feat(kubernetes): Use ibytes as the default volume size unit sent to the Kubernetes API * fix(kubernetes): only display b units in list and details views * feat(kubernetes): add note to application details (#212) * feat(kubernetes): add note to application details * fix(kubernetes): remove eslintcache * feat(kubernetes): update application note UI * feat(kubernetes): add an update button to the note form when a note is already associated to an app * feat(kubernetes): fix with UI changes * fix(kubernetes): change few things * fix(kubernetes): remove duplicate button * fix(kubernetes): just use a ternary Co-authored-by: Anthony Lapenna * feat(kubernetes): fix data persistence display for isolated DAP (#223) * feat(kubernetes): add a quick action to copy application name to clipboard (#225) * feat(kubernetes): revert useless converter changes (#228) * feat(kubernetes): edit application view (#200) * feat(kubernetes): application to formValues conversion * feat(kubernetes): extract applicationFormValues conversion as converter function * feat(kubernetes): draft app patch * feat(kubernetes): patch on all apps services + service service + pvc service * feat(kubernetes): move name to labels and use UUID as kubernetes Name + patch recreate if necessary * feat(kubernetes): move user app name to label and use UUID for Kubernetes Name field * feat(kubernetes): kubernetes service patch mechanism * feat(kubernetes): application edit * feat(kubernetes): remove stack edit on app details * feat(kubernetes): revert app name saving in label - now reuse kubernetes Name field * feat(kubernetes): remove the ability to edit the DAP * feat(kubernetes): cancel button on edit view * feat(kubernetes): remove ability to add/remove persisted folders for SFS edition * feat(kubernetes): minor UI update and action changes * feat(kubernetes): minor UI update * feat(kubernetes): remove ability to edit app volumes sizes + disable update button if no changes are made + codeclimate * fix(kubernetes): resource reservation sliders in app edit * fix(kubernetes): patch returned with 422 when trying to create nested objects * fix(kubernetes): changing app deployment type wasn't working (delete failure) * style(kubernetes): codeclimate * fix(kubernetes): app edit - limits sliders max value * feat(kubernetes): remove prefix on service name as we enforce DNS compliant app names * fix(kubernetes): edit app formvalues replica based on target replica count and not total pods count * fix(kubernetes): disable update for RWO on multi replica + delete service when changing app type * fix(kubernetes): app details running / target pods display * feat(kubernetes): add partial patch for app details view Co-authored-by: Anthony Lapenna * feat(kubernetes): disable edit capability for external and system apps (#233) * feat(kubernetes): minor UI update * fix(kubernetes): edit application issues (#235) * feat(kubernetes): disable edition of load balancer if it's in pending state * fix(kubernetes): now able to change from LB to other publishing types * feat(kuberntes): modal on edit click to inform on potential service interruption * feat(kubernetes): hide note when empty + add capability to collapse it * fix(kubernetes): UI/API desync + app update button enabled in some cases where it shouldn't be * fix(kubernetes): all apps are now using rolling updates with specific conditions * style(kubernetes): code indent * fix(kubernetes): disable sync process on endpoint init as current endpoint is not saved in client state * fix(kubernetes): sliders refresh on app create + app details bad display for sfs running pods * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna * feat(kubernetes): bump up kubectl version to v1.18.0 * feat(kubernetes): when refreshing a view, remember currently opened tabs (#226) * feat(kubernetes): When refreshing a view, remember currently opened tabs * fix(kubernetes): only persist the current tab inside the actual view * fix(kubernetes): not working with refresh in view header * fix(kubernetes): skip error on 404 headless service retrieval if missconfigured in sfs (#242) * refactor(kubernetes): use KubernetesResourcePoolService instead of KubernetesNamespaceService (#243) * fix(kubernetes): create service before app to enforce port availability (#239) * fix(kubernetes): external flag on application ports mappings datatable (#245) * refactor(kubernetes): remove unused KubernetesResourcePoolHelper (#246) * refactor(kubernetes): make all *service.getAllAsync functions consistent (#249) * feat(kubernetes): Tag external applications in the application table of the resource pool details view (#251) * feat(kubernetes): add ability to redeploy application (#240) * feat(kubernetes): add ability to redeploy application * feat(kubernetes): allow redeploy for external apps * Revert "feat(kubernetes): allow redeploy for external apps" This reverts commit 093375a7e93c1a07b845ebca1618da034a97fbcd. * refactor(kubernetes): use KubernetesPodService instead of REST KubernetesPods (#247) * feat(kubernetes): prevent configuration properties edition (#248) * feat(kubernetes): prevent configuration properties edition * feat(kubernetes): Relocate the Data/Actions to a separate panel * feat(kubernetes): remove unused functions * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna * refactor(kubernetes): Simplify the FileReader usage (#254) * refactor(kubernetes): simplify FileReader usage * refactor(kubernetes): Simplify FileReader usage * refactor(kubernetes): rename e as event for readability * feat(kubernetes): Tag system Configs in the Config details view (#257) * refactor(kubernetes): Refactor the isFormValid function of multiple controllers (#253) * refactor(kubernetes): refactor isFormValid functions in configurations * refactor(kubernetes): refactor isformValid functions in create application * refactor(kubernetes): remove duplicate lines * refactor(kubernetes): remove commented line * feat(kubernetes): Tag external volumes and configs (#250) * feat(kubernetes): Tag external volumes and configs * feat(kubernetes): remove .eslintcache * feat(kubernetes): change few things * feat(kubernetes): don't tag system configuration as external * feat(kubernetes): minor UI update * feat(kubernetes): extract inline css and clean all tags Co-authored-by: Anthony Lapenna * fix(kubernetes): daemon set edit (#258) * fix(kubernetes): persistent folder unit parsing * fix(kubernetes): edit daemonset on RWO storage * fix(kubernetes): external SFS had unlinked volumes (#264) * feat(kubernetes): prevent to override two different configs on the same filesystem path (#259) * feat(kubernetes): prevent to override two different configs on the same filesystem path * feat(kubernetes): The validation should only be triggered across Configurations. * feat(kubernetes): fix validations issues * feat(kubernetes): fix form validation * feat(kubernetes): fix few things * refactor(kubernetes): Review the code mirror component update for configurations (#260) * refactor(kubernetes): extract duplicate configuration code into a component * refactor(kubernetes): fix form validation issues * refactor(kubernetes): fix missing value * refactor(kubernetes): remove useless await * feat(kubernetes): Update the shared access policy configuration for Storage (#263) * feat(kubernetes): Update the shared access policy configuration for Storage * Update app/kubernetes/models/storage-class/models.js * feat(kubernetes): remove ROX references and checks Co-authored-by: Anthony Lapenna Co-authored-by: xAt0mZ * feat(kubernetes): provide the remove/restore UX for environment variables when editing an application (#261) * feat(kubernetes): Provide the remove/restore UX for environment variables when editing an application * feat(kubernetes): fix ui issue * feat(kubernetes): change few things * fix(kubernetes): Invalid display for exposed ports in accessing the application section (#267) * feat(kubernetes): application rollback (#269) * feat(kubernetes): retrieve all versions of a deployment * feat(kubernetes): application history for all types * feat(kubernetes): deployment rollback * feat(kubernetes): daemonset / statefulset rollback * feat(kubernetes): remove the revision selector and rollback on previous version everytime * feat(kubernetes): minor UI changes Co-authored-by: Anthony Lapenna * feat(kubernetes): reservations should be computed based on requests instead of limits (#268) * feat(kubernetes): Reservations should be computed based on requests instead of limits * feat(kubernetes): use requests instead of limits in application details * feat(kubernetes): removes unused limits * feat(kubernetes): Not so useless * feat(kubernetes): use service selectors to bind apps and services (#270) * feat(kubernetes): use service selectors to bind apps and services * Update app/kubernetes/services/statefulSetService.js * style(kubernetes): remove comment block Co-authored-by: Anthony Lapenna * chore(version): bump version number * feat(kubernetes): update feedback panel text * chore(app): add prettier to k8s * style(app): apply prettier to k8s codebase * fix(kubernetes): Cannot read property 'port' of undefined (#272) * fix(kubernetes): Cannot read property 'port' of undefined * fix(kubernetes): concat app ports outside publishedports loop * fix(application): fix broken display of the persistence layer (#274) * chore(kubernetes): fix conflicts * chore(kubernetes): fix issues related to conflict resolution * refactor(kubernetes): refactor code related to conflict resolution * fix(kubernetes): fix a minor issue with assets import * chore(app): update yarn.lock * fix(application): ports mapping are now correctly detected (#300) * fix(build-system): fix missing docker binary download step * feat(kubernetes): application auto scaling details (#301) * feat(kubernetes): application auto scaling details * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna * feat(kubernetes): Introduce a "used by" column in the volume list view (#303) Co-authored-by: xAt0mZ Co-authored-by: Maxime Bajeux Co-authored-by: xAt0mZ --- .codeclimate.yml | 58 +- .github/ISSUE_TEMPLATE/Bug_report.md | 95 +- CONTRIBUTING.md | 26 +- api/chisel/service.go | 10 +- api/cmd/portainer/main.go | 121 +- api/docker/client.go | 4 +- api/docker/snapshot.go | 43 +- api/docker/snapshotter.go | 28 - api/exec/kubernetes_deploy.go | 89 + api/exec/swarm_stack.go | 2 +- api/go.mod | 4 + api/go.sum | 151 ++ api/http/handler/auth/handler.go | 16 +- api/http/handler/auth/logout.go | 21 + .../edgegroups/associated_endpoints.go | 2 +- .../handler/edgegroups/edgegroup_create.go | 2 +- .../handler/edgegroups/edgegroup_update.go | 2 +- api/http/handler/edgejobs/edgejob_create.go | 2 +- api/http/handler/edgejobs/edgejob_update.go | 2 +- api/http/handler/endpointgroups/endpoints.go | 2 +- api/http/handler/endpointproxy/handler.go | 2 + .../handler/endpointproxy/proxy_docker.go | 2 +- .../handler/endpointproxy/proxy_kubernetes.go | 73 + api/http/handler/endpoints/endpoint_create.go | 107 +- .../handler/endpoints/endpoint_snapshot.go | 12 +- .../handler/endpoints/endpoint_snapshots.go | 14 +- .../endpoints/endpoint_status_inspect.go | 2 +- api/http/handler/endpoints/endpoint_update.go | 7 +- api/http/handler/endpoints/handler.go | 6 +- api/http/handler/handler.go | 2 + api/http/handler/settings/handler.go | 6 +- .../handler/stacks/create_kubernetes_stack.go | 58 + api/http/handler/stacks/handler.go | 3 +- api/http/handler/stacks/stack_create.go | 6 + api/http/handler/tags/tag_delete.go | 2 +- api/http/handler/websocket/attach.go | 2 +- api/http/handler/websocket/exec.go | 2 +- api/http/handler/websocket/handler.go | 16 +- api/http/handler/websocket/hijack.go | 7 +- api/http/handler/websocket/pod.go | 116 + api/http/handler/websocket/proxy.go | 4 +- api/http/handler/websocket/stream.go | 18 +- api/http/proxy/factory/docker.go | 2 +- api/http/proxy/factory/docker/transport.go | 2 +- api/http/proxy/factory/factory.go | 30 +- api/http/proxy/factory/kubernetes.go | 109 + api/http/proxy/factory/kubernetes/token.go | 79 + .../proxy/factory/kubernetes/token_cache.go | 69 + .../proxy/factory/kubernetes/transport.go | 156 ++ api/http/proxy/manager.go | 12 +- api/http/security/bouncer.go | 2 +- api/http/server.go | 75 +- api/internal/edge/edgegroup.go | 2 +- api/internal/snapshot/snapshot.go | 72 +- api/kubernetes.go | 11 + api/kubernetes/cli/access.go | 86 + api/kubernetes/cli/client.go | 145 + api/kubernetes/cli/exec.go | 57 + api/kubernetes/cli/naming.go | 26 + api/kubernetes/cli/role.go | 33 + api/kubernetes/cli/secret.go | 74 + api/kubernetes/cli/service_account.go | 182 ++ api/kubernetes/snapshot.go | 83 + api/libcompose/compose_stack.go | 2 +- api/portainer.go | 211 +- app/__module.js | 1 + app/assets/css/app.css | 54 +- app/assets/images/kubernetes_endpoint.png | Bin 0 -> 4079 bytes app/constants.js | 4 +- .../containerNetworksDatatableController.js | 137 +- .../containersDatatable.html | 3 + .../volumesNFSForm/volumesnfsForm.html | 9 +- app/docker/services/containerService.js | 2 +- .../containers/inspect/containerinspect.html | 2 +- .../edge-groups-selector.html | 10 +- .../edge-groups-selector.js | 4 +- app/edge/components/group-form/groupForm.html | 6 +- .../models/registryRepository.js | 1 + .../services/registryV2Service.js | 3 +- .../edit/registryRepositoryController.js | 2 +- app/index.html | 2 +- app/kubernetes/__module.js | 261 ++ .../pods-datatable/podsDatatable.html | 157 ++ .../pods-datatable/podsDatatable.js | 12 + .../applicationsDatatable.html | 193 ++ .../applicationsDatatable.js | 15 + .../applicationsDatatableController.js | 77 + .../applicationsPortsDatatable.html | 192 ++ .../applicationsPortsDatatable.js | 13 + .../applicationsPortsDatatableController.js | 103 + .../applicationsStacksDatatable.html | 183 ++ .../applicationsStacksDatatable.js | 14 + .../applicationsStacksDatatableController.js | 110 + .../configurationsDatatable.html | 162 ++ .../configurationsDatatable.js | 13 + .../configurationsDatatableController.js | 83 + .../events-datatable/eventsDatatable.html | 134 + .../events-datatable/eventsDatatable.js | 14 + .../integratedApplicationsDatatable.html | 127 + .../integratedApplicationsDatatable.js | 13 + .../nodeApplicationsDatatable.html | 156 ++ .../nodeApplicationsDatatable.js | 13 + .../nodeApplicationsDatatableController.js | 54 + .../nodes-datatable/nodesDatatable.html | 164 ++ .../nodes-datatable/nodesDatatable.js | 13 + .../resourcePoolApplicationsDatatable.html | 145 + .../resourcePoolApplicationsDatatable.js | 13 + ...urcePoolApplicationsDatatableController.js | 47 + .../resourcePoolsDatatable.html | 160 ++ .../resourcePoolsDatatable.js | 14 + .../resourcePoolsDatatableController.js | 77 + .../volumes-datatable/volumesDatatable.html | 192 ++ .../volumes-datatable/volumesDatatable.js | 14 + .../volumesDatatableController.js | 91 + .../feedback-panel/feedbackPanel.html | 9 + .../feedback-panel/feedbackPanel.js | 3 + .../kubernetesConfigurationData.html | 97 + .../kubernetesConfigurationData.js | 8 + .../kubernetesConfigurationDataController.js | 68 + .../kubernetesSidebarContent.html | 18 + .../kubernetesSidebarContent.js | 6 + .../resourceReservation.html | 32 + .../resourceReservation.js | 11 + .../resourceReservationController.js | 23 + .../components/view-header/viewHeader.html | 10 + .../components/view-header/viewHeader.js | 9 + .../components/view-loading/viewLoading.html | 8 + .../components/view-loading/viewLoading.js | 6 + .../yaml-inspector/yamlInspector.html | 7 + .../yaml-inspector/yamlInspector.js | 8 + .../yaml-inspector/yamlInspectorController.js | 17 + app/kubernetes/converters/application.js | 302 +++ app/kubernetes/converters/configMap.js | 81 + app/kubernetes/converters/configuration.js | 31 + app/kubernetes/converters/daemonSet.js | 79 + app/kubernetes/converters/deployment.js | 80 + app/kubernetes/converters/event.js | 15 + app/kubernetes/converters/namespace.js | 29 + app/kubernetes/converters/node.js | 65 + .../converters/persistentVolumeClaim.js | 68 + app/kubernetes/converters/pod.js | 32 + app/kubernetes/converters/resourcePool.js | 12 + app/kubernetes/converters/resourceQuota.js | 87 + app/kubernetes/converters/secret.js | 58 + app/kubernetes/converters/service.js | 88 + app/kubernetes/converters/statefulSet.js | 84 + app/kubernetes/converters/storageClass.js | 14 + app/kubernetes/converters/volume.js | 12 + app/kubernetes/filters/applicationFilters.js | 72 + .../filters/configurationFilters.js | 13 + app/kubernetes/filters/eventFilters.js | 16 + app/kubernetes/filters/filters.js | 11 + app/kubernetes/filters/nodeFilters.js | 35 + app/kubernetes/filters/podFilters.js | 84 + app/kubernetes/helpers/application/index.js | 273 ++ .../helpers/application/rollback.js | 76 + app/kubernetes/helpers/commonHelper.js | 12 + app/kubernetes/helpers/configMapHelper.js | 36 + app/kubernetes/helpers/configurationHelper.js | 40 + app/kubernetes/helpers/eventHelper.js | 10 + .../helpers/formValidationHelper.js | 15 + app/kubernetes/helpers/history/daemonset.js | 27 + app/kubernetes/helpers/history/deployment.js | 56 + app/kubernetes/helpers/history/index.js | 50 + app/kubernetes/helpers/history/statefulset.js | 27 + app/kubernetes/helpers/namespaceHelper.js | 15 + app/kubernetes/helpers/resourceQuotaHelper.js | 9 + .../helpers/resourceReservationHelper.js | 44 + app/kubernetes/helpers/serviceHelper.js | 13 + app/kubernetes/helpers/stackHelper.js | 26 + app/kubernetes/helpers/volumeHelper.js | 37 + .../horizontal-pod-auto-scaler/converter.js | 23 + .../horizontal-pod-auto-scaler/helper.js | 26 + .../horizontal-pod-auto-scaler/models.js | 23 + .../horizontal-pod-auto-scaler/rest.js | 50 + .../horizontal-pod-auto-scaler/service.js | 135 + .../models/application/formValues.js | 118 + app/kubernetes/models/application/models.js | 114 + app/kubernetes/models/application/payloads.js | 142 + app/kubernetes/models/common/params.js | 11 + app/kubernetes/models/common/payloads.js | 15 + app/kubernetes/models/config-map/models.js | 21 + app/kubernetes/models/config-map/payloads.js | 27 + .../models/configuration/formvalues.js | 35 + app/kubernetes/models/configuration/models.js | 27 + app/kubernetes/models/daemon-set/models.js | 24 + app/kubernetes/models/daemon-set/payloads.js | 50 + app/kubernetes/models/deploy.js | 4 + app/kubernetes/models/deployment/models.js | 25 + app/kubernetes/models/deployment/payloads.js | 51 + app/kubernetes/models/event/models.js | 16 + app/kubernetes/models/history/models.js | 32 + app/kubernetes/models/namespace/models.js | 18 + app/kubernetes/models/namespace/payloads.js | 14 + app/kubernetes/models/node/models.js | 40 + app/kubernetes/models/pod/models.js | 21 + app/kubernetes/models/port/models.js | 33 + app/kubernetes/models/resource-pool/models.js | 20 + .../models/resource-quota/models.js | 34 + .../models/resource-quota/payloads.js | 43 + .../models/resource-reservation/models.js | 13 + app/kubernetes/models/secret/models.js | 18 + app/kubernetes/models/secret/payloads.js | 31 + app/kubernetes/models/service/models.js | 45 + app/kubernetes/models/service/payloads.js | 22 + app/kubernetes/models/stack/models.js | 14 + app/kubernetes/models/stateful-set/models.js | 27 + .../models/stateful-set/payloads.js | 52 + app/kubernetes/models/storage-class/models.js | 33 + app/kubernetes/models/volume/models.js | 39 + app/kubernetes/models/volume/payloads.js | 23 + app/kubernetes/rest/configMap.js | 38 + app/kubernetes/rest/controllerRevision.js | 44 + app/kubernetes/rest/daemonSet.js | 50 + app/kubernetes/rest/deployment.js | 50 + app/kubernetes/rest/event.js | 38 + app/kubernetes/rest/health.js | 17 + app/kubernetes/rest/namespace.js | 42 + app/kubernetes/rest/node.js | 37 + app/kubernetes/rest/persistentVolumeClaim.js | 44 + app/kubernetes/rest/pod.js | 44 + app/kubernetes/rest/replicaSet.js | 44 + app/kubernetes/rest/resourceQuota.js | 38 + app/kubernetes/rest/response/transform.js | 7 + app/kubernetes/rest/secret.js | 38 + app/kubernetes/rest/service.js | 44 + app/kubernetes/rest/statefulSet.js | 50 + app/kubernetes/rest/storage.js | 37 + app/kubernetes/services/applicationService.js | 343 +++ app/kubernetes/services/configMapService.js | 115 + .../services/configurationService.js | 128 + .../services/controllerRevisionService.js | 31 + app/kubernetes/services/daemonSetService.js | 133 + app/kubernetes/services/deploymentService.js | 133 + app/kubernetes/services/eventService.js | 34 + app/kubernetes/services/healthService.js | 30 + app/kubernetes/services/historyService.js | 54 + app/kubernetes/services/namespaceService.js | 96 + app/kubernetes/services/nodeService.js | 50 + .../services/persistentVolumeClaimService.js | 115 + app/kubernetes/services/podService.js | 75 + app/kubernetes/services/replicaSetService.js | 31 + .../services/resourcePoolService.js | 113 + .../services/resourceQuotaService.js | 109 + app/kubernetes/services/secretService.js | 110 + app/kubernetes/services/serviceService.js | 115 + app/kubernetes/services/stackService.js | 32 + app/kubernetes/services/statefulSetService.js | 144 + app/kubernetes/services/storageService.js | 37 + app/kubernetes/services/volumeService.js | 70 + .../views/applications/applications.html | 60 + .../views/applications/applications.js | 5 + .../applications/applicationsController.js | 139 + .../views/applications/console/console.html | 71 + .../views/applications/console/console.js | 8 + .../applications/console/consoleController.js | 113 + .../create/createApplication.html | 904 ++++++ .../applications/create/createApplication.js | 8 + .../create/createApplicationController.js | 627 +++++ .../views/applications/edit/application.html | 477 ++++ .../views/applications/edit/application.js | 8 + .../edit/applicationController.js | 245 ++ .../views/applications/logs/logs.html | 62 + .../views/applications/logs/logs.js | 8 + .../views/applications/logs/logsController.js | 87 + app/kubernetes/views/cluster/cluster.html | 40 + app/kubernetes/views/cluster/cluster.js | 5 + .../views/cluster/clusterController.js | 88 + app/kubernetes/views/cluster/node/node.html | 107 + app/kubernetes/views/cluster/node/node.js | 8 + .../views/cluster/node/nodeController.js | 137 + .../views/configurations/configurations.html | 19 + .../views/configurations/configurations.js | 5 + .../configurationsController.js | 105 + .../create/createConfiguration.html | 135 + .../create/createConfiguration.js | 5 + .../create/createConfigurationController.js | 93 + .../configurations/edit/configuration.html | 120 + .../configurations/edit/configuration.js | 8 + .../edit/configurationController.js | 236 ++ app/kubernetes/views/configure/configure.html | 122 + .../views/configure/configureController.js | 115 + app/kubernetes/views/dashboard/dashboard.html | 99 + app/kubernetes/views/dashboard/dashboard.js | 5 + .../views/dashboard/dashboardController.js | 88 + app/kubernetes/views/deploy/deploy.html | 118 + app/kubernetes/views/deploy/deploy.js | 5 + .../views/deploy/deployController.js | 99 + .../access/resourcePoolAccess.html | 116 + .../access/resourcePoolAccess.js | 8 + .../access/resourcePoolAccessController.js | 138 + .../create/createResourcePool.html | 163 ++ .../create/createResourcePool.js | 5 + .../create/createResourcePoolController.js | 127 + .../resource-pools/edit/resourcePool.html | 189 ++ .../views/resource-pools/edit/resourcePool.js | 8 + .../edit/resourcePoolController.js | 230 ++ .../views/resource-pools/resourcePools.html | 19 + .../views/resource-pools/resourcePools.js | 5 + .../resource-pools/resourcePoolsController.js | 77 + app/kubernetes/views/stacks/logs/logs.html | 60 + app/kubernetes/views/stacks/logs/logs.js | 8 + .../views/stacks/logs/logsController.js | 122 + app/kubernetes/views/volumes/edit/volume.html | 99 + app/kubernetes/views/volumes/edit/volume.js | 8 + .../views/volumes/edit/volumeController.js | 130 + app/kubernetes/views/volumes/volumes.html | 20 + app/kubernetes/views/volumes/volumes.js | 5 + .../views/volumes/volumesController.js | 82 + app/portainer/__module.js | 36 +- .../access-datatable/accessDatatable.html | 8 +- .../accessDatatableController.js | 2 +- .../porAccessControlPanelController.js | 2 +- .../accessManagement/porAccessManagement.html | 2 - .../porAccessManagementController.js | 35 +- .../datatables/genericDatatableController.js | 1 + .../endpoint-item/endpointItem.html | 37 +- app/portainer/components/header-content.js | 2 +- .../components/slider/sliderController.js | 64 +- app/portainer/components/tooltip.js | 7 +- app/portainer/error.js | 6 + app/portainer/filters/filters.js | 8 +- app/portainer/models/endpoint/formValues.js | 41 + app/portainer/models/endpoint/models.js | 28 + app/portainer/rest/auth.js | 8 +- app/portainer/services/allSettled.js | 34 + app/portainer/services/api/accessService.js | 37 +- app/portainer/services/api/endpointService.js | 24 +- app/portainer/services/api/registryService.js | 2 +- app/portainer/services/api/stackService.js | 24 +- app/portainer/services/api/tagService.js | 12 +- app/portainer/services/authentication.js | 10 +- app/portainer/services/localStorage.js | 7 + app/portainer/services/modalService.js | 15 + app/portainer/services/notifications.js | 6 +- app/portainer/services/stateManager.js | 6 + .../create/createEndpointController.js | 53 +- .../endpoints/create/createendpoint.html | 63 +- .../views/endpoints/edit/endpoint.html | 42 +- .../endpoints/edit/endpointController.js | 43 +- app/portainer/views/home/home.html | 2 + app/portainer/views/home/homeController.js | 38 +- .../views/init/endpoint/includes/agent.html | 34 + .../views/init/endpoint/includes/azure.html | 63 + .../init/endpoint/includes/localDocker.html | 21 + .../endpoint/includes/localKubernetes.html | 12 + .../views/init/endpoint/includes/remote.html | 120 + .../views/init/endpoint/initEndpoint.html | 401 +-- .../init/endpoint/initEndpointController.js | 304 ++- app/portainer/views/logout/logout.html | 16 + .../views/logout/logoutController.js | 61 + app/portainer/views/sidebar/sidebar.html | 4 +- app/portainer/views/teams/edit/team.html | 4 +- app/vendors.js | 4 +- build/download_kompose_binary.sh | 10 + build/download_kubectl_binary.sh | 10 + gruntfile.js | 69 +- jsconfig.json | 1 + package.json | 9 +- webpack/webpack.common.js | 1 + yarn.lock | 2412 ++++++++--------- 361 files changed, 20794 insertions(+), 2349 deletions(-) delete mode 100644 api/docker/snapshotter.go create mode 100644 api/exec/kubernetes_deploy.go create mode 100644 api/http/handler/auth/logout.go create mode 100644 api/http/handler/endpointproxy/proxy_kubernetes.go create mode 100644 api/http/handler/stacks/create_kubernetes_stack.go create mode 100644 api/http/handler/websocket/pod.go create mode 100644 api/http/proxy/factory/kubernetes.go create mode 100644 api/http/proxy/factory/kubernetes/token.go create mode 100644 api/http/proxy/factory/kubernetes/token_cache.go create mode 100644 api/http/proxy/factory/kubernetes/transport.go create mode 100644 api/kubernetes.go create mode 100644 api/kubernetes/cli/access.go create mode 100644 api/kubernetes/cli/client.go create mode 100644 api/kubernetes/cli/exec.go create mode 100644 api/kubernetes/cli/naming.go create mode 100644 api/kubernetes/cli/role.go create mode 100644 api/kubernetes/cli/secret.go create mode 100644 api/kubernetes/cli/service_account.go create mode 100644 api/kubernetes/snapshot.go create mode 100644 app/assets/images/kubernetes_endpoint.png create mode 100644 app/kubernetes/__module.js create mode 100644 app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html create mode 100644 app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js create mode 100644 app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html create mode 100644 app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js create mode 100644 app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js create mode 100644 app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html create mode 100644 app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.js create mode 100644 app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js create mode 100644 app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html create mode 100644 app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js create mode 100644 app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js create mode 100644 app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.html create mode 100644 app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.js create mode 100644 app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js create mode 100644 app/kubernetes/components/datatables/events-datatable/eventsDatatable.html create mode 100644 app/kubernetes/components/datatables/events-datatable/eventsDatatable.js create mode 100644 app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.html create mode 100644 app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.js create mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html create mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js create mode 100644 app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js create mode 100644 app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html create mode 100644 app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js create mode 100644 app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html create mode 100644 app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.js create mode 100644 app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatableController.js create mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html create mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js create mode 100644 app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js create mode 100644 app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html create mode 100644 app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.js create mode 100644 app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js create mode 100644 app/kubernetes/components/feedback-panel/feedbackPanel.html create mode 100644 app/kubernetes/components/feedback-panel/feedbackPanel.js create mode 100644 app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html create mode 100644 app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.js create mode 100644 app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationDataController.js create mode 100644 app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html create mode 100644 app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js create mode 100644 app/kubernetes/components/resource-reservation/resourceReservation.html create mode 100644 app/kubernetes/components/resource-reservation/resourceReservation.js create mode 100644 app/kubernetes/components/resource-reservation/resourceReservationController.js create mode 100644 app/kubernetes/components/view-header/viewHeader.html create mode 100644 app/kubernetes/components/view-header/viewHeader.js create mode 100644 app/kubernetes/components/view-loading/viewLoading.html create mode 100644 app/kubernetes/components/view-loading/viewLoading.js create mode 100644 app/kubernetes/components/yaml-inspector/yamlInspector.html create mode 100644 app/kubernetes/components/yaml-inspector/yamlInspector.js create mode 100644 app/kubernetes/components/yaml-inspector/yamlInspectorController.js create mode 100644 app/kubernetes/converters/application.js create mode 100644 app/kubernetes/converters/configMap.js create mode 100644 app/kubernetes/converters/configuration.js create mode 100644 app/kubernetes/converters/daemonSet.js create mode 100644 app/kubernetes/converters/deployment.js create mode 100644 app/kubernetes/converters/event.js create mode 100644 app/kubernetes/converters/namespace.js create mode 100644 app/kubernetes/converters/node.js create mode 100644 app/kubernetes/converters/persistentVolumeClaim.js create mode 100644 app/kubernetes/converters/pod.js create mode 100644 app/kubernetes/converters/resourcePool.js create mode 100644 app/kubernetes/converters/resourceQuota.js create mode 100644 app/kubernetes/converters/secret.js create mode 100644 app/kubernetes/converters/service.js create mode 100644 app/kubernetes/converters/statefulSet.js create mode 100644 app/kubernetes/converters/storageClass.js create mode 100644 app/kubernetes/converters/volume.js create mode 100644 app/kubernetes/filters/applicationFilters.js create mode 100644 app/kubernetes/filters/configurationFilters.js create mode 100644 app/kubernetes/filters/eventFilters.js create mode 100644 app/kubernetes/filters/filters.js create mode 100644 app/kubernetes/filters/nodeFilters.js create mode 100644 app/kubernetes/filters/podFilters.js create mode 100644 app/kubernetes/helpers/application/index.js create mode 100644 app/kubernetes/helpers/application/rollback.js create mode 100644 app/kubernetes/helpers/commonHelper.js create mode 100644 app/kubernetes/helpers/configMapHelper.js create mode 100644 app/kubernetes/helpers/configurationHelper.js create mode 100644 app/kubernetes/helpers/eventHelper.js create mode 100644 app/kubernetes/helpers/formValidationHelper.js create mode 100644 app/kubernetes/helpers/history/daemonset.js create mode 100644 app/kubernetes/helpers/history/deployment.js create mode 100644 app/kubernetes/helpers/history/index.js create mode 100644 app/kubernetes/helpers/history/statefulset.js create mode 100644 app/kubernetes/helpers/namespaceHelper.js create mode 100644 app/kubernetes/helpers/resourceQuotaHelper.js create mode 100644 app/kubernetes/helpers/resourceReservationHelper.js create mode 100644 app/kubernetes/helpers/serviceHelper.js create mode 100644 app/kubernetes/helpers/stackHelper.js create mode 100644 app/kubernetes/helpers/volumeHelper.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/converter.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/helper.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/models.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/rest.js create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/service.js create mode 100644 app/kubernetes/models/application/formValues.js create mode 100644 app/kubernetes/models/application/models.js create mode 100644 app/kubernetes/models/application/payloads.js create mode 100644 app/kubernetes/models/common/params.js create mode 100644 app/kubernetes/models/common/payloads.js create mode 100644 app/kubernetes/models/config-map/models.js create mode 100644 app/kubernetes/models/config-map/payloads.js create mode 100644 app/kubernetes/models/configuration/formvalues.js create mode 100644 app/kubernetes/models/configuration/models.js create mode 100644 app/kubernetes/models/daemon-set/models.js create mode 100644 app/kubernetes/models/daemon-set/payloads.js create mode 100644 app/kubernetes/models/deploy.js create mode 100644 app/kubernetes/models/deployment/models.js create mode 100644 app/kubernetes/models/deployment/payloads.js create mode 100644 app/kubernetes/models/event/models.js create mode 100644 app/kubernetes/models/history/models.js create mode 100644 app/kubernetes/models/namespace/models.js create mode 100644 app/kubernetes/models/namespace/payloads.js create mode 100644 app/kubernetes/models/node/models.js create mode 100644 app/kubernetes/models/pod/models.js create mode 100644 app/kubernetes/models/port/models.js create mode 100644 app/kubernetes/models/resource-pool/models.js create mode 100644 app/kubernetes/models/resource-quota/models.js create mode 100644 app/kubernetes/models/resource-quota/payloads.js create mode 100644 app/kubernetes/models/resource-reservation/models.js create mode 100644 app/kubernetes/models/secret/models.js create mode 100644 app/kubernetes/models/secret/payloads.js create mode 100644 app/kubernetes/models/service/models.js create mode 100644 app/kubernetes/models/service/payloads.js create mode 100644 app/kubernetes/models/stack/models.js create mode 100644 app/kubernetes/models/stateful-set/models.js create mode 100644 app/kubernetes/models/stateful-set/payloads.js create mode 100644 app/kubernetes/models/storage-class/models.js create mode 100644 app/kubernetes/models/volume/models.js create mode 100644 app/kubernetes/models/volume/payloads.js create mode 100644 app/kubernetes/rest/configMap.js create mode 100644 app/kubernetes/rest/controllerRevision.js create mode 100644 app/kubernetes/rest/daemonSet.js create mode 100644 app/kubernetes/rest/deployment.js create mode 100644 app/kubernetes/rest/event.js create mode 100644 app/kubernetes/rest/health.js create mode 100644 app/kubernetes/rest/namespace.js create mode 100644 app/kubernetes/rest/node.js create mode 100644 app/kubernetes/rest/persistentVolumeClaim.js create mode 100644 app/kubernetes/rest/pod.js create mode 100644 app/kubernetes/rest/replicaSet.js create mode 100644 app/kubernetes/rest/resourceQuota.js create mode 100644 app/kubernetes/rest/response/transform.js create mode 100644 app/kubernetes/rest/secret.js create mode 100644 app/kubernetes/rest/service.js create mode 100644 app/kubernetes/rest/statefulSet.js create mode 100644 app/kubernetes/rest/storage.js create mode 100644 app/kubernetes/services/applicationService.js create mode 100644 app/kubernetes/services/configMapService.js create mode 100644 app/kubernetes/services/configurationService.js create mode 100644 app/kubernetes/services/controllerRevisionService.js create mode 100644 app/kubernetes/services/daemonSetService.js create mode 100644 app/kubernetes/services/deploymentService.js create mode 100644 app/kubernetes/services/eventService.js create mode 100644 app/kubernetes/services/healthService.js create mode 100644 app/kubernetes/services/historyService.js create mode 100644 app/kubernetes/services/namespaceService.js create mode 100644 app/kubernetes/services/nodeService.js create mode 100644 app/kubernetes/services/persistentVolumeClaimService.js create mode 100644 app/kubernetes/services/podService.js create mode 100644 app/kubernetes/services/replicaSetService.js create mode 100644 app/kubernetes/services/resourcePoolService.js create mode 100644 app/kubernetes/services/resourceQuotaService.js create mode 100644 app/kubernetes/services/secretService.js create mode 100644 app/kubernetes/services/serviceService.js create mode 100644 app/kubernetes/services/stackService.js create mode 100644 app/kubernetes/services/statefulSetService.js create mode 100644 app/kubernetes/services/storageService.js create mode 100644 app/kubernetes/services/volumeService.js create mode 100644 app/kubernetes/views/applications/applications.html create mode 100644 app/kubernetes/views/applications/applications.js create mode 100644 app/kubernetes/views/applications/applicationsController.js create mode 100644 app/kubernetes/views/applications/console/console.html create mode 100644 app/kubernetes/views/applications/console/console.js create mode 100644 app/kubernetes/views/applications/console/consoleController.js create mode 100644 app/kubernetes/views/applications/create/createApplication.html create mode 100644 app/kubernetes/views/applications/create/createApplication.js create mode 100644 app/kubernetes/views/applications/create/createApplicationController.js create mode 100644 app/kubernetes/views/applications/edit/application.html create mode 100644 app/kubernetes/views/applications/edit/application.js create mode 100644 app/kubernetes/views/applications/edit/applicationController.js create mode 100644 app/kubernetes/views/applications/logs/logs.html create mode 100644 app/kubernetes/views/applications/logs/logs.js create mode 100644 app/kubernetes/views/applications/logs/logsController.js create mode 100644 app/kubernetes/views/cluster/cluster.html create mode 100644 app/kubernetes/views/cluster/cluster.js create mode 100644 app/kubernetes/views/cluster/clusterController.js create mode 100644 app/kubernetes/views/cluster/node/node.html create mode 100644 app/kubernetes/views/cluster/node/node.js create mode 100644 app/kubernetes/views/cluster/node/nodeController.js create mode 100644 app/kubernetes/views/configurations/configurations.html create mode 100644 app/kubernetes/views/configurations/configurations.js create mode 100644 app/kubernetes/views/configurations/configurationsController.js create mode 100644 app/kubernetes/views/configurations/create/createConfiguration.html create mode 100644 app/kubernetes/views/configurations/create/createConfiguration.js create mode 100644 app/kubernetes/views/configurations/create/createConfigurationController.js create mode 100644 app/kubernetes/views/configurations/edit/configuration.html create mode 100644 app/kubernetes/views/configurations/edit/configuration.js create mode 100644 app/kubernetes/views/configurations/edit/configurationController.js create mode 100644 app/kubernetes/views/configure/configure.html create mode 100644 app/kubernetes/views/configure/configureController.js create mode 100644 app/kubernetes/views/dashboard/dashboard.html create mode 100644 app/kubernetes/views/dashboard/dashboard.js create mode 100644 app/kubernetes/views/dashboard/dashboardController.js create mode 100644 app/kubernetes/views/deploy/deploy.html create mode 100644 app/kubernetes/views/deploy/deploy.js create mode 100644 app/kubernetes/views/deploy/deployController.js create mode 100644 app/kubernetes/views/resource-pools/access/resourcePoolAccess.html create mode 100644 app/kubernetes/views/resource-pools/access/resourcePoolAccess.js create mode 100644 app/kubernetes/views/resource-pools/access/resourcePoolAccessController.js create mode 100644 app/kubernetes/views/resource-pools/create/createResourcePool.html create mode 100644 app/kubernetes/views/resource-pools/create/createResourcePool.js create mode 100644 app/kubernetes/views/resource-pools/create/createResourcePoolController.js create mode 100644 app/kubernetes/views/resource-pools/edit/resourcePool.html create mode 100644 app/kubernetes/views/resource-pools/edit/resourcePool.js create mode 100644 app/kubernetes/views/resource-pools/edit/resourcePoolController.js create mode 100644 app/kubernetes/views/resource-pools/resourcePools.html create mode 100644 app/kubernetes/views/resource-pools/resourcePools.js create mode 100644 app/kubernetes/views/resource-pools/resourcePoolsController.js create mode 100644 app/kubernetes/views/stacks/logs/logs.html create mode 100644 app/kubernetes/views/stacks/logs/logs.js create mode 100644 app/kubernetes/views/stacks/logs/logsController.js create mode 100644 app/kubernetes/views/volumes/edit/volume.html create mode 100644 app/kubernetes/views/volumes/edit/volume.js create mode 100644 app/kubernetes/views/volumes/edit/volumeController.js create mode 100644 app/kubernetes/views/volumes/volumes.html create mode 100644 app/kubernetes/views/volumes/volumes.js create mode 100644 app/kubernetes/views/volumes/volumesController.js create mode 100644 app/portainer/error.js create mode 100644 app/portainer/models/endpoint/formValues.js create mode 100644 app/portainer/models/endpoint/models.js create mode 100644 app/portainer/services/allSettled.js create mode 100644 app/portainer/views/init/endpoint/includes/agent.html create mode 100644 app/portainer/views/init/endpoint/includes/azure.html create mode 100644 app/portainer/views/init/endpoint/includes/localDocker.html create mode 100644 app/portainer/views/init/endpoint/includes/localKubernetes.html create mode 100644 app/portainer/views/init/endpoint/includes/remote.html create mode 100644 app/portainer/views/logout/logout.html create mode 100644 app/portainer/views/logout/logoutController.js create mode 100755 build/download_kompose_binary.sh create mode 100755 build/download_kubectl_binary.sh diff --git a/.codeclimate.yml b/.codeclimate.yml index 07eb34e8b..32136dcac 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,62 +1,44 @@ version: "2" checks: argument-count: - enabled: true - config: - threshold: 4 + enabled: false complex-logic: - enabled: true - config: - threshold: 4 + enabled: false file-lines: - enabled: true - config: - threshold: 300 + enabled: false method-complexity: enabled: false method-count: - enabled: true - config: - threshold: 20 + enabled: false method-lines: - enabled: true - config: - threshold: 50 + enabled: false nested-control-flow: - enabled: true - config: - threshold: 4 + enabled: false return-statements: enabled: false similar-code: - enabled: true - config: - threshold: #language-specific defaults. overrides affect all languages. + enabled: false identical-code: - enabled: true - config: - threshold: #language-specific defaults. overrides affect all languages. + enabled: false plugins: gofmt: enabled: true - golint: - enabled: true - govet: - enabled: true - csslint: - enabled: true - duplication: - enabled: true - config: - languages: - javascript: - mass_threshold: 80 eslint: enabled: true channel: "eslint-5" config: config: .eslintrc.yml - fixme: - enabled: true exclude_patterns: +- assets/ +- build/ +- dist/ +- distribution/ +- node_modules - test/ +- webpack/ +- gruntfile.js +- webpack.config.js +- api/ +- "!app/kubernetes/**" +- .github/ +- .tmp/ diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 9005ccd40..00b13e087 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,47 +1,48 @@ ---- -name: Bug report -about: Create a bug report - ---- - - - -**Bug description** -A clear and concise description of what the bug is. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Portainer Logs** -Provide the logs of your Portainer container or Service. -You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer) - -**Steps to reproduce the issue:** -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Technical details:** -* Portainer version: -* Docker version (managed by Portainer): -* Platform (windows/linux): -* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`): -* Browser: - -**Additional context** -Add any other context about the problem here. +--- +name: Bug report +about: Create a bug report +--- + + + +**Bug description** +A clear and concise description of what the bug is. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Portainer Logs** +Provide the logs of your Portainer container or Service. +You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer) + +**Steps to reproduce the issue:** + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Technical details:** + +- Portainer version: +- Docker version (managed by Portainer): +- Platform (windows/linux): +- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`): +- Browser: + +**Additional context** +Add any other context about the problem here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3f4d5a37..537ae511f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,10 +6,10 @@ Some basic conventions for contributing to this project. Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork. -* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring -* Develop in a topic branch, not master/develop +- Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring +- Develop in a topic branch, not master/develop -When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator). +When creating a new branch, prefix it with the _type_ of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator). For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`. @@ -37,14 +37,14 @@ Lines should not exceed 100 characters. This allows the message to be easier to Must be one of the following: -* **feat**: A new feature -* **fix**: A bug fix -* **docs**: Documentation only changes -* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) -* **refactor**: A code change that neither fixes a bug or adds a feature -* **test**: Adding missing tests -* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation +- **refactor**: A code change that neither fixes a bug or adds a feature +- **test**: Adding missing tests +- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation ### Scope @@ -57,9 +57,9 @@ You can use the **area** label tag associated on the issue here (for `area/conta The subject contains succinct description of the change: -* use the imperative, present tense: "change" not "changed" nor "changes" -* don't capitalize first letter -* no dot (.) at the end +- use the imperative, present tense: "change" not "changed" nor "changes" +- don't capitalize first letter +- no dot (.) at the end ## Contribution process diff --git a/api/chisel/service.go b/api/chisel/service.go index e21b67358..12ea9ef31 100644 --- a/api/chisel/service.go +++ b/api/chisel/service.go @@ -28,7 +28,7 @@ type Service struct { serverPort string tunnelDetailsMap cmap.ConcurrentMap dataStore portainer.DataStore - snapshotter portainer.Snapshotter + snapshotService portainer.SnapshotService chiselServer *chserver.Server } @@ -45,7 +45,7 @@ func NewService(dataStore portainer.DataStore) *Service { // be found inside the database, it will generate a new one randomly and persist it. // It starts the tunnel status verification process in the background. // The snapshotter is used in the tunnel status verification process. -func (service *Service) StartTunnelServer(addr, port string, snapshotter portainer.Snapshotter) error { +func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error { keySeed, err := service.retrievePrivateKeySeed() if err != nil { return err @@ -78,7 +78,7 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotter portain return err } - service.snapshotter = snapshotter + service.snapshotService = snapshotService go service.startTunnelVerificationLoop() return nil @@ -177,13 +177,13 @@ func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tun } endpointURL := endpoint.URL + endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort) - snapshot, err := service.snapshotter.CreateSnapshot(endpoint) + err = service.snapshotService.SnapshotEndpoint(endpoint) if err != nil { return err } - endpoint.Snapshots = []portainer.Snapshot{*snapshot} endpoint.URL = endpointURL return service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 728018ccd..830a8aede 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -6,12 +6,9 @@ import ( "strings" "time" - "github.com/portainer/portainer/api/chisel" - "github.com/portainer/portainer/api/internal/authorization" - "github.com/portainer/portainer/api/internal/snapshot" - - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt" + "github.com/portainer/portainer/api/chisel" "github.com/portainer/portainer/api/cli" "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/docker" @@ -20,7 +17,11 @@ import ( "github.com/portainer/portainer/api/git" "github.com/portainer/portainer/api/http" "github.com/portainer/portainer/api/http/client" + "github.com/portainer/portainer/api/internal/authorization" + "github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/jwt" + "github.com/portainer/portainer/api/kubernetes" + kubecli "github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/ldap" "github.com/portainer/portainer/api/libcompose" ) @@ -78,6 +79,10 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService) } +func initKubernetesDeployer(assetsPath string) portainer.KubernetesDeployer { + return exec.NewKubernetesDeployer(assetsPath) +} + func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) { settings, err := dataStore.Settings().Settings() if err != nil { @@ -107,12 +112,24 @@ func initGitService() portainer.GitService { return git.NewService() } -func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory { +func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory { return docker.NewClientFactory(signatureService, reverseTunnelService) } -func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter { - return docker.NewSnapshotter(clientFactory) +func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *kubecli.ClientFactory { + return kubecli.NewClientFactory(signatureService, reverseTunnelService) +} + +func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) { + dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory) + kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory) + + snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter) + if err != nil { + return nil, err + } + + return snapshotService, nil } func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService) error { @@ -187,7 +204,7 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D return generateAndStoreKeyPair(fileService, signatureService) } -func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { +func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error { tlsConfiguration := portainer.TLSConfiguration{ TLS: *flags.TLS, TLSSkipVerify: *flags.TLSSkipVerify, @@ -214,7 +231,8 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat Extensions: []portainer.EndpointExtension{}, TagIDs: []portainer.TagID{}, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } if strings.HasPrefix(endpoint.URL, "tcp://") { @@ -233,10 +251,15 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat } } - return snapshotAndPersistEndpoint(endpoint, dataStore, snapshotter) + err := snapshotService.SnapshotEndpoint(endpoint) + if err != nil { + log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) + } + + return dataStore.Endpoint().CreateEndpoint(endpoint) } -func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { +func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error { if strings.HasPrefix(endpointURL, "tcp://") { _, err := client.ExecutePingOperation(endpointURL, nil) if err != nil { @@ -257,27 +280,19 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore, Extensions: []portainer.EndpointExtension{}, TagIDs: []portainer.TagID{}, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } - return snapshotAndPersistEndpoint(endpoint, dataStore, snapshotter) -} - -func snapshotAndPersistEndpoint(endpoint *portainer.Endpoint, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { - snapshot, err := snapshotter.CreateSnapshot(endpoint) - endpoint.Status = portainer.EndpointStatusUp + err := snapshotService.SnapshotEndpoint(endpoint) if err != nil { log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) } - if snapshot != nil { - endpoint.Snapshots = []portainer.Snapshot{*snapshot} - } - return dataStore.Endpoint().CreateEndpoint(endpoint) } -func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) error { +func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snapshotService portainer.SnapshotService) error { if *flags.EndpointURL == "" { return nil } @@ -293,9 +308,9 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap } if *flags.TLS || *flags.TLSSkipVerify { - return createTLSSecuredEndpoint(flags, dataStore, snapshotter) + return createTLSSecuredEndpoint(flags, dataStore, snapshotService) } - return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotter) + return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService) } func initExtensionManager(fileService portainer.FileService, dataStore portainer.DataStore) (portainer.ExtensionManager, error) { @@ -357,11 +372,10 @@ func main() { reverseTunnelService := chisel.NewService(dataStore) - clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService) + dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService) + kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService) - snapshotter := initSnapshotter(clientFactory) - - snapshotService, err := snapshot.NewService(*flags.SnapshotInterval, dataStore, snapshotter) + snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory) if err != nil { log.Fatal(err) } @@ -374,6 +388,8 @@ func main() { composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService) + kubernetesDeployer := initKubernetesDeployer(*flags.Assets) + if dataStore.IsNew() { err = updateSettingsFromFlags(dataStore, flags) if err != nil { @@ -388,7 +404,7 @@ func main() { applicationStatus := initStatus(flags) - err = initEndpoint(flags, dataStore, snapshotter) + err = initEndpoint(flags, dataStore, snapshotService) if err != nil { log.Fatal(err) } @@ -432,32 +448,33 @@ func main() { go terminateIfNoAdminCreated(dataStore) - err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter) + err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService) if err != nil { log.Fatal(err) } var server portainer.Server = &http.Server{ - ReverseTunnelService: reverseTunnelService, - Status: applicationStatus, - BindAddress: *flags.Addr, - AssetsPath: *flags.Assets, - DataStore: dataStore, - SwarmStackManager: swarmStackManager, - ComposeStackManager: composeStackManager, - ExtensionManager: extensionManager, - CryptoService: cryptoService, - JWTService: jwtService, - FileService: fileService, - LDAPService: ldapService, - GitService: gitService, - SignatureService: digitalSignatureService, - SnapshotService: snapshotService, - Snapshotter: snapshotter, - SSL: *flags.SSL, - SSLCert: *flags.SSLCert, - SSLKey: *flags.SSLKey, - DockerClientFactory: clientFactory, + ReverseTunnelService: reverseTunnelService, + Status: applicationStatus, + BindAddress: *flags.Addr, + AssetsPath: *flags.Assets, + DataStore: dataStore, + SwarmStackManager: swarmStackManager, + ComposeStackManager: composeStackManager, + KubernetesDeployer: kubernetesDeployer, + ExtensionManager: extensionManager, + CryptoService: cryptoService, + JWTService: jwtService, + FileService: fileService, + LDAPService: ldapService, + GitService: gitService, + SignatureService: digitalSignatureService, + SnapshotService: snapshotService, + SSL: *flags.SSL, + SSLCert: *flags.SSLCert, + SSLKey: *flags.SSLKey, + DockerClientFactory: dockerClientFactory, + KubernetesClientFactory: kubernetesClientFactory, } log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) diff --git a/api/docker/client.go b/api/docker/client.go index c1bd7a8d0..6ccb5ad7c 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -31,7 +31,7 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers } } -// CreateClient is a generic function to create a Docker client based on +// createClient is a generic function to create a Docker client based on // a specific endpoint configuration. The nodeName parameter can be used // with an agent enabled endpoint to target a specific node in an agent cluster. func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) { @@ -39,7 +39,7 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam return nil, unsupportedEnvironmentType } else if endpoint.Type == portainer.AgentOnDockerEnvironment { return createAgentClient(endpoint, factory.signatureService, nodeName) - } else if endpoint.Type == portainer.EdgeAgentEnvironment { + } else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName) } diff --git a/api/docker/snapshot.go b/api/docker/snapshot.go index 0ab887373..f9ead6d3e 100644 --- a/api/docker/snapshot.go +++ b/api/docker/snapshot.go @@ -12,13 +12,36 @@ import ( "github.com/portainer/portainer/api" ) -func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Snapshot, error) { +// Snapshotter represents a service used to create endpoint snapshots +type Snapshotter struct { + clientFactory *ClientFactory +} + +// NewSnapshotter returns a new Snapshotter instance +func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter { + return &Snapshotter{ + clientFactory: clientFactory, + } +} + +// CreateSnapshot creates a snapshot of a specific Docker endpoint +func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) { + cli, err := snapshotter.clientFactory.CreateClient(endpoint, "") + if err != nil { + return nil, err + } + defer cli.Close() + + return snapshot(cli, endpoint) +} + +func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) { _, err := cli.Ping(context.Background()) if err != nil { return nil, err } - snapshot := &portainer.Snapshot{ + snapshot := &portainer.DockerSnapshot{ StackCount: 0, } @@ -68,7 +91,7 @@ func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Snap return snapshot, nil } -func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotInfo(snapshot *portainer.DockerSnapshot, cli *client.Client) error { info, err := cli.Info(context.Background()) if err != nil { return err @@ -82,7 +105,7 @@ func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotNodes(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error { nodes, err := cli.NodeList(context.Background(), types.NodeListOptions{}) if err != nil { return err @@ -98,7 +121,7 @@ func snapshotNodes(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotSwarmServices(snapshot *portainer.DockerSnapshot, cli *client.Client) error { stacks := make(map[string]struct{}) services, err := cli.ServiceList(context.Background(), types.ServiceListOptions{}) @@ -119,7 +142,7 @@ func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) err return nil } -func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client) error { containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true}) if err != nil { return err @@ -159,7 +182,7 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error return nil } -func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotImages(snapshot *portainer.DockerSnapshot, cli *client.Client) error { images, err := cli.ImageList(context.Background(), types.ImageListOptions{}) if err != nil { return err @@ -170,7 +193,7 @@ func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotVolumes(snapshot *portainer.DockerSnapshot, cli *client.Client) error { volumes, err := cli.VolumeList(context.Background(), filters.Args{}) if err != nil { return err @@ -181,7 +204,7 @@ func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotNetworks(snapshot *portainer.DockerSnapshot, cli *client.Client) error { networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{}) if err != nil { return err @@ -190,7 +213,7 @@ func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } -func snapshotVersion(snapshot *portainer.Snapshot, cli *client.Client) error { +func snapshotVersion(snapshot *portainer.DockerSnapshot, cli *client.Client) error { version, err := cli.ServerVersion(context.Background()) if err != nil { return err diff --git a/api/docker/snapshotter.go b/api/docker/snapshotter.go deleted file mode 100644 index 25eceb023..000000000 --- a/api/docker/snapshotter.go +++ /dev/null @@ -1,28 +0,0 @@ -package docker - -import ( - "github.com/portainer/portainer/api" -) - -// Snapshotter represents a service used to create endpoint snapshots -type Snapshotter struct { - clientFactory *ClientFactory -} - -// NewSnapshotter returns a new Snapshotter instance -func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter { - return &Snapshotter{ - clientFactory: clientFactory, - } -} - -// CreateSnapshot creates a snapshot of a specific endpoint -func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) { - cli, err := snapshotter.clientFactory.CreateClient(endpoint, "") - if err != nil { - return nil, err - } - defer cli.Close() - - return snapshot(cli, endpoint) -} diff --git a/api/exec/kubernetes_deploy.go b/api/exec/kubernetes_deploy.go new file mode 100644 index 000000000..0330237e0 --- /dev/null +++ b/api/exec/kubernetes_deploy.go @@ -0,0 +1,89 @@ +package exec + +import ( + "bytes" + "errors" + "io/ioutil" + "os/exec" + "path" + "runtime" + "strings" + + portainer "github.com/portainer/portainer/api" +) + +// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment. +type KubernetesDeployer struct { + binaryPath string +} + +// NewKubernetesDeployer initializes a new KubernetesDeployer service. +func NewKubernetesDeployer(binaryPath string) *KubernetesDeployer { + return &KubernetesDeployer{ + binaryPath: binaryPath, + } +} + +// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint. +// If composeFormat is set to true, it will leverage the kompose binary to deploy a compose compliant manifest. +// Otherwise it will use kubectl to deploy the manifest. +func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) { + if composeFormat { + convertedData, err := deployer.convertComposeData(data) + if err != nil { + return nil, err + } + data = string(convertedData) + } + + token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + return nil, err + } + + command := path.Join(deployer.binaryPath, "kubectl") + if runtime.GOOS == "windows" { + command = path.Join(deployer.binaryPath, "kubectl.exe") + } + + args := make([]string, 0) + args = append(args, "--server", endpoint.URL) + args = append(args, "--insecure-skip-tls-verify") + args = append(args, "--token", string(token)) + args = append(args, "--namespace", namespace) + args = append(args, "apply", "-f", "-") + + var stderr bytes.Buffer + cmd := exec.Command(command, args...) + cmd.Stderr = &stderr + cmd.Stdin = strings.NewReader(data) + + output, err := cmd.Output() + if err != nil { + return nil, errors.New(stderr.String()) + } + + return output, nil +} + +func (deployer *KubernetesDeployer) convertComposeData(data string) ([]byte, error) { + command := path.Join(deployer.binaryPath, "kompose") + if runtime.GOOS == "windows" { + command = path.Join(deployer.binaryPath, "kompose.exe") + } + + args := make([]string, 0) + args = append(args, "convert", "-f", "-", "--stdout") + + var stderr bytes.Buffer + cmd := exec.Command(command, args...) + cmd.Stderr = &stderr + cmd.Stdin = strings.NewReader(data) + + output, err := cmd.Output() + if err != nil { + return nil, errors.New(stderr.String()) + } + + return output, nil +} diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index d5b779a02..94d9e0904 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -121,7 +121,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa args = append(args, "--config", dataPath) endpointURL := endpoint.URL - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port) } diff --git a/api/go.mod b/api/go.mod index 3af5acf37..650af2de9 100644 --- a/api/go.mod +++ b/api/go.mod @@ -29,8 +29,12 @@ require ( github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 + golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/src-d/go-git.v4 v4.13.1 + k8s.io/api v0.17.2 + k8s.io/apimachinery v0.17.2 + k8s.io/client-go v0.17.2 ) replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 diff --git a/api/go.sum b/api/go.sum index 66b50d287..d7b7db557 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,6 +1,15 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.3.8 h1:dvxbxtpTIjdAbx2OtL26p4eq0iEvys/U5yrsTJb3NZI= github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= @@ -8,6 +17,9 @@ github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+q github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= @@ -36,6 +48,7 @@ github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -60,14 +73,20 @@ github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU= github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck= @@ -77,21 +96,42 @@ github.com/go-ldap/ldap/v3 v3.1.8 h1:5vU/2jOh9HqprwXp8aF915s9p6Z8wmbSEVF7/gdTFhM github.com/go-ldap/ldap/v3 v3.1.8/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v0.0.0-20160317213430-0eeaf8392f5b/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= @@ -101,6 +141,12 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -115,13 +161,17 @@ github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 h1:7ufdyC3aMxF github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8buj+yNfmLEP0ENlbG/FRnK6bVmuhqXnukYCs9sDvY= github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 h1:0c9jcgBtHRtDU//jTrcCgWG6UHjMZytiq/3WhraNgUM= github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw= github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= @@ -134,6 +184,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -145,12 +196,22 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449 h1:Aq8iG72akPb/kszE7ksZ5ldV+JYPYii/KZOxlpJF07s= @@ -160,9 +221,11 @@ github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5X github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw= github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8= @@ -191,14 +254,20 @@ github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc= github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4= github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= @@ -210,30 +279,58 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg= github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= +golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -241,18 +338,36 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= @@ -260,17 +375,53 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20191114100352-16d7abae0d2a h1:86XISgFlG7lPOWj6wYLxd+xqhhVt/WQjS4Tf39rP09s= +k8s.io/api v0.0.0-20191114100352-16d7abae0d2a/go.mod h1:qetVJgs5i8jwdFIdoOZ70ks0ecgU+dYwqZ2uD1srwOU= +k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc= +k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= +k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb h1:ZUNsbuPdXWrj0rZziRfCWcFg9ZP31OKkziqCbiphznI= +k8s.io/apimachinery v0.0.0-20191028221656-72ed19daf4bb/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ= +k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4= +k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/client-go v0.0.0-20191114101535-6c5935290e33 h1:07mhG/2oEoo3N+sHVOo0L9PJ/qvbk3N5n2dj8IWefnQ= +k8s.io/client-go v0.0.0-20191114101535-6c5935290e33/go.mod h1:4L/zQOBkEf4pArQJ+CMk1/5xjA30B5oyWv+Bzb44DOw= +k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc= +k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= +k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1 h1:+ySTxfHnfzZb9ys375PXNlLhkJPLKgHajBU0N62BDvE= +k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index 1f8415597..9bc98f834 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -7,6 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -14,12 +15,13 @@ import ( // Handler is the HTTP handler used to handle authentication operations. type Handler struct { *mux.Router - DataStore portainer.DataStore - CryptoService portainer.CryptoService - JWTService portainer.JWTService - LDAPService portainer.LDAPService - ProxyManager *proxy.Manager - AuthorizationService *authorization.Service + DataStore portainer.DataStore + CryptoService portainer.CryptoService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + ProxyManager *proxy.Manager + AuthorizationService *authorization.Service + KubernetesTokenCacheManager *kubernetes.TokenCacheManager } // NewHandler creates a handler to manage authentication operations. @@ -32,6 +34,8 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.validateOAuth)))).Methods(http.MethodPost) h.Handle("/auth", rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost) + h.Handle("/auth/logout", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/auth/logout.go b/api/http/handler/auth/logout.go new file mode 100644 index 000000000..90519d5b9 --- /dev/null +++ b/api/http/handler/auth/logout.go @@ -0,0 +1,21 @@ +package auth + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api/http/security" +) + +// POST request on /logout +func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err} + } + + handler.KubernetesTokenCacheManager.RemoveUserFromCache(int(tokenData.ID)) + + return response.Empty(w) +} diff --git a/api/http/handler/edgegroups/associated_endpoints.go b/api/http/handler/edgegroups/associated_endpoints.go index 8eeed5620..b34711eda 100644 --- a/api/http/handler/edgegroups/associated_endpoints.go +++ b/api/http/handler/edgegroups/associated_endpoints.go @@ -38,7 +38,7 @@ func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatc results := []portainer.EndpointID{} for _, endpoint := range endpoints { - if _, ok := endpointSet[endpoint.ID]; ok && endpoint.Type == portainer.EdgeAgentEnvironment { + if _, ok := endpointSet[endpoint.ID]; ok && (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) { results = append(results, endpoint.ID) } } diff --git a/api/http/handler/edgegroups/edgegroup_create.go b/api/http/handler/edgegroups/edgegroup_create.go index fc0d09d37..90c733073 100644 --- a/api/http/handler/edgegroups/edgegroup_create.go +++ b/api/http/handler/edgegroups/edgegroup_create.go @@ -67,7 +67,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} } - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { endpointIDs = append(endpointIDs, endpoint.ID) } } diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index c2c4e36d2..2be4fb346 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -87,7 +87,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err} } - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { endpointIDs = append(endpointIDs, endpoint.ID) } } diff --git a/api/http/handler/edgejobs/edgejob_create.go b/api/http/handler/edgejobs/edgejob_create.go index 76720006c..33f91493a 100644 --- a/api/http/handler/edgejobs/edgejob_create.go +++ b/api/http/handler/edgejobs/edgejob_create.go @@ -187,7 +187,7 @@ func (handler *Handler) addAndPersistEdgeJob(edgeJob *portainer.EdgeJob, file [] return err } - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnDockerEnvironment && endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment { delete(edgeJob.Endpoints, ID) } } diff --git a/api/http/handler/edgejobs/edgejob_update.go b/api/http/handler/edgejobs/edgejob_update.go index b33ad67b8..559ba26f2 100644 --- a/api/http/handler/edgejobs/edgejob_update.go +++ b/api/http/handler/edgejobs/edgejob_update.go @@ -82,7 +82,7 @@ func (handler *Handler) updateEdgeSchedule(edgeJob *portainer.EdgeJob, payload * return err } - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnDockerEnvironment && endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment { continue } diff --git a/api/http/handler/endpointgroups/endpoints.go b/api/http/handler/endpointgroups/endpoints.go index e854ca635..c1757d3c7 100644 --- a/api/http/handler/endpointgroups/endpoints.go +++ b/api/http/handler/endpointgroups/endpoints.go @@ -6,7 +6,7 @@ import ( ) func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) error { - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { return nil } diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index 870f2de80..037cb4dfb 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -27,6 +27,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) h.PathPrefix("/{id}/docker").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) + h.PathPrefix("/{id}/kubernetes").Handler( + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToKubernetesAPI))) h.PathPrefix("/{id}/storidge").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI))) return h diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index 8a228fcf4..041a6f178 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -30,7 +30,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { if endpoint.EdgeID == "" { return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")} } diff --git a/api/http/handler/endpointproxy/proxy_kubernetes.go b/api/http/handler/endpointproxy/proxy_kubernetes.go new file mode 100644 index 000000000..744ab4340 --- /dev/null +++ b/api/http/handler/endpointproxy/proxy_kubernetes.go @@ -0,0 +1,73 @@ +package endpointproxy + +import ( + "errors" + "fmt" + "time" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/portainer/api" + + "net/http" +) + +func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + + if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { + if endpoint.EdgeID == "" { + return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")} + } + + tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID) + if tunnel.Status == portainer.EdgeAgentIdle { + handler.ProxyManager.DeleteEndpointProxy(endpoint) + + err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err} + } + + settings, err := handler.DataStore.Settings().Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second + time.Sleep(waitForAgentToConnect * 2) + } + } + + var proxy http.Handler + proxy = handler.ProxyManager.GetEndpointProxy(endpoint) + if proxy == nil { + proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err} + } + } + + requestPrefix := fmt.Sprintf("/%d/kubernetes", endpointID) + if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { + requestPrefix = fmt.Sprintf("/%d", endpointID) + } + + http.StripPrefix(requestPrefix, proxy).ServeHTTP(w, r) + return nil +} diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index a2b90c1cd..9b8692903 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -13,7 +13,6 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/internal/edge" ) @@ -118,11 +117,11 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } payload.AzureAuthenticationKey = azureAuthenticationKey default: - url, err := request.RetrieveMultiPartFormValue(r, "URL", true) + endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true) if err != nil { return portainer.Error("Invalid endpoint URL") } - payload.URL = url + payload.URL = endpointURL publicURL, _ := request.RetrieveMultiPartFormValue(r, "PublicURL", true) payload.PublicURL = publicURL @@ -167,7 +166,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * EdgeStacks: map[portainer.EdgeStackID]bool{}, } - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { relatedEdgeStacks := edge.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks) for _, stackID := range relatedEdgeStacks { relationObject.EdgeStacks[stackID] = true @@ -183,14 +182,22 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * } func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment { + switch portainer.EndpointType(payload.EndpointType) { + case portainer.AzureEnvironment: return handler.createAzureEndpoint(payload) - } else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment { - return handler.createEdgeAgentEndpoint(payload) + + case portainer.EdgeAgentOnDockerEnvironment: + return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnDockerEnvironment) + + case portainer.KubernetesLocalEnvironment: + return handler.createKubernetesEndpoint(payload) + + case portainer.EdgeAgentOnKubernetesEnvironment: + return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnKubernetesEnvironment) } if payload.TLS { - return handler.createTLSSecuredEndpoint(payload) + return handler.createTLSSecuredEndpoint(payload, portainer.EndpointType(payload.EndpointType)) } return handler.createUnsecuredEndpoint(payload) } @@ -222,7 +229,8 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po AzureCredentials: credentials, TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } err = handler.saveEndpointAndUpdateAuthorizations(endpoint) @@ -233,8 +241,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po return endpoint, nil } -func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - endpointType := portainer.EdgeAgentEnvironment +func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) { endpointID := handler.DataStore.Endpoint().GetNextIdentifier() portainerURL, err := url.Parse(payload.URL) @@ -267,9 +274,10 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) Extensions: []portainer.EndpointExtension{}, TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, EdgeKey: edgeKey, EdgeCheckinInterval: payload.EdgeCheckinInterval, + Kubernetes: portainer.KubernetesDefault(), } err = handler.saveEndpointAndUpdateAuthorizations(endpoint) @@ -288,14 +296,6 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) if runtime.GOOS == "windows" { payload.URL = "npipe:////./pipe/docker_engine" } - } else { - agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, nil) - if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err} - } - if agentOnDockerEnvironment { - endpointType = portainer.AgentOnDockerEnvironment - } } endpointID := handler.DataStore.Endpoint().GetNextIdentifier() @@ -314,7 +314,8 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) Extensions: []portainer.EndpointExtension{}, TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } err := handler.snapshotAndPersistEndpoint(endpoint) @@ -325,22 +326,42 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) return endpoint, nil } -func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipClientVerify, payload.TLSSkipVerify) +func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { + if payload.URL == "" { + payload.URL = "https://kubernetes.default.svc" + } + + endpointID := handler.DataStore.Endpoint().GetNextIdentifier() + + endpoint := &portainer.Endpoint{ + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: payload.URL, + Type: portainer.KubernetesLocalEnvironment, + GroupID: portainer.EndpointGroupID(payload.GroupID), + PublicURL: payload.PublicURL, + TLSConfig: portainer.TLSConfiguration{ + TLS: payload.TLS, + TLSSkipVerify: payload.TLSSkipVerify, + }, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + TagIDs: payload.TagIDs, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), + } + + err := handler.snapshotAndPersistEndpoint(endpoint) if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to create TLS configuration", err} + return nil, err } - agentOnDockerEnvironment, err := client.ExecutePingOperation(payload.URL, tlsConfig) - if err != nil { - return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to ping Docker environment", err} - } - - endpointType := portainer.DockerEnvironment - if agentOnDockerEnvironment { - endpointType = portainer.AgentOnDockerEnvironment - } + return endpoint, nil +} +func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) { endpointID := handler.DataStore.Endpoint().GetNextIdentifier() endpoint := &portainer.Endpoint{ ID: portainer.EndpointID(endpointID), @@ -358,25 +379,25 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) Extensions: []portainer.EndpointExtension{}, TagIDs: payload.TagIDs, Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + Snapshots: []portainer.DockerSnapshot{}, + Kubernetes: portainer.KubernetesDefault(), } - filesystemError := handler.storeTLSFiles(endpoint, payload) + err := handler.storeTLSFiles(endpoint, payload) if err != nil { - return nil, filesystemError + return nil, err } - endpointCreationError := handler.snapshotAndPersistEndpoint(endpoint) - if endpointCreationError != nil { - return nil, endpointCreationError + err = handler.snapshotAndPersistEndpoint(endpoint) + if err != nil { + return nil, err } return endpoint, nil } func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) *httperror.HandlerError { - snapshot, err := handler.Snapshotter.CreateSnapshot(endpoint) - endpoint.Status = portainer.EndpointStatusUp + err := handler.SnapshotService.SnapshotEndpoint(endpoint) if err != nil { if strings.Contains(err.Error(), "Invalid request signature") { err = errors.New("agent already paired with another Portainer instance") @@ -384,10 +405,6 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with endpoint", err} } - if snapshot != nil { - endpoint.Snapshots = []portainer.Snapshot{*snapshot} - } - err = handler.saveEndpointAndUpdateAuthorizations(endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err} diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 18182db17..03816b642 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/snapshot" ) // POST request on /api/endpoints/:id/snapshot @@ -23,11 +24,11 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - if endpoint.Type == portainer.AzureEnvironment { - return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for Azure endpoints", err} + if !snapshot.SupportDirectSnapshot(endpoint) { + return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for this endpoint", err} } - snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint) + snapshotError := handler.SnapshotService.SnapshotEndpoint(endpoint) latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { @@ -39,9 +40,8 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) latestEndpointReference.Status = portainer.EndpointStatusDown } - if snapshot != nil { - latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} - } + latestEndpointReference.Snapshots = endpoint.Snapshots + latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go index 33d6f30d0..3fd83d071 100644 --- a/api/http/handler/endpoints/endpoint_snapshots.go +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -7,6 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/internal/snapshot" ) // POST request on /api/endpoints/snapshot @@ -17,11 +18,11 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request } for _, endpoint := range endpoints { - if endpoint.Type == portainer.AzureEnvironment { + if !snapshot.SupportDirectSnapshot(&endpoint) { continue } - snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint) + snapshotError := handler.SnapshotService.SnapshotEndpoint(&endpoint) latestEndpointReference, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { @@ -29,15 +30,14 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request continue } - latestEndpointReference.Status = portainer.EndpointStatusUp + endpoint.Status = portainer.EndpointStatusUp if snapshotError != nil { log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) - latestEndpointReference.Status = portainer.EndpointStatusDown + endpoint.Status = portainer.EndpointStatusDown } - if snapshot != nil { - latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} - } + latestEndpointReference.Snapshots = endpoint.Snapshots + latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index a48dac06d..b29a1da5b 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type stackStatusResponse struct { diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index bf73d9320..8979571b0 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -30,6 +30,7 @@ type endpointUpdatePayload struct { UserAccessPolicies portainer.UserAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies EdgeCheckinInterval *int + Kubernetes *portainer.KubernetesData } func (payload *endpointUpdatePayload) Validate(r *http.Request) error { @@ -120,6 +121,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } + if payload.Kubernetes != nil { + endpoint.Kubernetes = *payload.Kubernetes + } + updateAuthorizations := false if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) { endpoint.UserAccessPolicies = payload.UserAccessPolicies @@ -227,7 +232,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } - if endpoint.Type == portainer.EdgeAgentEnvironment && (groupIDChanged || tagsChanged) { + if (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && (groupIDChanged || tagsChanged) { relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation inside the database", err} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 6610d134c..3722c6e24 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -2,7 +2,7 @@ package endpoints import ( httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" @@ -15,7 +15,7 @@ import ( func hideFields(endpoint *portainer.Endpoint) { endpoint.AzureCredentials = portainer.AzureCredentials{} if len(endpoint.Snapshots) > 0 { - endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{} + endpoint.Snapshots[0].SnapshotRaw = portainer.DockerSnapshotRaw{} } } @@ -28,7 +28,7 @@ type Handler struct { FileService portainer.FileService ProxyManager *proxy.Manager ReverseTunnelService portainer.ReverseTunnelService - Snapshotter portainer.Snapshotter + SnapshotService portainer.SnapshotService } // NewHandler creates a handler to manage endpoint operations. diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 8dd9c3492..113fa60c4 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -87,6 +87,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/docker/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) + case strings.Contains(r.URL.Path, "/kubernetes/"): + http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/storidge/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/azure/"): diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index c4471a2c5..143f99962 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -1,14 +1,14 @@ package settings import ( - "github.com/portainer/portainer/api/internal/authorization" "net/http" + "github.com/portainer/portainer/api/internal/authorization" + "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" - "github.com/portainer/portainer/api/internal/snapshot" ) func hideFields(settings *portainer.Settings) { @@ -24,7 +24,7 @@ type Handler struct { FileService portainer.FileService JWTService portainer.JWTService LDAPService portainer.LDAPService - SnapshotService *snapshot.Service + SnapshotService portainer.SnapshotService } // NewHandler creates a handler to manage settings operations. diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go new file mode 100644 index 000000000..cbf8eb1fe --- /dev/null +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -0,0 +1,58 @@ +package stacks + +import ( + "net/http" + + "github.com/asaskevich/govalidator" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" +) + +type kubernetesStackPayload struct { + ComposeFormat bool + Namespace string + StackFileContent string +} + +func (payload *kubernetesStackPayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.StackFileContent) { + return portainer.Error("Invalid stack file content") + } + if govalidator.IsNull(payload.Namespace) { + return portainer.Error("Invalid namespace") + } + return nil +} + +type createKubernetesStackResponse struct { + Output string `json:"Output"` +} + +func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError { + var payload kubernetesStackPayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to deploy Kubernetes stack", err} + } + + resp := &createKubernetesStackResponse{ + Output: string(output), + } + + return response.JSON(w, resp) +} + +func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) { + handler.stackCreationMutex.Lock() + defer handler.stackCreationMutex.Unlock() + + return handler.KubernetesDeployer.Deploy(endpoint, data, composeFormat, namespace) +} diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index f0a6fc0bf..5271cffb1 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -6,7 +6,7 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -22,6 +22,7 @@ type Handler struct { GitService portainer.GitService SwarmStackManager portainer.SwarmStackManager ComposeStackManager portainer.ComposeStackManager + KubernetesDeployer portainer.KubernetesDeployer } // NewHandler creates a handler to manage stack operations. diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index c57464c2c..b4a3a3992 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -66,6 +66,12 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID) case portainer.DockerComposeStack: return handler.createComposeStack(w, r, method, endpoint, tokenData.ID) + case portainer.KubernetesStack: + if tokenData.Role != portainer.AdministratorRole { + return &httperror.HandlerError{http.StatusForbidden, "Access denied", portainer.ErrUnauthorized} + } + + return handler.createKubernetesStack(w, r, endpoint) } return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)} diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index ce6a3840b..5df4f2e2b 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -73,7 +73,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe } for _, endpoint := range endpoints { - if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && endpoint.Type == portainer.EdgeAgentEnvironment { + if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) { err = handler.updateEndpointRelations(endpoint, edgeGroups, edgeStacks) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint relations in the database", err} diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index 2a498f82e..dee223853 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -64,7 +64,7 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque if params.endpoint.Type == portainer.AgentOnDockerEnvironment { return handler.proxyAgentWebsocketRequest(w, r, params) - } else if params.endpoint.Type == portainer.EdgeAgentEnvironment { + } else if params.endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { return handler.proxyEdgeAgentWebsocketRequest(w, r, params) } diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index 8c3318a83..eb59956ff 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -70,7 +70,7 @@ func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request if params.endpoint.Type == portainer.AgentOnDockerEnvironment { return handler.proxyAgentWebsocketRequest(w, r, params) - } else if params.endpoint.Type == portainer.EdgeAgentEnvironment { + } else if params.endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { return handler.proxyEdgeAgentWebsocketRequest(w, r, params) } diff --git a/api/http/handler/websocket/handler.go b/api/http/handler/websocket/handler.go index eb86de0a9..05cd88cfc 100644 --- a/api/http/handler/websocket/handler.go +++ b/api/http/handler/websocket/handler.go @@ -4,18 +4,20 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/websocket" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/kubernetes/cli" ) // Handler is the HTTP handler used to handle websocket operations. type Handler struct { *mux.Router - DataStore portainer.DataStore - SignatureService portainer.DigitalSignatureService - ReverseTunnelService portainer.ReverseTunnelService - requestBouncer *security.RequestBouncer - connectionUpgrader websocket.Upgrader + DataStore portainer.DataStore + SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService + KubernetesClientFactory *cli.ClientFactory + requestBouncer *security.RequestBouncer + connectionUpgrader websocket.Upgrader } // NewHandler creates a handler to manage websocket operations. @@ -29,5 +31,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec))) h.PathPrefix("/websocket/attach").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketAttach))) + h.PathPrefix("/websocket/pod").Handler( + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketPodExec))) return h } diff --git a/api/http/handler/websocket/hijack.go b/api/http/handler/websocket/hijack.go index f8a7b6624..a991f3bec 100644 --- a/api/http/handler/websocket/hijack.go +++ b/api/http/handler/websocket/hijack.go @@ -2,9 +2,10 @@ package websocket import ( "fmt" - "github.com/gorilla/websocket" "net/http" "net/http/httputil" + + "github.com/gorilla/websocket" ) func hijackRequest(websocketConn *websocket.Conn, httpConn *httputil.ClientConn, request *http.Request) error { @@ -24,8 +25,8 @@ func hijackRequest(websocketConn *websocket.Conn, httpConn *httputil.ClientConn, defer tcpConn.Close() errorChan := make(chan error, 1) - go streamFromTCPConnToWebsocketConn(websocketConn, brw, errorChan) - go streamFromWebsocketConnToTCPConn(websocketConn, tcpConn, errorChan) + go streamFromReaderToWebsocket(websocketConn, brw, errorChan) + go streamFromWebsocketToWriter(websocketConn, tcpConn, errorChan) err = <-errorChan if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { diff --git a/api/http/handler/websocket/pod.go b/api/http/handler/websocket/pod.go new file mode 100644 index 000000000..46f7f1dfd --- /dev/null +++ b/api/http/handler/websocket/pod.go @@ -0,0 +1,116 @@ +package websocket + +import ( + "io" + "log" + "net/http" + "strings" + + "github.com/gorilla/websocket" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + portainer "github.com/portainer/portainer/api" +) + +// websocketPodExec handles GET requests on /websocket/pod?token=&endpointId=&namespace=&podName=&containerName=&command= +// The request will be upgraded to the websocket protocol. +// Authentication and access is controlled via the mandatory token query parameter. +// The following parameters query parameters are mandatory: +// * token: JWT token used for authentication against this endpoint +// * endpointId: endpoint ID of the endpoint where the resource is located +// * namespace: namespace where the container is located +// * podName: name of the pod containing the container +// * containerName: name of the container +// * command: command to execute in the container +func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} + } + + namespace, err := request.RetrieveQueryParameter(r, "namespace", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: namespace", err} + } + + podName, err := request.RetrieveQueryParameter(r, "podName", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: podName", err} + } + + containerName, err := request.RetrieveQueryParameter(r, "containerName", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: containerName", err} + } + + command, err := request.RetrieveQueryParameter(r, "command", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: command", err} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + + params := &webSocketRequestParams{ + endpoint: endpoint, + } + + r.Header.Del("Origin") + + if endpoint.Type == portainer.AgentOnKubernetesEnvironment { + err := handler.proxyAgentWebsocketRequest(w, r, params) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to proxy websocket request to agent", err} + } + return nil + } else if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { + err := handler.proxyEdgeAgentWebsocketRequest(w, r, params) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to proxy websocket request to Edge agent", err} + } + return nil + } + + commandArray := strings.Split(command, " ") + + websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to upgrade the connection", err} + } + defer websocketConn.Close() + + stdinReader, stdinWriter := io.Pipe() + defer stdinWriter.Close() + stdoutReader, stdoutWriter := io.Pipe() + defer stdoutWriter.Close() + + errorChan := make(chan error, 1) + go streamFromWebsocketToWriter(websocketConn, stdinWriter, errorChan) + go streamFromReaderToWebsocket(websocketConn, stdoutReader, errorChan) + + cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err} + } + + err = cli.StartExecProcess(namespace, podName, containerName, commandArray, stdinReader, stdoutWriter) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err} + } + + err = <-errorChan + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { + log.Printf("websocket error: %s \n", err.Error()) + } + + return nil +} diff --git a/api/http/handler/websocket/proxy.go b/api/http/handler/websocket/proxy.go index bd8e3f4f7..9ec317d9b 100644 --- a/api/http/handler/websocket/proxy.go +++ b/api/http/handler/websocket/proxy.go @@ -33,7 +33,9 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r } func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { - agentURL, err := url.Parse(params.endpoint.URL) + // TODO: k8s merge - make sure this is still working with Docker agent + //agentURL, err := url.Parse(params.endpoint.URL) + agentURL, err := url.Parse(fmt.Sprintf("http://%s", params.endpoint.URL)) if err != nil { return err } diff --git a/api/http/handler/websocket/stream.go b/api/http/handler/websocket/stream.go index a598b7cb7..131951803 100644 --- a/api/http/handler/websocket/stream.go +++ b/api/http/handler/websocket/stream.go @@ -1,13 +1,15 @@ package websocket import ( - "bufio" - "github.com/gorilla/websocket" - "net" + "io" "unicode/utf8" + + "github.com/gorilla/websocket" ) -func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net.Conn, errorChan chan error) { +const readerBufferSize = 2048 + +func streamFromWebsocketToWriter(websocketConn *websocket.Conn, writer io.Writer, errorChan chan error) { for { _, in, err := websocketConn.ReadMessage() if err != nil { @@ -15,7 +17,7 @@ func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net break } - _, err = tcpConn.Write(in) + _, err = writer.Write(in) if err != nil { errorChan <- err break @@ -23,10 +25,10 @@ func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net } } -func streamFromTCPConnToWebsocketConn(websocketConn *websocket.Conn, br *bufio.Reader, errorChan chan error) { +func streamFromReaderToWebsocket(websocketConn *websocket.Conn, reader io.Reader, errorChan chan error) { for { - out := make([]byte, 2048) - _, err := br.Read(out) + out := make([]byte, readerBufferSize) + _, err := reader.Read(out) if err != nil { errorChan <- err break diff --git a/api/http/proxy/factory/docker.go b/api/http/proxy/factory/docker.go index 149c488d9..513b5731d 100644 --- a/api/http/proxy/factory/docker.go +++ b/api/http/proxy/factory/docker.go @@ -32,7 +32,7 @@ func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) ( } func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) { - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) endpoint.URL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port) } diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 4c12fb93b..09027d0c4 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -132,7 +132,7 @@ func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Res func (transport *Transport) executeDockerRequest(request *http.Request) (*http.Response, error) { response, err := transport.HTTPTransport.RoundTrip(request) - if transport.endpoint.Type != portainer.EdgeAgentEnvironment { + if transport.endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { return response, err } diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go index 6ebedbb9f..e3e4c1e3b 100644 --- a/api/http/proxy/factory/factory.go +++ b/api/http/proxy/factory/factory.go @@ -6,7 +6,11 @@ import ( "net/http/httputil" "net/url" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" + + "github.com/portainer/portainer/api/kubernetes/cli" + "github.com/portainer/portainer/api/docker" ) @@ -21,20 +25,24 @@ var extensionPorts = map[portainer.ExtensionID]string{ type ( // ProxyFactory is a factory to create reverse proxies to Docker endpoints and extensions ProxyFactory struct { - dataStore portainer.DataStore - signatureService portainer.DigitalSignatureService - reverseTunnelService portainer.ReverseTunnelService - dockerClientFactory *docker.ClientFactory + dataStore portainer.DataStore + signatureService portainer.DigitalSignatureService + reverseTunnelService portainer.ReverseTunnelService + dockerClientFactory *docker.ClientFactory + kubernetesClientFactory *cli.ClientFactory + kubernetesTokenCacheManager *kubernetes.TokenCacheManager } ) // NewProxyFactory returns a pointer to a new instance of a ProxyFactory -func NewProxyFactory(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory) *ProxyFactory { +func NewProxyFactory(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *ProxyFactory { return &ProxyFactory{ - dataStore: dataStore, - signatureService: signatureService, - reverseTunnelService: tunnelService, - dockerClientFactory: clientFactory, + dataStore: dataStore, + signatureService: signatureService, + reverseTunnelService: tunnelService, + dockerClientFactory: clientFactory, + kubernetesClientFactory: kubernetesClientFactory, + kubernetesTokenCacheManager: kubernetesTokenCacheManager, } } @@ -74,6 +82,8 @@ func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (htt switch endpoint.Type { case portainer.AzureEnvironment: return newAzureProxy(endpoint) + case portainer.EdgeAgentOnKubernetesEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.KubernetesLocalEnvironment: + return factory.newKubernetesProxy(endpoint) } return factory.newDockerProxy(endpoint) diff --git a/api/http/proxy/factory/kubernetes.go b/api/http/proxy/factory/kubernetes.go new file mode 100644 index 000000000..2cb09dc62 --- /dev/null +++ b/api/http/proxy/factory/kubernetes.go @@ -0,0 +1,109 @@ +package factory + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" +) + +func (factory *ProxyFactory) newKubernetesProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + switch endpoint.Type { + case portainer.KubernetesLocalEnvironment: + return factory.newKubernetesLocalProxy(endpoint) + case portainer.EdgeAgentOnKubernetesEnvironment: + return factory.newKubernetesEdgeHTTPProxy(endpoint) + } + + return factory.newKubernetesAgentHTTPSProxy(endpoint) +} + +func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + remoteURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return nil, err + } + + tokenCache := factory.kubernetesTokenCacheManager.CreateTokenCache(int(endpoint.ID)) + tokenManager, err := kubernetes.NewTokenManager(kubecli, factory.dataStore, tokenCache, true) + if err != nil { + return nil, err + } + + transport, err := kubernetes.NewLocalTransport(tokenManager) + if err != nil { + return nil, err + } + + proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) + proxy.Transport = transport + + return proxy, nil +} + +func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpoint.URL = fmt.Sprintf("http://localhost:%d", tunnel.Port) + + endpointURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return nil, err + } + + tokenCache := factory.kubernetesTokenCacheManager.CreateTokenCache(int(endpoint.ID)) + tokenManager, err := kubernetes.NewTokenManager(kubecli, factory.dataStore, tokenCache, false) + if err != nil { + return nil, err + } + + endpointURL.Scheme = "http" + proxy := newSingleHostReverseProxyWithHostHeader(endpointURL) + proxy.Transport = kubernetes.NewEdgeTransport(factory.reverseTunnelService, endpoint.ID, tokenManager) + + return proxy, nil +} + +func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + endpointURL := fmt.Sprintf("https://%s", endpoint.URL) + remoteURL, err := url.Parse(endpointURL) + if err != nil { + return nil, err + } + + remoteURL.Scheme = "https" + + kubecli, err := factory.kubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return nil, err + } + + tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) + if err != nil { + return nil, err + } + + tokenCache := factory.kubernetesTokenCacheManager.CreateTokenCache(int(endpoint.ID)) + tokenManager, err := kubernetes.NewTokenManager(kubecli, factory.dataStore, tokenCache, false) + if err != nil { + return nil, err + } + + proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) + proxy.Transport = kubernetes.NewAgentTransport(factory.signatureService, tlsConfig, tokenManager) + + return proxy, nil +} diff --git a/api/http/proxy/factory/kubernetes/token.go b/api/http/proxy/factory/kubernetes/token.go new file mode 100644 index 000000000..0e84f2d83 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/token.go @@ -0,0 +1,79 @@ +package kubernetes + +import ( + "io/ioutil" + "sync" + + portainer "github.com/portainer/portainer/api" +) + +const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" + +type tokenManager struct { + tokenCache *tokenCache + kubecli portainer.KubeClient + dataStore portainer.DataStore + mutex sync.Mutex + adminToken string +} + +// NewTokenManager returns a pointer to a new instance of tokenManager. +// If the useLocalAdminToken parameter is set to true, it will search for the local admin service account +// and associate it to the manager. +func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore, cache *tokenCache, setLocalAdminToken bool) (*tokenManager, error) { + tokenManager := &tokenManager{ + tokenCache: cache, + kubecli: kubecli, + dataStore: dataStore, + mutex: sync.Mutex{}, + adminToken: "", + } + + if setLocalAdminToken { + token, err := ioutil.ReadFile(defaultServiceAccountTokenFile) + if err != nil { + return nil, err + } + + tokenManager.adminToken = string(token) + } + + return tokenManager, nil +} + +func (manager *tokenManager) getAdminServiceAccountToken() string { + return manager.adminToken +} + +func (manager *tokenManager) getUserServiceAccountToken(userID int, username string) (string, error) { + manager.mutex.Lock() + defer manager.mutex.Unlock() + + token, ok := manager.tokenCache.getToken(userID) + if !ok { + memberships, err := manager.dataStore.TeamMembership().TeamMembershipsByUserID(portainer.UserID(userID)) + if err != nil { + return "", err + } + + teamIds := make([]int, 0) + for _, membership := range memberships { + teamIds = append(teamIds, int(membership.TeamID)) + } + + err = manager.kubecli.SetupUserServiceAccount(userID, username, teamIds) + if err != nil { + return "", err + } + + serviceAccountToken, err := manager.kubecli.GetServiceAccountBearerToken(userID, username) + if err != nil { + return "", err + } + + manager.tokenCache.addToken(userID, serviceAccountToken) + token = serviceAccountToken + } + + return token, nil +} diff --git a/api/http/proxy/factory/kubernetes/token_cache.go b/api/http/proxy/factory/kubernetes/token_cache.go new file mode 100644 index 000000000..552e6b3a1 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/token_cache.go @@ -0,0 +1,69 @@ +package kubernetes + +import ( + "strconv" + + "github.com/orcaman/concurrent-map" +) + +type ( + // TokenCacheManager represents a service used to manage multiple tokenCache objects. + TokenCacheManager struct { + tokenCaches cmap.ConcurrentMap + } + + tokenCache struct { + userTokenCache cmap.ConcurrentMap + } +) + +// NewTokenCacheManager returns a pointer to a new instance of TokenCacheManager +func NewTokenCacheManager() *TokenCacheManager { + return &TokenCacheManager{ + tokenCaches: cmap.New(), + } +} + +// CreateTokenCache will create a new tokenCache object, associate it to the manager map of caches +// and return a pointer to that tokenCache instance. +func (manager *TokenCacheManager) CreateTokenCache(endpointID int) *tokenCache { + tokenCache := newTokenCache() + + key := strconv.Itoa(endpointID) + manager.tokenCaches.Set(key, tokenCache) + + return tokenCache +} + +// RemoveUserFromCache will ensure that the specific userID is removed from all registered caches. +func (manager *TokenCacheManager) RemoveUserFromCache(userID int) { + for cache := range manager.tokenCaches.IterBuffered() { + cache.Val.(*tokenCache).removeToken(userID) + } +} + +func newTokenCache() *tokenCache { + return &tokenCache{ + userTokenCache: cmap.New(), + } +} + +func (cache *tokenCache) getToken(userID int) (string, bool) { + key := strconv.Itoa(userID) + token, ok := cache.userTokenCache.Get(key) + if ok { + return token.(string), true + } + + return "", false +} + +func (cache *tokenCache) addToken(userID int, token string) { + key := strconv.Itoa(userID) + cache.userTokenCache.Set(key, token) +} + +func (cache *tokenCache) removeToken(userID int) { + key := strconv.Itoa(userID) + cache.userTokenCache.Remove(key) +} diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go new file mode 100644 index 000000000..7837ce647 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -0,0 +1,156 @@ +package kubernetes + +import ( + "crypto/tls" + "fmt" + "net/http" + + "github.com/portainer/portainer/api/http/security" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" +) + +type ( + localTransport struct { + httpTransport *http.Transport + tokenManager *tokenManager + } + + agentTransport struct { + httpTransport *http.Transport + tokenManager *tokenManager + signatureService portainer.DigitalSignatureService + } + + edgeTransport struct { + httpTransport *http.Transport + tokenManager *tokenManager + reverseTunnelService portainer.ReverseTunnelService + endpointIdentifier portainer.EndpointID + } +) + +// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API +func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) { + config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true) + if err != nil { + return nil, err + } + + transport := &localTransport{ + httpTransport: &http.Transport{ + TLSClientConfig: config, + }, + tokenManager: tokenManager, + } + + return transport, nil +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + var token string + if tokenData.Role == portainer.AdministratorRole { + token = transport.tokenManager.getAdminServiceAccountToken() + } else { + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + if err != nil { + return nil, err + } + } + + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + return transport.httpTransport.RoundTrip(request) +} + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent +func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport { + transport := &agentTransport{ + httpTransport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + tokenManager: tokenManager, + signatureService: signatureService, + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + var token string + if tokenData.Role == portainer.AdministratorRole { + token = transport.tokenManager.getAdminServiceAccountToken() + } else { + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + if err != nil { + return nil, err + } + } + + request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) + + signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return nil, err + } + + request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey()) + request.Header.Set(portainer.PortainerAgentSignatureHeader, signature) + + return transport.httpTransport.RoundTrip(request) +} + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent +func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport { + transport := &edgeTransport{ + httpTransport: &http.Transport{}, + tokenManager: tokenManager, + reverseTunnelService: reverseTunnelService, + endpointIdentifier: endpointIdentifier, + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + var token string + if tokenData.Role == portainer.AdministratorRole { + token = transport.tokenManager.getAdminServiceAccountToken() + } else { + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + if err != nil { + return nil, err + } + } + + request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) + + response, err := transport.httpTransport.RoundTrip(request) + + if err == nil { + transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpointIdentifier) + } else { + transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpointIdentifier) + } + + return response, err +} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 336481f9c..10c2f4cf6 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -4,8 +4,12 @@ import ( "net/http" "strconv" - "github.com/orcaman/concurrent-map" - "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" + + cmap "github.com/orcaman/concurrent-map" + "github.com/portainer/portainer/api/kubernetes/cli" + + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/proxy/factory" ) @@ -23,12 +27,12 @@ type ( ) // NewManager initializes a new proxy Service -func NewManager(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory) *Manager { +func NewManager(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *Manager { return &Manager{ endpointProxies: cmap.New(), extensionProxies: cmap.New(), legacyExtensionProxies: cmap.New(), - proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory), + proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager), } } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 1fe96ad32..97abab731 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -125,7 +125,7 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp // AuthorizedEdgeEndpointOperation verifies that the request was received from a valid Edge endpoint func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error { - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment && endpoint.Type != portainer.EdgeAgentOnDockerEnvironment { return errors.New("Invalid endpoint type") } diff --git a/api/http/server.go b/api/http/server.go index b86378512..4693c5334 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -1,23 +1,20 @@ package http import ( + "net/http" + "path/filepath" "time" - "github.com/portainer/portainer/api/http/handler/edgegroups" - "github.com/portainer/portainer/api/http/handler/edgestacks" - "github.com/portainer/portainer/api/http/handler/edgetemplates" - "github.com/portainer/portainer/api/http/handler/endpointedge" - "github.com/portainer/portainer/api/http/handler/support" - "github.com/portainer/portainer/api/internal/snapshot" - - "github.com/portainer/portainer/api/http/handler/roles" - portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/handler" "github.com/portainer/portainer/api/http/handler/auth" "github.com/portainer/portainer/api/http/handler/dockerhub" + "github.com/portainer/portainer/api/http/handler/edgegroups" "github.com/portainer/portainer/api/http/handler/edgejobs" + "github.com/portainer/portainer/api/http/handler/edgestacks" + "github.com/portainer/portainer/api/http/handler/edgetemplates" + "github.com/portainer/portainer/api/http/handler/endpointedge" "github.com/portainer/portainer/api/http/handler/endpointgroups" "github.com/portainer/portainer/api/http/handler/endpointproxy" "github.com/portainer/portainer/api/http/handler/endpoints" @@ -26,9 +23,11 @@ import ( "github.com/portainer/portainer/api/http/handler/motd" "github.com/portainer/portainer/api/http/handler/registries" "github.com/portainer/portainer/api/http/handler/resourcecontrols" + "github.com/portainer/portainer/api/http/handler/roles" "github.com/portainer/portainer/api/http/handler/settings" "github.com/portainer/portainer/api/http/handler/stacks" "github.com/portainer/portainer/api/http/handler/status" + "github.com/portainer/portainer/api/http/handler/support" "github.com/portainer/portainer/api/http/handler/tags" "github.com/portainer/portainer/api/http/handler/teammemberships" "github.com/portainer/portainer/api/http/handler/teams" @@ -38,43 +37,43 @@ import ( "github.com/portainer/portainer/api/http/handler/webhooks" "github.com/portainer/portainer/api/http/handler/websocket" "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" - - "net/http" - "path/filepath" + "github.com/portainer/portainer/api/kubernetes/cli" ) // Server implements the portainer.Server interface type Server struct { - BindAddress string - AssetsPath string - Status *portainer.Status - ReverseTunnelService portainer.ReverseTunnelService - ExtensionManager portainer.ExtensionManager - ComposeStackManager portainer.ComposeStackManager - CryptoService portainer.CryptoService - SignatureService portainer.DigitalSignatureService - SnapshotService *snapshot.Service - Snapshotter portainer.Snapshotter - FileService portainer.FileService - DataStore portainer.DataStore - GitService portainer.GitService - JWTService portainer.JWTService - LDAPService portainer.LDAPService - SwarmStackManager portainer.SwarmStackManager - Handler *handler.Handler - SSL bool - SSLCert string - SSLKey string - DockerClientFactory *docker.ClientFactory + BindAddress string + AssetsPath string + Status *portainer.Status + ReverseTunnelService portainer.ReverseTunnelService + ExtensionManager portainer.ExtensionManager + ComposeStackManager portainer.ComposeStackManager + CryptoService portainer.CryptoService + SignatureService portainer.DigitalSignatureService + SnapshotService portainer.SnapshotService + FileService portainer.FileService + DataStore portainer.DataStore + GitService portainer.GitService + JWTService portainer.JWTService + LDAPService portainer.LDAPService + SwarmStackManager portainer.SwarmStackManager + Handler *handler.Handler + SSL bool + SSLCert string + SSLKey string + DockerClientFactory *docker.ClientFactory + KubernetesClientFactory *cli.ClientFactory + KubernetesDeployer portainer.KubernetesDeployer } // Start starts the HTTP server func (server *Server) Start() error { - proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory) - authorizationService := authorization.NewService(server.DataStore) + kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager() + proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager) rbacExtensionURL := proxyManager.GetExtensionURL(portainer.RBACExtension) requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService, rbacExtensionURL) @@ -88,6 +87,7 @@ func (server *Server) Start() error { authHandler.LDAPService = server.LDAPService authHandler.ProxyManager = proxyManager authHandler.AuthorizationService = authorizationService + authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager var roleHandler = roles.NewHandler(requestBouncer) roleHandler.DataStore = server.DataStore @@ -116,8 +116,9 @@ func (server *Server) Start() error { endpointHandler.AuthorizationService = authorizationService endpointHandler.FileService = server.FileService endpointHandler.ProxyManager = proxyManager + endpointHandler.SnapshotService = server.SnapshotService + endpointHandler.ProxyManager = proxyManager endpointHandler.ReverseTunnelService = server.ReverseTunnelService - endpointHandler.Snapshotter = server.Snapshotter var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer) endpointEdgeHandler.DataStore = server.DataStore @@ -163,6 +164,7 @@ func (server *Server) Start() error { stackHandler.FileService = server.FileService stackHandler.SwarmStackManager = server.SwarmStackManager stackHandler.ComposeStackManager = server.ComposeStackManager + stackHandler.KubernetesDeployer = server.KubernetesDeployer stackHandler.GitService = server.GitService var tagHandler = tags.NewHandler(requestBouncer) @@ -195,6 +197,7 @@ func (server *Server) Start() error { websocketHandler.DataStore = server.DataStore websocketHandler.SignatureService = server.SignatureService websocketHandler.ReverseTunnelService = server.ReverseTunnelService + websocketHandler.KubernetesClientFactory = server.KubernetesClientFactory var webhookHandler = webhooks.NewHandler(requestBouncer) webhookHandler.DataStore = server.DataStore diff --git a/api/internal/edge/edgegroup.go b/api/internal/edge/edgegroup.go index 968d5cff2..0b0140acb 100644 --- a/api/internal/edge/edgegroup.go +++ b/api/internal/edge/edgegroup.go @@ -13,7 +13,7 @@ func EdgeGroupRelatedEndpoints(edgeGroup *portainer.EdgeGroup, endpoints []porta endpointIDs := []portainer.EndpointID{} for _, endpoint := range endpoints { - if endpoint.Type != portainer.EdgeAgentEnvironment { + if endpoint.Type != portainer.EdgeAgentOnDockerEnvironment && endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment { continue } diff --git a/api/internal/snapshot/snapshot.go b/api/internal/snapshot/snapshot.go index f5c0c49a5..da9565cce 100644 --- a/api/internal/snapshot/snapshot.go +++ b/api/internal/snapshot/snapshot.go @@ -7,16 +7,19 @@ import ( "github.com/portainer/portainer/api" ) -// Service repesents a service to manage system snapshots +// Service repesents a service to manage endpoint snapshots. +// It provides an interface to start background snapshots as well as +// specific Docker/Kubernetes endpoint snapshot methods. type Service struct { dataStore portainer.DataStore refreshSignal chan struct{} snapshotIntervalInSeconds float64 - snapshotter portainer.Snapshotter + dockerSnapshotter portainer.DockerSnapshotter + kubernetesSnapshotter portainer.KubernetesSnapshotter } // NewService creates a new instance of a service -func NewService(snapshotInterval string, dataStore portainer.DataStore, snapshotter portainer.Snapshotter) (*Service, error) { +func NewService(snapshotInterval string, dataStore portainer.DataStore, dockerSnapshotter portainer.DockerSnapshotter, kubernetesSnapshotter portainer.KubernetesSnapshotter) (*Service, error) { snapshotFrequency, err := time.ParseDuration(snapshotInterval) if err != nil { return nil, err @@ -25,11 +28,12 @@ func NewService(snapshotInterval string, dataStore portainer.DataStore, snapshot return &Service{ dataStore: dataStore, snapshotIntervalInSeconds: snapshotFrequency.Seconds(), - snapshotter: snapshotter, + dockerSnapshotter: dockerSnapshotter, + kubernetesSnapshotter: kubernetesSnapshotter, }, nil } -// Start starts the service +// Start will start a background routine to execute periodic snapshots of endpoints func (service *Service) Start() { if service.refreshSignal != nil { return @@ -62,6 +66,55 @@ func (service *Service) SetSnapshotInterval(snapshotInterval string) error { return nil } +// SupportDirectSnapshot checks whether an endpoint can be used to trigger a direct a snapshot. +// It is mostly true for all endpoints except Edge and Azure endpoints. +func SupportDirectSnapshot(endpoint *portainer.Endpoint) bool { + switch endpoint.Type { + case portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment, portainer.AzureEnvironment: + return false + } + return true +} + +// SnapshotEndpoint will create a snapshot of the endpoint based on the endpoint type. +// If the snapshot is a success, it will be associated to the endpoint. +func (service *Service) SnapshotEndpoint(endpoint *portainer.Endpoint) error { + switch endpoint.Type { + case portainer.AzureEnvironment: + return nil + case portainer.KubernetesLocalEnvironment, portainer.AgentOnKubernetesEnvironment, portainer.EdgeAgentOnKubernetesEnvironment: + return service.snapshotKubernetesEndpoint(endpoint) + } + + return service.snapshotDockerEndpoint(endpoint) +} + +func (service *Service) snapshotKubernetesEndpoint(endpoint *portainer.Endpoint) error { + snapshot, err := service.kubernetesSnapshotter.CreateSnapshot(endpoint) + if err != nil { + return err + } + + if snapshot != nil { + endpoint.Kubernetes.Snapshots = []portainer.KubernetesSnapshot{*snapshot} + } + + return nil +} + +func (service *Service) snapshotDockerEndpoint(endpoint *portainer.Endpoint) error { + snapshot, err := service.dockerSnapshotter.CreateSnapshot(endpoint) + if err != nil { + return err + } + + if snapshot != nil { + endpoint.Snapshots = []portainer.DockerSnapshot{*snapshot} + } + + return nil +} + func (service *Service) startSnapshotLoop() error { ticker := time.NewTicker(time.Duration(service.snapshotIntervalInSeconds) * time.Second) go func() { @@ -96,11 +149,11 @@ func (service *Service) snapshotEndpoints() error { } for _, endpoint := range endpoints { - if endpoint.Type == portainer.EdgeAgentEnvironment { + if !SupportDirectSnapshot(&endpoint) { continue } - snapshot, snapshotError := service.snapshotter.CreateSnapshot(&endpoint) + snapshotError := service.SnapshotEndpoint(&endpoint) latestEndpointReference, err := service.dataStore.Endpoint().Endpoint(endpoint.ID) if latestEndpointReference == nil { @@ -114,9 +167,8 @@ func (service *Service) snapshotEndpoints() error { latestEndpointReference.Status = portainer.EndpointStatusDown } - if snapshot != nil { - latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} - } + latestEndpointReference.Snapshots = endpoint.Snapshots + latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots err = service.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if err != nil { diff --git a/api/kubernetes.go b/api/kubernetes.go new file mode 100644 index 000000000..6ca8a3a78 --- /dev/null +++ b/api/kubernetes.go @@ -0,0 +1,11 @@ +package portainer + +func KubernetesDefault() KubernetesData { + return KubernetesData{ + Configuration: KubernetesConfiguration{ + UseLoadBalancer: false, + StorageClasses: []KubernetesStorageClassConfig{}, + }, + Snapshots: []KubernetesSnapshot{}, + } +} diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go new file mode 100644 index 000000000..bd79d3fce --- /dev/null +++ b/api/kubernetes/cli/access.go @@ -0,0 +1,86 @@ +package cli + +import ( + "encoding/json" + + portainer "github.com/portainer/portainer/api" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ( + accessPolicies struct { + UserAccessPolicies portainer.UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies portainer.TeamAccessPolicies `json:"TeamAccessPolicies"` + } + + namespaceAccessPolicies map[string]accessPolicies +) + +func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error { + configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + accessData := configMap.Data[portainerConfigMapAccessPoliciesKey] + + var accessPolicies namespaceAccessPolicies + err = json.Unmarshal([]byte(accessData), &accessPolicies) + if err != nil { + return err + } + + namespaces, err := kcl.cli.CoreV1().Namespaces().List(metav1.ListOptions{}) + if err != nil { + return err + } + + for _, namespace := range namespaces.Items { + if namespace.Name == defaultNamespace { + continue + } + + policies, ok := accessPolicies[namespace.Name] + if !ok { + err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) + if err != nil { + return err + } + continue + } + + if !hasUserAccessToNamespace(userID, teamIDs, policies) { + err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) + if err != nil { + return err + } + continue + } + + err = kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + } + + return nil +} + +func hasUserAccessToNamespace(userID int, teamIDs []int, policies accessPolicies) bool { + _, userAccess := policies.UserAccessPolicies[portainer.UserID(userID)] + if userAccess { + return true + } + + for _, teamID := range teamIDs { + _, teamAccess := policies.TeamAccessPolicies[portainer.TeamID(teamID)] + if teamAccess { + return true + } + } + + return false +} diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go new file mode 100644 index 000000000..b87faac92 --- /dev/null +++ b/api/kubernetes/cli/client.go @@ -0,0 +1,145 @@ +package cli + +import ( + "errors" + "fmt" + "net/http" + "strconv" + + cmap "github.com/orcaman/concurrent-map" + + portainer "github.com/portainer/portainer/api" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +type ( + // ClientFactory is used to create Kubernetes clients + ClientFactory struct { + reverseTunnelService portainer.ReverseTunnelService + signatureService portainer.DigitalSignatureService + endpointClients cmap.ConcurrentMap + } + + // KubeClient represent a service used to execute Kubernetes operations + KubeClient struct { + cli *kubernetes.Clientset + } +) + +// NewClientFactory returns a new instance of a ClientFactory +func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *ClientFactory { + return &ClientFactory{ + signatureService: signatureService, + reverseTunnelService: reverseTunnelService, + endpointClients: cmap.New(), + } +} + +// GetKubeClient checks if an existing client is already registered for the endpoint and returns it if one is found. +// If no client is registered, it will create a new client, register it, and returns it. +func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { + key := strconv.Itoa(int(endpoint.ID)) + client, ok := factory.endpointClients.Get(key) + if !ok { + client, err := factory.createKubeClient(endpoint) + if err != nil { + return nil, err + } + + factory.endpointClients.Set(key, client) + return client, nil + } + + return client.(portainer.KubeClient), nil +} + +func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { + cli, err := factory.CreateClient(endpoint) + if err != nil { + return nil, err + } + + kubecli := &KubeClient{ + cli: cli, + } + + return kubecli, nil +} + +// CreateClient returns a pointer to a new Clientset instance +func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) { + switch endpoint.Type { + case portainer.KubernetesLocalEnvironment: + return buildLocalClient() + case portainer.AgentOnKubernetesEnvironment: + return factory.buildAgentClient(endpoint) + case portainer.EdgeAgentOnKubernetesEnvironment: + return factory.buildEdgeClient(endpoint) + } + + return nil, errors.New("unsupported endpoint type") +} + +type agentHeaderRoundTripper struct { + signatureHeader string + publicKeyHeader string + + roundTripper http.RoundTripper +} + +// RoundTrip is the implementation of the http.RoundTripper interface. +// It decorates the request with specific agent headers +func (rt *agentHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Add(portainer.PortainerAgentPublicKeyHeader, rt.publicKeyHeader) + req.Header.Add(portainer.PortainerAgentSignatureHeader, rt.signatureHeader) + + return rt.roundTripper.RoundTrip(req) +} + +func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) { + endpointURL := fmt.Sprintf("https://%s/kubernetes", endpoint.URL) + signature, err := factory.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return nil, err + } + + config, err := clientcmd.BuildConfigFromFlags(endpointURL, "") + if err != nil { + return nil, err + } + config.Insecure = true + + config.Wrap(func(rt http.RoundTripper) http.RoundTripper { + return &agentHeaderRoundTripper{ + signatureHeader: signature, + publicKeyHeader: factory.signatureService.EncodedPublicKey(), + roundTripper: rt, + } + }) + + return kubernetes.NewForConfig(config) +} + +func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) { + tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpointURL := fmt.Sprintf("http://localhost:%d/kubernetes", tunnel.Port) + + config, err := clientcmd.BuildConfigFromFlags(endpointURL, "") + if err != nil { + return nil, err + } + config.Insecure = true + + return kubernetes.NewForConfig(config) +} + +func buildLocalClient() (*kubernetes.Clientset, error) { + config, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + + return kubernetes.NewForConfig(config) +} diff --git a/api/kubernetes/cli/exec.go b/api/kubernetes/cli/exec.go new file mode 100644 index 000000000..1716b10e6 --- /dev/null +++ b/api/kubernetes/cli/exec.go @@ -0,0 +1,57 @@ +package cli + +import ( + "errors" + "io" + + "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + utilexec "k8s.io/client-go/util/exec" +) + +// StartExecProcess will start an exec process inside a container located inside a pod inside a specific namespace +// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write +// to the stdout parameter. +// This function only works against a local endpoint using an in-cluster config. +func (kcl *KubeClient) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error { + config, err := rest.InClusterConfig() + if err != nil { + return err + } + + req := kcl.cli.CoreV1().RESTClient(). + Post(). + Resource("pods"). + Name(podName). + Namespace(namespace). + SubResource("exec") + + req.VersionedParams(&v1.PodExecOptions{ + Container: containerName, + Command: command, + Stdin: true, + Stdout: true, + Stderr: true, + TTY: true, + }, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL()) + if err != nil { + return err + } + + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: stdin, + Stdout: stdout, + Tty: true, + }) + if err != nil { + if _, ok := err.(utilexec.ExitError); !ok { + return errors.New("unable to start exec process") + } + } + + return nil +} diff --git a/api/kubernetes/cli/naming.go b/api/kubernetes/cli/naming.go new file mode 100644 index 000000000..9c101e5bd --- /dev/null +++ b/api/kubernetes/cli/naming.go @@ -0,0 +1,26 @@ +package cli + +import "fmt" + +const ( + defaultNamespace = "default" + portainerNamespace = "portainer" + portainerUserCRName = "portainer-cr-user" + portainerUserCRBName = "portainer-crb-user" + portainerUserServiceAccountPrefix = "portainer-sa-user" + portainerRBPrefix = "portainer-rb" + portainerConfigMapName = "portainer-config" + portainerConfigMapAccessPoliciesKey = "NamespaceAccessPolicies" +) + +func userServiceAccountName(userID int, username string) string { + return fmt.Sprintf("%s-%d-%s", portainerUserServiceAccountPrefix, userID, username) +} + +func userServiceAccountTokenSecretName(serviceAccountName string) string { + return fmt.Sprintf("%s-secret", serviceAccountName) +} + +func namespaceClusterRoleBindingName(namespace string) string { + return fmt.Sprintf("%s-%s", portainerRBPrefix, namespace) +} diff --git a/api/kubernetes/cli/role.go b/api/kubernetes/cli/role.go new file mode 100644 index 000000000..e19f1f22a --- /dev/null +++ b/api/kubernetes/cli/role.go @@ -0,0 +1,33 @@ +package cli + +import ( + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule { + return []rbacv1.PolicyRule{ + { + Verbs: []string{"list"}, + Resources: []string{"namespaces", "nodes"}, + APIGroups: []string{""}, + }, + } +} + +func (kcl *KubeClient) createPortainerUserClusterRole() error { + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: portainerUserCRName, + }, + Rules: getPortainerUserDefaultPolicies(), + } + + _, err := kcl.cli.RbacV1().ClusterRoles().Create(clusterRole) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} diff --git a/api/kubernetes/cli/secret.go b/api/kubernetes/cli/secret.go new file mode 100644 index 000000000..87ba35f53 --- /dev/null +++ b/api/kubernetes/cli/secret.go @@ -0,0 +1,74 @@ +package cli + +import ( + "errors" + "time" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" + + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (kcl *KubeClient) createServiceAccountToken(serviceAccountName string) error { + serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName) + + serviceAccountSecret := &v1.Secret{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountSecretName, + Annotations: map[string]string{ + "kubernetes.io/service-account.name": serviceAccountName, + }, + }, + Type: "kubernetes.io/service-account-token", + } + + _, err := kcl.cli.CoreV1().Secrets(portainerNamespace).Create(serviceAccountSecret) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (kcl *KubeClient) getServiceAccountToken(serviceAccountName string) (string, error) { + serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName) + + secret, err := kcl.cli.CoreV1().Secrets(portainerNamespace).Get(serviceAccountSecretName, metav1.GetOptions{}) + if err != nil { + return "", err + } + + // API token secret is populated asynchronously. + // Is it created by the controller and will depend on the environment/secret-store: + // https://github.com/kubernetes/kubernetes/issues/67882#issuecomment-422026204 + // as a work-around, we wait for up to 5 seconds for the secret to be populated. + timeout := time.After(5 * time.Second) + searchingForSecret := true + for searchingForSecret { + select { + case <-timeout: + return "", errors.New("unable to find secret token associated to user service account (timeout)") + default: + secret, err = kcl.cli.CoreV1().Secrets(portainerNamespace).Get(serviceAccountSecretName, metav1.GetOptions{}) + if err != nil { + return "", err + } + + if len(secret.Data) > 0 { + searchingForSecret = false + break + } + + time.Sleep(1 * time.Second) + } + } + + secretTokenData, ok := secret.Data["token"] + if ok { + return string(secretTokenData), nil + } + + return "", errors.New("unable to find secret token associated to user service account") +} diff --git a/api/kubernetes/cli/service_account.go b/api/kubernetes/cli/service_account.go new file mode 100644 index 000000000..1af1b47f9 --- /dev/null +++ b/api/kubernetes/cli/service_account.go @@ -0,0 +1,182 @@ +package cli + +import ( + "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GetServiceAccountBearerToken returns the ServiceAccountToken associated to the specified user. +func (kcl *KubeClient) GetServiceAccountBearerToken(userID int, username string) (string, error) { + serviceAccountName := userServiceAccountName(userID, username) + + return kcl.getServiceAccountToken(serviceAccountName) +} + +// SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes +// cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user. +//It will also create required default RoleBinding and ClusterRoleBinding rules. +func (kcl *KubeClient) SetupUserServiceAccount(userID int, username string, teamIDs []int) error { + serviceAccountName := userServiceAccountName(userID, username) + + err := kcl.ensureRequiredResourcesExist() + if err != nil { + return err + } + + err = kcl.ensureServiceAccountForUserExists(serviceAccountName) + if err != nil { + return err + } + + return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName) +} + +func (kcl *KubeClient) ensureRequiredResourcesExist() error { + return kcl.createPortainerUserClusterRole() +} + +func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName string) error { + err := kcl.createUserServiceAccount(portainerNamespace, serviceAccountName) + if err != nil { + return err + } + + err = kcl.createServiceAccountToken(serviceAccountName) + if err != nil { + return err + } + + err = kcl.ensureServiceAccountHasPortainerUserClusterRole(serviceAccountName) + if err != nil { + return err + } + + return kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace) +} + +func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error { + serviceAccount := &v1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + }, + } + + _, err := kcl.cli.CoreV1().ServiceAccounts(namespace).Create(serviceAccount) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (kcl *KubeClient) ensureServiceAccountHasPortainerUserClusterRole(serviceAccountName string) error { + clusterRoleBinding, err := kcl.cli.RbacV1().ClusterRoleBindings().Get(portainerUserCRBName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + clusterRoleBinding = &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: portainerUserCRBName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: portainerNamespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: portainerUserCRName, + }, + } + + _, err := kcl.cli.RbacV1().ClusterRoleBindings().Create(clusterRoleBinding) + return err + } else if err != nil { + return err + } + + for _, subject := range clusterRoleBinding.Subjects { + if subject.Name == serviceAccountName { + return nil + } + } + + clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: portainerNamespace, + }) + + _, err = kcl.cli.RbacV1().ClusterRoleBindings().Update(clusterRoleBinding) + return err +} + +func (kcl *KubeClient) removeNamespaceAccessForServiceAccount(serviceAccountName, namespace string) error { + roleBindingName := namespaceClusterRoleBindingName(namespace) + + roleBinding, err := kcl.cli.RbacV1().RoleBindings(namespace).Get(roleBindingName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + updatedSubjects := roleBinding.Subjects[:0] + + for _, subject := range roleBinding.Subjects { + if subject.Name != serviceAccountName { + updatedSubjects = append(updatedSubjects, subject) + } + } + + roleBinding.Subjects = updatedSubjects + + _, err = kcl.cli.RbacV1().RoleBindings(namespace).Update(roleBinding) + return err +} + +func (kcl *KubeClient) ensureNamespaceAccessForServiceAccount(serviceAccountName, namespace string) error { + roleBindingName := namespaceClusterRoleBindingName(namespace) + + roleBinding, err := kcl.cli.RbacV1().RoleBindings(namespace).Get(roleBindingName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + roleBinding = &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: roleBindingName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: portainerNamespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + Kind: "ClusterRole", + Name: "edit", + }, + } + + _, err = kcl.cli.RbacV1().RoleBindings(namespace).Create(roleBinding) + return err + } else if err != nil { + return err + } + + for _, subject := range roleBinding.Subjects { + if subject.Name == serviceAccountName { + return nil + } + } + + roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ + Kind: "ServiceAccount", + Name: serviceAccountName, + Namespace: portainerNamespace, + }) + + _, err = kcl.cli.RbacV1().RoleBindings(namespace).Update(roleBinding) + return err +} diff --git a/api/kubernetes/snapshot.go b/api/kubernetes/snapshot.go new file mode 100644 index 000000000..8382d95ab --- /dev/null +++ b/api/kubernetes/snapshot.go @@ -0,0 +1,83 @@ +package kubernetes + +import ( + "log" + "time" + + "github.com/portainer/portainer/api/kubernetes/cli" + + portainer "github.com/portainer/portainer/api" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type Snapshotter struct { + clientFactory *cli.ClientFactory +} + +// NewSnapshotter returns a new Snapshotter instance +func NewSnapshotter(clientFactory *cli.ClientFactory) *Snapshotter { + return &Snapshotter{ + clientFactory: clientFactory, + } +} + +// CreateSnapshot creates a snapshot of a specific Kubernetes endpoint +func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.KubernetesSnapshot, error) { + client, err := snapshotter.clientFactory.CreateClient(endpoint) + if err != nil { + return nil, err + } + + return snapshot(client, endpoint) +} + +func snapshot(cli *kubernetes.Clientset, endpoint *portainer.Endpoint) (*portainer.KubernetesSnapshot, error) { + res := cli.RESTClient().Get().AbsPath("/healthz").Do() + if res.Error() != nil { + return nil, res.Error() + } + + snapshot := &portainer.KubernetesSnapshot{} + + err := snapshotVersion(snapshot, cli) + if err != nil { + log.Printf("[WARN] [kubernetes,snapshot] [message: unable to snapshot cluster version] [endpoint: %s] [err: %s]", endpoint.Name, err) + } + + err = snapshotNodes(snapshot, cli) + if err != nil { + log.Printf("[WARN] [kubernetes,snapshot] [message: unable to snapshot cluster nodes] [endpoint: %s] [err: %s]", endpoint.Name, err) + } + + snapshot.Time = time.Now().Unix() + return snapshot, nil +} + +func snapshotVersion(snapshot *portainer.KubernetesSnapshot, cli *kubernetes.Clientset) error { + versionInfo, err := cli.ServerVersion() + if err != nil { + return err + } + + snapshot.KubernetesVersion = versionInfo.GitVersion + return nil +} + +func snapshotNodes(snapshot *portainer.KubernetesSnapshot, cli *kubernetes.Clientset) error { + nodeList, err := cli.CoreV1().Nodes().List(metav1.ListOptions{}) + if err != nil { + return err + } + + var totalCPUs, totalMemory int64 + for _, node := range nodeList.Items { + totalCPUs += node.Status.Capacity.Cpu().Value() + totalMemory += node.Status.Capacity.Memory().Value() + } + + snapshot.TotalCPU = totalCPUs + snapshot.TotalMemory = totalMemory + snapshot.NodeCount = len(nodeList.Items) + return nil +} diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go index d3bf546c3..ec885b65b 100644 --- a/api/libcompose/compose_stack.go +++ b/api/libcompose/compose_stack.go @@ -37,7 +37,7 @@ func NewComposeStackManager(dataPath string, reverseTunnelService portainer.Reve func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (client.Factory, error) { endpointURL := endpoint.URL - if endpoint.Type == portainer.EdgeAgentEnvironment { + if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port) } diff --git a/api/portainer.go b/api/portainer.go index 05edb3866..582e7cf84 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1,6 +1,7 @@ package portainer import ( + "io" "time" ) @@ -60,42 +61,6 @@ type ( SnapshotInterval *string } - // CLIService represents a service for managing CLI - CLIService interface { - ParseFlags(version string) (*CLIFlags, error) - ValidateFlags(flags *CLIFlags) error - } - - // DataStore defines the interface to manage the data - DataStore interface { - Open() error - Init() error - Close() error - IsNew() bool - MigrateData() error - - DockerHub() DockerHubService - EdgeGroup() EdgeGroupService - EdgeJob() EdgeJobService - EdgeStack() EdgeStackService - Endpoint() EndpointService - EndpointGroup() EndpointGroupService - EndpointRelation() EndpointRelationService - Extension() ExtensionService - Registry() RegistryService - ResourceControl() ResourceControlService - Role() RoleService - Settings() SettingsService - Stack() StackService - Tag() TagService - TeamMembership() TeamMembershipService - Team() TeamService - TunnelServer() TunnelServerService - User() UserService - Version() VersionService - Webhook() WebhookService - } - // DockerHub represents all the required information to connect and use the // Docker Hub DockerHub struct { @@ -104,6 +69,34 @@ type ( Password string `json:"Password,omitempty"` } + // DockerSnapshot represents a snapshot of a specific Docker endpoint at a specific time + DockerSnapshot struct { + Time int64 `json:"Time"` + DockerVersion string `json:"DockerVersion"` + Swarm bool `json:"Swarm"` + TotalCPU int `json:"TotalCPU"` + TotalMemory int64 `json:"TotalMemory"` + RunningContainerCount int `json:"RunningContainerCount"` + StoppedContainerCount int `json:"StoppedContainerCount"` + HealthyContainerCount int `json:"HealthyContainerCount"` + UnhealthyContainerCount int `json:"UnhealthyContainerCount"` + VolumeCount int `json:"VolumeCount"` + ImageCount int `json:"ImageCount"` + ServiceCount int `json:"ServiceCount"` + StackCount int `json:"StackCount"` + SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"` + } + + // DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API + DockerSnapshotRaw struct { + Containers interface{} `json:"Containers"` + Volumes interface{} `json:"Volumes"` + Networks interface{} `json:"Networks"` + Images interface{} `json:"Images"` + Info interface{} `json:"Info"` + Version interface{} `json:"Version"` + } + // EdgeGroup represents an Edge group EdgeGroup struct { ID EdgeGroupID `json:"Id"` @@ -191,12 +184,13 @@ type ( AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` TagIDs []TagID `json:"TagIds"` Status EndpointStatus `json:"Status"` - Snapshots []Snapshot `json:"Snapshots"` + Snapshots []DockerSnapshot `json:"Snapshots"` UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` EdgeID string `json:"EdgeID,omitempty"` EdgeKey string `json:"EdgeKey"` EdgeCheckinInterval int `json:"EdgeCheckinInterval"` + Kubernetes KubernetesData `json:"Kubernetes"` // Deprecated fields // Deprecated in DBVersion == 4 @@ -257,7 +251,6 @@ type ( EndpointStatus int // EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file - // // Deprecated EndpointSyncJob struct{} @@ -303,6 +296,33 @@ type ( // JobType represents a job type JobType int + // KubernetesData contains all the Kubernetes related endpoint information + KubernetesData struct { + Snapshots []KubernetesSnapshot `json:"Snapshots"` + Configuration KubernetesConfiguration `json:"Configuration"` + } + + // KubernetesSnapshot represents a snapshot of a specific Kubernetes endpoint at a specific time + KubernetesSnapshot struct { + Time int64 `json:"Time"` + KubernetesVersion string `json:"KubernetesVersion"` + NodeCount int `json:"NodeCount"` + TotalCPU int64 `json:"TotalCPU"` + TotalMemory int64 `json:"TotalMemory"` + } + + // KubernetesConfiguration represents the configuration of a Kubernetes endpoint + KubernetesConfiguration struct { + UseLoadBalancer bool `json:"UseLoadBalancer"` + StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` + } + + // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration + KubernetesStorageClassConfig struct { + Name string `json:"Name"` + AccessModes []string `json:"AccessModes"` + } + // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server LDAPGroupSearchSettings struct { GroupBaseDN string `json:"GroupBaseDN"` @@ -488,33 +508,8 @@ type ( DisplayExternalContributors bool } - // Snapshot represents a snapshot of a specific endpoint at a specific time - Snapshot struct { - Time int64 `json:"Time"` - DockerVersion string `json:"DockerVersion"` - Swarm bool `json:"Swarm"` - TotalCPU int `json:"TotalCPU"` - TotalMemory int64 `json:"TotalMemory"` - RunningContainerCount int `json:"RunningContainerCount"` - StoppedContainerCount int `json:"StoppedContainerCount"` - HealthyContainerCount int `json:"HealthyContainerCount"` - UnhealthyContainerCount int `json:"UnhealthyContainerCount"` - VolumeCount int `json:"VolumeCount"` - ImageCount int `json:"ImageCount"` - ServiceCount int `json:"ServiceCount"` - StackCount int `json:"StackCount"` - SnapshotRaw SnapshotRaw `json:"SnapshotRaw"` - } - - // SnapshotRaw represents all the information related to a snapshot as returned by the Docker API - SnapshotRaw struct { - Containers interface{} `json:"Containers"` - Volumes interface{} `json:"Volumes"` - Networks interface{} `json:"Networks"` - Images interface{} `json:"Images"` - Info interface{} `json:"Info"` - Version interface{} `json:"Version"` - } + // SnapshotJob represents a scheduled job that can create endpoint snapshots + SnapshotJob struct{} // Stack represents a Docker stack created via docker stack deploy Stack struct { @@ -733,6 +728,12 @@ type ( // WebhookType represents the type of resource a webhook is related to WebhookType int + // CLIService represents a service for managing CLI + CLIService interface { + ParseFlags(version string) (*CLIFlags, error) + ValidateFlags(flags *CLIFlags) error + } + // ComposeStackManager represents a service to manage Compose stacks ComposeStackManager interface { Up(stack *Stack, endpoint *Endpoint) error @@ -745,6 +746,36 @@ type ( CompareHashAndData(hash string, data string) error } + // DataStore defines the interface to manage the data + DataStore interface { + Open() error + Init() error + Close() error + IsNew() bool + MigrateData() error + + DockerHub() DockerHubService + EdgeGroup() EdgeGroupService + EdgeJob() EdgeJobService + EdgeStack() EdgeStackService + Endpoint() EndpointService + EndpointGroup() EndpointGroupService + EndpointRelation() EndpointRelationService + Extension() ExtensionService + Registry() RegistryService + ResourceControl() ResourceControlService + Role() RoleService + Settings() SettingsService + Stack() StackService + Tag() TagService + TeamMembership() TeamMembershipService + Team() TeamService + TunnelServer() TunnelServerService + User() UserService + Version() VersionService + Webhook() WebhookService + } + // DigitalSignatureService represents a service to manage digital signatures DigitalSignatureService interface { ParseKeyPair(private, public []byte) error @@ -760,6 +791,11 @@ type ( UpdateDockerHub(registry *DockerHub) error } + // DockerSnapshotter represents a service used to create Docker endpoint snapshots + DockerSnapshotter interface { + CreateSnapshot(endpoint *Endpoint) (*DockerSnapshot, error) + } + // EdgeGroupService represents a service to manage Edge groups EdgeGroupService interface { EdgeGroups() ([]EdgeGroup, error) @@ -876,6 +912,23 @@ type ( SetUserSessionDuration(userSessionDuration time.Duration) } + // KubeClient represents a service used to query a Kubernetes environment + KubeClient interface { + SetupUserServiceAccount(userID int, username string, teamIDs []int) error + GetServiceAccountBearerToken(userID int, username string) (string, error) + StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error + } + + // KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint + KubernetesDeployer interface { + Deploy(endpoint *Endpoint, data string, composeFormat bool, namespace string) ([]byte, error) + } + + // KubernetesSnapshotter represents a service used to create Kubernetes endpoint snapshots + KubernetesSnapshotter interface { + CreateSnapshot(endpoint *Endpoint) (*KubernetesSnapshot, error) + } + // LDAPService represents a service used to authenticate users against a LDAP/AD LDAPService interface { AuthenticateUser(username, password string, settings *LDAPSettings) error @@ -904,7 +957,7 @@ type ( // ReverseTunnelService represensts a service used to manage reverse tunnel connections. ReverseTunnelService interface { - StartTunnelServer(addr, port string, snapshotter Snapshotter) error + StartTunnelServer(addr, port string, snapshotService SnapshotService) error GenerateEdgeKey(url, host string, endpointIdentifier int) string SetTunnelStatusToActive(endpointID EndpointID) SetTunnelStatusToRequired(endpointID EndpointID) error @@ -933,11 +986,6 @@ type ( Start() error } - // Snapshotter represents a service used to create endpoint snapshots - Snapshotter interface { - CreateSnapshot(endpoint *Endpoint) (*Snapshot, error) - } - // StackService represents a service for managing stack data StackService interface { Stack(ID StackID) (*Stack, error) @@ -949,6 +997,13 @@ type ( GetNextIdentifier() int } + // StackService represents a service for managing endpoint snapshots + SnapshotService interface { + Start() + SetSnapshotInterval(snapshotInterval string) error + SnapshotEndpoint(endpoint *Endpoint) error + } + // SwarmStackManager represents a service to manage Swarm stacks SwarmStackManager interface { Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) @@ -1048,6 +1103,8 @@ const ( PortainerAgentSignatureHeader = "X-PortainerAgent-Signature" // PortainerAgentPublicKeyHeader represent the name of the header containing the public key PortainerAgentPublicKeyHeader = "X-PortainerAgent-PublicKey" + // PortainerAgentKubernetesSATokenHeader represent the name of the header containing a Kubernetes SA token + PortainerAgentKubernetesSATokenHeader = "X-PortainerAgent-SA-Token" // PortainerAgentSignatureMessage represents the message used to create a digital signature // to be used when communicating with an agent PortainerAgentSignatureMessage = "Portainer-App" @@ -1115,8 +1172,14 @@ const ( AgentOnDockerEnvironment // AzureEnvironment represents an endpoint connected to an Azure environment AzureEnvironment - // EdgeAgentEnvironment represents an endpoint connected to an Edge agent - EdgeAgentEnvironment + // EdgeAgentOnDockerEnvironment represents an endpoint connected to an Edge agent deployed on a Docker environment + EdgeAgentOnDockerEnvironment + // KubernetesLocalEnvironment represents an endpoint connected to a local Kubernetes environment + KubernetesLocalEnvironment + // AgentOnKubernetesEnvironment represents an endpoint connected to a Portainer agent deployed on a Kubernetes environment + AgentOnKubernetesEnvironment + // EdgeAgentOnKubernetesEnvironment represents an endpoint connected to an Edge agent deployed on a Kubernetes environment + EdgeAgentOnKubernetesEnvironment ) const ( @@ -1185,6 +1248,8 @@ const ( DockerSwarmStack // DockerComposeStack represents a stack managed via docker-compose DockerComposeStack + // KubernetesStack represents a stack managed via kubectl + KubernetesStack ) const ( diff --git a/app/__module.js b/app/__module.js index 0aed3bfa1..886251dfe 100644 --- a/app/__module.js +++ b/app/__module.js @@ -30,6 +30,7 @@ angular.module('portainer', [ 'portainer.agent', 'portainer.azure', 'portainer.docker', + 'portainer.kubernetes', 'portainer.edge', 'portainer.extensions', 'portainer.integrations', diff --git a/app/assets/css/app.css b/app/assets/css/app.css index f57e3802f..b521dd890 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -118,7 +118,6 @@ a[ng-click] { .fa.tooltip-icon { margin-left: 5px; font-size: 1.3em; - color: #337ab7; } .fa.green-icon { @@ -151,6 +150,11 @@ a[ng-click] { margin-right: 5px; } +.widget .widget-body table tbody .label-margins { + margin-left: 5px; + margin-right: 0; +} + .widget .widget-body table tbody .fit-text-size { font-size: 90% !important; } @@ -888,6 +892,17 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { vertical-align: middle; } +.striked::after { + border-bottom: 0.2em solid #777777; + content: ''; + left: 0; + margin-top: calc(0.2em / 2 * -1); + position: absolute; + right: 0; + top: 50%; + z-index: 2; +} + /*bootbox override*/ .modal-open { padding-right: 0 !important; @@ -966,3 +981,40 @@ json-tree .branch-preview { opacity: 0.5; } /* !json-tree override */ + +/* uib-progressbar override */ +.progress-bar { + color: #4e4e4e; +} +/* !uib-progressbar override */ + +.loading-view-area { + height: 85%; + display: flex; + align-items: center; +} + +/* bootstrap extend */ +.input-xs { + height: 22px; + padding: 2px 5px; + font-size: 12px; + line-height: 1.5; /* If Placeholder of the input is moved up, rem/modify this. */ + border-radius: 3px; +} +/* !bootstrap extend */ + +/* spinkit override */ +.sk-fold { + width: 57px; + height: 57px; +} + +.sk-fold-cube { + background-color: white; +} + +.sk-fold-cube:before { + background-color: #337ab7; +} +/* !spinkit override */ diff --git a/app/assets/images/kubernetes_endpoint.png b/app/assets/images/kubernetes_endpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..3a85817cc4b3b749446560712627f3dfab6df825 GIT binary patch literal 4079 zcmV$y00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru;|T%=3m=he!TJCI4}3{P zK~!ko)th;IR8^M8zvtDfC6zrPB)Fh}LZc!y!=mES3W@^_4GPljN3l`R)(@i4qN4UV zC<=`PSF6ACs;XL3h2^kPs<%?gcBbbgX9B2*G<+TjECF&)mYY1ZxPpU)K@2l=uT6t) zo$M^UFNeHL8$c0okBEHoPY<{W&g_^f@q0=-1uBy~EP! z-@^%T2zWt67B(IGp9ydpa4Rqu$lSEg&%#e?*mf|0+nLyFyaC0}yE(XjL zzX$#%BK4;OxSR%UrUG{Z^GyqO91ifnqH;W5g-vVUq$~@v(=_Hz%ArpeJGxB(rU9>r zi1$>qE~f}~Nm#4S_czq>;wLrKdevzqhO`uosiQL(b-5eB2fPd1DI%&ppe0$LN~w;( z0$?m)e|4pY55KOXu+qeClQTjz0YlL#N9X22ZtfYD4&Vpg10EKU!eoF;658wuOa~@Z z*I9i0O+7Dt>LCzN*pdt;=jZDL4LDY2pe3<~bAY#itG#~3>z{jgb)^TVy?xqrJB4-6bwU%kd}<*9qkYMS-++lj)0lK)I;k!J z_#famKw)di(GG6xPfxz-bry-hcA6 z5S(R&l9*rgb}?^a4uBWv91nC$5EcMwZF1nqXSs^(*Rp)@d12(?9`h zzVk71W-*pU3NR<0hFgMh*H=4z>^dAkPBZbUuqg&jPFwaA0V8m{%w)>}Kfo373KA`j zIiCaA-&#)j_ID$Z!j{T$PHYz#mkcu0lzGjeC3mJ)!xcN*ut~ zS{7g4_(U<~9+Jk7z}*YW35MgeB9vdRnOy)txyNGAw8DlM-0VFsPtN7)OI#;_ z&LU#9Mp}JWZt>EROZftd2VbiIAS+elvuE>RU|tL}1+xA2&Wx%=9rC4DOS{hCdassF{kuD3#>`FExNUH1 z!tvN@k4Dl~EedX~S+`Qkl){2|1DPMU`d@m^XgkfyaKG88J^0V!~8( z#T7JQ=51O0b$VV58SOh3jC`IrG#xX+1Vz_`Rof$2E@)6{v@XX2Bm?-yektftggk0y z2@5`|iMeOYm8q*(-*`+ZH z-C9#HU?_B(3;{lj_F*09wQ-LRr#->8i>VmU!xH*GHN@aw2Cp2J z>@m5s$7?ksw8KR~=6@LL)0_AE>HokjC1my3tTW;WheXEAE*=@KcFFZib@|fUcCv0?t~tvA)>^Ta%xPjTj&s=XYubAf7yTR}N3VQ;n{{ zgQGLzPtq1@5De>=!mxfRRMlBAiYToy6Yj)l0=td`=zD?VA^=fJ4OEIv!=EgsxYBI3 znAdGWs!J%yw(<1s*<5sRS>h@sp7r$ z_4GK~&Y+8(T-MXcg=g7uIRuwI7R-fb*?Ir*#<*VVRm@mg!N$G*xMOcU5cz$A4h*jM zt4Pl&9^j=@JH!O~Z}<5rtv0#p5?3TuT)o}Pj5jK=35_8nY<6F4^|lH4aFKy-gF{6@ z4i*Oa%j#M*4eU1I#rtyTf1xu%eD80lqf3FE>w3Gw`%l7@Q5k%>qXd^j;>2Uvb2P|B zo$cdvV94HMK}?JIHL$?M=gSy2AcZYI_}R42PgSi20@wFVVg960E2e%wxti=$j()D>~Z1182sadHlH_aNHe%m2Gx=mtQG;6l|sQ0T#vj6ay zEFOHdJW>|}WTXgvx;bJ()1(*5@dgy@pDUnaR>*Wl40Q9%yVa4FPJ^4wy(>Gy?Bz8U z+YbgJ&BK79SijrH+MV9W+*)$C?2skeG++ZlFMRtFDuPTQ8Y&Q7a!t%)Cbjq>Or<;@E1KccrutUr{rZ28Y zxZ4p;xHE3aqThuMfcr({h(v9Tz7O0yZE*$bcKVVA-K~?Ix2AW9X}N}b!vK7~)yve^ zDq;$b26=T#F8#tR?DZ>Bq8b1SxaFBr4iyHI+V(N6kV}NEW^Uw#_zT^&l~yXkhR9!JB7$XiOOk#`J99edzJfdBqq zZ&F*M)5m0z;nsNla}O^q_aI?&7EQ>{wvm?+N_5IS7DbgNhN*Dqf?+ADYAvRY&fvF0 z(pdP3C*iYRp_Z|WTdz$8SRf*=CA3}I1pEfmV$jgXi>RzkY`4B3+s5*l`Dg+|W+eVx z^uEwnb3;cW#ywlg!NSCv65Y?X^UkAr0Q*FwXG@1#v{t|Z+-TFNT=rysi+O5O|9SD= zTr`2b$ATO$GuxFYlvRfoY1VJE6Y^u-CM>!?7oZyWc|6Uw30Oo3v)g5q8yLO4UqYposX|YV0L22{jB@@$R}h=KNQ6 zn|_s_ty5TTwEG~8QjnXWQQ@)L^xK)YXEAc18{iHRd9$s9X<7nc5s{U^YrnoGmD{ds zdw-|6(m174KO(dV*0f+qA6Em|KO_P+*?t&GsnwQZ@YrWcI9z0$_7gtI$i?S5c=MqS z02_fpt@&-c0EbD;1^lEG9j|?)s97)i? this.itemCanExpand(item)).length; - }; + item.Expanded = expanded; + if (!expanded) { + item.Highlighted = false; + } + if (!item.Expanded) { + this.state.expandAll = false; + } + }; - this.expandAll = function () { - this.state.expandAll = !this.state.expandAll; - _.forEach(this.dataset, (item) => { - if (this.itemCanExpand(item)) { - this.expandItem(item, this.state.expandAll); - } - }); - }; + this.itemCanExpand = function (item) { + return item.GlobalIPv6Address !== ''; + }; - this.$onInit = function () { - this.setDefaults(); - this.prepareTableFromDataset(); + this.hasExpandableItems = function () { + return _.filter(this.dataset, (item) => this.itemCanExpand(item)).length; + }; - this.state.orderBy = this.orderBy; - var storedOrder = DatatableService.getDataTableOrder(this.tableKey); - if (storedOrder !== null) { - this.state.reverseOrder = storedOrder.reverse; - this.state.orderBy = storedOrder.orderBy; + this.expandAll = function () { + this.state.expandAll = !this.state.expandAll; + _.forEach(this.dataset, (item) => { + if (this.itemCanExpand(item)) { + this.expandItem(item, this.state.expandAll); } + }); + }; - var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); - if (textFilter !== null) { - this.state.textFilter = textFilter; - this.onTextFilterChange(); - } + this.$onInit = function () { + this.setDefaults(); + this.prepareTableFromDataset(); - var storedFilters = DatatableService.getDataTableFilters(this.tableKey); - if (storedFilters !== null) { - this.filters = storedFilters; - } - if (this.filters && this.filters.state) { - this.filters.state.open = false; - } + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } - var storedSettings = DatatableService.getDataTableSettings(this.tableKey); - if (storedSettings !== null) { - this.settings = storedSettings; - this.settings.open = false; - } + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } - _.forEach(this.dataset, (item) => { - item.Expanded = true; - item.Highlighted = true; - }); - }; - } - ]); \ No newline at end of file + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + + _.forEach(this.dataset, (item) => { + item.Expanded = true; + item.Highlighted = true; + }); + }; + }, +]); diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 819fc8c0b..72e7e4a11 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -59,6 +59,9 @@ +
+ Close +
diff --git a/app/docker/components/volumesNFSForm/volumesnfsForm.html b/app/docker/components/volumesNFSForm/volumesnfsForm.html index 3a197ebcd..fa6496f30 100644 --- a/app/docker/components/volumesNFSForm/volumesnfsForm.html +++ b/app/docker/components/volumesNFSForm/volumesnfsForm.html @@ -38,7 +38,14 @@
- +
diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index 65a90ba72..c7a92f17a 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -1,4 +1,4 @@ -import { ContainerDetailsViewModel, ContainerViewModel, ContainerStatsViewModel } from '../models/container'; +import { ContainerDetailsViewModel, ContainerStatsViewModel, ContainerViewModel } from '../models/container'; angular.module('portainer.docker').factory('ContainerService', [ '$q', diff --git a/app/docker/views/containers/inspect/containerinspect.html b/app/docker/views/containers/inspect/containerinspect.html index def4954d5..eeedd2360 100644 --- a/app/docker/views/containers/inspect/containerinspect.html +++ b/app/docker/views/containers/inspect/containerinspect.html @@ -9,7 +9,7 @@
- + diff --git a/app/edge/components/edge-groups-selector/edge-groups-selector.html b/app/edge/components/edge-groups-selector/edge-groups-selector.html index 2badca9a2..41fa18a68 100644 --- a/app/edge/components/edge-groups-selector/edge-groups-selector.html +++ b/app/edge/components/edge-groups-selector/edge-groups-selector.html @@ -1,16 +1,10 @@ - + {{ $item.Name }} - + {{ item.Name }} diff --git a/app/edge/components/edge-groups-selector/edge-groups-selector.js b/app/edge/components/edge-groups-selector/edge-groups-selector.js index 3322780b8..66f8c429d 100644 --- a/app/edge/components/edge-groups-selector/edge-groups-selector.js +++ b/app/edge/components/edge-groups-selector/edge-groups-selector.js @@ -2,6 +2,6 @@ angular.module('portainer.edge').component('edgeGroupsSelector', { templateUrl: './edge-groups-selector.html', bindings: { model: '=', - items: '<' - } + items: '<', + }, }); diff --git a/app/edge/components/group-form/groupForm.html b/app/edge/components/group-form/groupForm.html index a33a534df..b527440d0 100644 --- a/app/edge/components/group-form/groupForm.html +++ b/app/edge/components/group-form/groupForm.html @@ -59,11 +59,9 @@ tags="$ctrl.tags" groups="$ctrl.groups" has-backend-pagination="true" - - on-associate="$ctrl.associateEndpoint" - on-dissociate="$ctrl.dissociateEndpoint" + on-associate="($ctrl.associateEndpoint)" + on-dissociate="($ctrl.dissociateEndpoint)" > -
diff --git a/app/extensions/registry-management/models/registryRepository.js b/app/extensions/registry-management/models/registryRepository.js index 4e772ae21..eb723bd75 100644 --- a/app/extensions/registry-management/models/registryRepository.js +++ b/app/extensions/registry-management/models/registryRepository.js @@ -1,4 +1,5 @@ import _ from 'lodash-es'; + export function RegistryRepositoryViewModel(item) { if (item.name && item.tags) { this.Name = item.name; diff --git a/app/extensions/registry-management/services/registryV2Service.js b/app/extensions/registry-management/services/registryV2Service.js index e877a4b99..242ca364b 100644 --- a/app/extensions/registry-management/services/registryV2Service.js +++ b/app/extensions/registry-management/services/registryV2Service.js @@ -1,6 +1,5 @@ import _ from 'lodash-es'; -import { RepositoryShortTag } from '../models/repositoryTag'; -import { RepositoryAddTagPayload } from '../models/repositoryTag'; +import { RepositoryAddTagPayload, RepositoryShortTag } from '../models/repositoryTag'; import { RegistryRepositoryViewModel } from '../models/registryRepository'; import genericAsyncGenerator from './genericAsyncGenerator'; diff --git a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js index 6bf5cb475..e8ab74f09 100644 --- a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js +++ b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { RepositoryTagViewModel, RepositoryShortTag } from '../../../models/repositoryTag'; +import { RepositoryShortTag, RepositoryTagViewModel } from '../../../models/repositoryTag'; angular.module('portainer.app').controller('RegistryRepositoryController', [ '$q', diff --git a/app/index.html b/app/index.html index ef7137d15..89e9c87ea 100644 --- a/app/index.html +++ b/app/index.html @@ -26,7 +26,7 @@ id="page-wrapper" ng-class="{ open: toggle && ['portainer.auth', 'portainer.init.admin', 'portainer.init.endpoint'].indexOf($state.current.name) === -1, - nopadding: ['portainer.auth', 'portainer.init.admin', 'portainer.init.endpoint'].indexOf($state.current.name) > -1 || applicationState.loading + nopadding: ['portainer.auth', 'portainer.init.admin', 'portainer.init.endpoint', 'portainer.logout'].indexOf($state.current.name) > -1 || applicationState.loading }" ng-cloak > diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js new file mode 100644 index 000000000..29fd6e81e --- /dev/null +++ b/app/kubernetes/__module.js @@ -0,0 +1,261 @@ +angular.module('portainer.kubernetes', ['portainer.app']).config([ + '$stateRegistryProvider', + function ($stateRegistryProvider) { + 'use strict'; + + const kubernetes = { + name: 'kubernetes', + url: '/kubernetes', + parent: 'root', + abstract: true, + resolve: { + endpointID: [ + 'EndpointProvider', + '$state', + function (EndpointProvider, $state) { + const id = EndpointProvider.endpointID(); + if (!id) { + return $state.go('portainer.home'); + } + }, + ], + }, + }; + + const applications = { + name: 'kubernetes.applications', + url: '/applications', + views: { + 'content@': { + component: 'kubernetesApplicationsView', + }, + }, + }; + + const applicationCreation = { + name: 'kubernetes.applications.new', + url: '/new', + views: { + 'content@': { + component: 'kubernetesCreateApplicationView', + }, + }, + }; + + const application = { + name: 'kubernetes.applications.application', + url: '/:namespace/:name', + views: { + 'content@': { + component: 'kubernetesApplicationView', + }, + }, + }; + + const applicationEdit = { + name: 'kubernetes.applications.application.edit', + url: '/edit', + views: { + 'content@': { + component: 'kubernetesCreateApplicationView', + }, + }, + }; + + const applicationConsole = { + name: 'kubernetes.applications.application.console', + url: '/:pod/console', + views: { + 'content@': { + component: 'kubernetesApplicationConsoleView', + }, + }, + }; + + const applicationLogs = { + name: 'kubernetes.applications.application.logs', + url: '/:pod/logs', + views: { + 'content@': { + component: 'kubernetesApplicationLogsView', + }, + }, + }; + + const stacks = { + name: 'kubernetes.stacks', + url: '/stacks', + abstract: true, + }; + + const stack = { + name: 'kubernetes.stacks.stack', + url: '/:namespace/:name', + abstract: true, + }; + + const stackLogs = { + name: 'kubernetes.stacks.stack.logs', + url: '/logs', + views: { + 'content@': { + component: 'kubernetesStackLogsView', + }, + }, + }; + + const configurations = { + name: 'kubernetes.configurations', + url: '/configurations', + views: { + 'content@': { + component: 'kubernetesConfigurationsView', + }, + }, + }; + + const configurationCreation = { + name: 'kubernetes.configurations.new', + url: '/new', + views: { + 'content@': { + component: 'kubernetesCreateConfigurationView', + }, + }, + }; + + const configuration = { + name: 'kubernetes.configurations.configuration', + url: '/:namespace/:name', + views: { + 'content@': { + component: 'kubernetesConfigurationView', + }, + }, + }; + + const cluster = { + name: 'kubernetes.cluster', + url: '/cluster', + views: { + 'content@': { + component: 'kubernetesClusterView', + }, + }, + }; + + const node = { + name: 'kubernetes.cluster.node', + url: '/:name', + views: { + 'content@': { + component: 'kubernetesNodeView', + }, + }, + }; + + const dashboard = { + name: 'kubernetes.dashboard', + url: '/dashboard', + views: { + 'content@': { + component: 'kubernetesDashboardView', + }, + }, + }; + + const deploy = { + name: 'kubernetes.deploy', + url: '/deploy', + views: { + 'content@': { + component: 'kubernetesDeployView', + }, + }, + }; + + const resourcePools = { + name: 'kubernetes.resourcePools', + url: '/pools', + views: { + 'content@': { + component: 'kubernetesResourcePoolsView', + }, + }, + }; + + const resourcePoolCreation = { + name: 'kubernetes.resourcePools.new', + url: '/new', + views: { + 'content@': { + component: 'kubernetesCreateResourcePoolView', + }, + }, + }; + + const resourcePool = { + name: 'kubernetes.resourcePools.resourcePool', + url: '/:id', + views: { + 'content@': { + component: 'kubernetesResourcePoolView', + }, + }, + }; + + const resourcePoolAccess = { + name: 'kubernetes.resourcePools.resourcePool.access', + url: '/access', + views: { + 'content@': { + component: 'kubernetesResourcePoolAccessView', + }, + }, + }; + + const volumes = { + name: 'kubernetes.volumes', + url: '/volumes', + views: { + 'content@': { + component: 'kubernetesVolumesView', + }, + }, + }; + + const volume = { + name: 'kubernetes.volumes.volume', + url: '/:namespace/:name', + views: { + 'content@': { + component: 'kubernetesVolumeView', + }, + }, + }; + + $stateRegistryProvider.register(kubernetes); + $stateRegistryProvider.register(applications); + $stateRegistryProvider.register(applicationCreation); + $stateRegistryProvider.register(application); + $stateRegistryProvider.register(applicationEdit); + $stateRegistryProvider.register(applicationConsole); + $stateRegistryProvider.register(applicationLogs); + $stateRegistryProvider.register(stacks); + $stateRegistryProvider.register(stack); + $stateRegistryProvider.register(stackLogs); + $stateRegistryProvider.register(configurations); + $stateRegistryProvider.register(configurationCreation); + $stateRegistryProvider.register(configuration); + $stateRegistryProvider.register(cluster); + $stateRegistryProvider.register(dashboard); + $stateRegistryProvider.register(deploy); + $stateRegistryProvider.register(node); + $stateRegistryProvider.register(resourcePools); + $stateRegistryProvider.register(resourcePoolCreation); + $stateRegistryProvider.register(resourcePool); + $stateRegistryProvider.register(resourcePoolAccess); + $stateRegistryProvider.register(volumes); + $stateRegistryProvider.register(volume); + }, +]); diff --git a/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html b/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html new file mode 100644 index 000000000..0b5ba805a --- /dev/null +++ b/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.html @@ -0,0 +1,157 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Image + + + + + + Status + + + + + + Node + + + + + + Creation date + + + + Actions
{{ item.Name }}{{ image }}
{{ item.Status }} + + + {{ item.Node }} + + + - + {{ item.CreationDate | getisodate }} + Logs + + Console + +
Loading...
No pod available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js b/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js new file mode 100644 index 000000000..faa7df25f --- /dev/null +++ b/app/kubernetes/components/datatables/application/pods-datatable/podsDatatable.js @@ -0,0 +1,12 @@ +angular.module('portainer.kubernetes').component('kubernetesPodsDatatable', { + templateUrl: './podsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html new file mode 100644 index 000000000..4dee337e4 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -0,0 +1,193 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Stack + + + + + + Resource pool + + + + + + Image + + + + + Deployment + + Publishing mode + + + Created + + + +
+ + + + + {{ item.Name }} + system + external + {{ item.StackName }} + {{ item.ResourcePool }} + {{ item.Image }} + Replicated + Global + {{ item.RunningPodsCount }} / {{ item.TotalPodsCount }} + + + + + {{ item.ServiceType | kubernetesApplicationServiceTypeText }} + + + + - + {{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }}
Loading...
No application available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js new file mode 100644 index 000000000..fea41d460 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.js @@ -0,0 +1,15 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationsDatatable', { + templateUrl: './applicationsDatatable.html', + controller: 'KubernetesApplicationsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + removeAction: '<', + refreshCallback: '<', + onPublishingModeClick: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js new file mode 100644 index 000000000..aaee33363 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatableController.js @@ -0,0 +1,77 @@ +import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesApplicationsDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + 'Authentication', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + var ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsShowSystemChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem; + }; + + /** + * Do not allow applications in system namespaces to be selected + */ + this.allowSelection = function (item) { + return !this.isSystemNamespace(item); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html new file mode 100644 index 000000000..cf66330d6 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html @@ -0,0 +1,192 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Application + + + + + Publishing mode + + + Exposed port + + + + + + Container port + + + + + + Protocol + + + +
+ + + + + {{ item.Name }} + system + external + + + Load balancer + + {{ item.LoadBalancerIPAddress }} + pending + + + Internal + Cluster + + {{ item.Ports[0].Port }} + + access + + {{ item.Ports[0].TargetPort }}{{ item.Ports[0].Protocol }}
-- + {{ port.Port }} + + access + + {{ port.TargetPort }}{{ port.Protocol }}
Loading...
No application port mapping available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.js b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.js new file mode 100644 index 000000000..139b000c5 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationsPortsDatatable', { + templateUrl: './applicationsPortsDatatable.html', + controller: 'KubernetesApplicationsPortsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js new file mode 100644 index 000000000..e7086a40c --- /dev/null +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js @@ -0,0 +1,103 @@ +import _ from 'lodash-es'; +import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + 'Authentication', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + this.state = Object.assign(this.state, { + expandedItems: [], + expandAll: false, + }); + + var ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsShowSystemChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem; + }; + + this.expandItem = function (item, expanded) { + if (!this.itemCanExpand(item)) { + return; + } + + item.Expanded = expanded; + if (!expanded) { + item.Highlighted = false; + } + }; + + this.itemCanExpand = function (item) { + return item.Ports.length > 1; + }; + + this.hasExpandableItems = function () { + return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length; + }; + + this.expandAll = function () { + this.state.expandAll = !this.state.expandAll; + _.forEach(this.state.filteredDataSet, (item) => { + if (this.itemCanExpand(item)) { + this.expandItem(item, this.state.expandAll); + } + }); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html new file mode 100644 index 000000000..4bef79088 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.html @@ -0,0 +1,183 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + Stack + + + + + + Resource pool + + + + + + Applications + + + + + Actions +
+ + + + + + + + + {{ item.Name }} + + {{ item.ResourcePool }} + system + {{ item.Applications.length }} + Logs +
+ {{ app.Name }} + external +
Loading...
No stack available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js new file mode 100644 index 000000000..3826ba62b --- /dev/null +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationsStacksDatatable', { + templateUrl: './applicationsStacksDatatable.html', + controller: 'KubernetesApplicationsStacksDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + removeAction: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js new file mode 100644 index 000000000..cc77ba205 --- /dev/null +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js @@ -0,0 +1,110 @@ +import _ from 'lodash-es'; +import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + 'Authentication', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + this.state = Object.assign(this.state, { + expandedItems: [], + expandAll: false, + }); + + var ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsRepeaterChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + /** + * Do not allow applications in system namespaces to be selected + */ + this.allowSelection = function (item) { + return !this.isSystemNamespace(item); + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemNamespace(item) || ctrl.settings.showSystem; + }; + + this.expandItem = function (item, expanded) { + if (!this.itemCanExpand(item)) { + return; + } + + item.Expanded = expanded; + if (!expanded) { + item.Highlighted = false; + } + }; + + this.itemCanExpand = function (item) { + return item.Applications.length > 0; + }; + + this.hasExpandableItems = function () { + return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length; + }; + + this.expandAll = function () { + this.state.expandAll = !this.state.expandAll; + _.forEach(this.state.filteredDataSet, (item) => { + if (this.itemCanExpand(item)) { + this.expandItem(item, this.state.expandAll); + } + }); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.html b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.html new file mode 100644 index 000000000..daedca169 --- /dev/null +++ b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.html @@ -0,0 +1,162 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Resource Pool + + + + + + Type + + + + + + Created + + + +
+ + + + + {{ item.Name }} + external + unused + system + + {{ item.Namespace }} + {{ item.Type | kubernetesConfigurationTypeText }}{{ item.CreationDate | getisodate }} {{ item.ConfigurationOwner ? 'by ' + item.ConfigurationOwner : '' }}
Loading...
No configuration available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.js b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.js new file mode 100644 index 000000000..bf68244e0 --- /dev/null +++ b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesConfigurationsDatatable', { + templateUrl: './configurationsDatatable.html', + controller: 'KubernetesConfigurationsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + refreshCallback: '<', + removeAction: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js new file mode 100644 index 000000000..18417d951 --- /dev/null +++ b/app/kubernetes/components/datatables/configurations-datatable/configurationsDatatableController.js @@ -0,0 +1,83 @@ +import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; + +angular.module('portainer.docker').controller('KubernetesConfigurationsDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + 'Authentication', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + const ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsShowSystemChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace); + }; + + this.isSystemToken = function (item) { + return KubernetesConfigurationHelper.isSystemToken(item); + }; + + this.isSystemConfig = function (item) { + return ctrl.isSystemNamespace(item) || ctrl.isSystemToken(item); + }; + + this.isExternalConfiguration = function (item) { + return KubernetesConfigurationHelper.isExternalConfiguration(item); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemConfig(item) || (ctrl.settings.showSystem && ctrl.isAdmin); + }; + + /** + * Do not allow configurations in system namespaces to be selected + */ + this.allowSelection = function (item) { + return !this.isSystemConfig(item) && !item.Used; + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/events-datatable/eventsDatatable.html b/app/kubernetes/components/datatables/events-datatable/eventsDatatable.html new file mode 100644 index 000000000..6a6d14603 --- /dev/null +++ b/app/kubernetes/components/datatables/events-datatable/eventsDatatable.html @@ -0,0 +1,134 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + Date + + + + + + Kind + + + + + + Type + + + + + + Message + + + +
{{ item.Date | getisodate }}{{ item.Involved.kind }}{{ item.Type }}{{ item.Message }}
Loading...
No event available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/events-datatable/eventsDatatable.js b/app/kubernetes/components/datatables/events-datatable/eventsDatatable.js new file mode 100644 index 000000000..ef4f16504 --- /dev/null +++ b/app/kubernetes/components/datatables/events-datatable/eventsDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.kubernetes').component('kubernetesEventsDatatable', { + templateUrl: './eventsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + loading: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.html b/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.html new file mode 100644 index 000000000..12a0d200b --- /dev/null +++ b/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.html @@ -0,0 +1,127 @@ +
+ + +
+
+ + {{ $ctrl.titleText }} +
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Stack + + + + + + Image + + + +
{{ item.Name }}{{ item.StackName }}{{ item.Image }}
Loading...
No application available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.js b/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.js new file mode 100644 index 000000000..35729102f --- /dev/null +++ b/app/kubernetes/components/datatables/integrated-applications-datatable/integratedApplicationsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesIntegratedApplicationsDatatable', { + templateUrl: './integratedApplicationsDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html new file mode 100644 index 000000000..4ab919bd1 --- /dev/null +++ b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.html @@ -0,0 +1,156 @@ +
+ + +
+
+ + {{ $ctrl.titleText }} +
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Stack + + + + + + Resource pool + + + + + + Image + + + + + + CPU reservation + + + + + + Memory reservation + + + +
+ {{ item.Name }} + system + external + {{ item.StackName }} + {{ item.ResourcePool }} + {{ item.Image }}{{ item.CPU | kubernetesApplicationCPUValue }}{{ item.Memory | humansize }}
Loading...
No stack available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js new file mode 100644 index 000000000..ed9879453 --- /dev/null +++ b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesNodeApplicationsDatatable', { + templateUrl: './nodeApplicationsDatatable.html', + controller: 'KubernetesNodeApplicationsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js new file mode 100644 index 000000000..9bb25c50a --- /dev/null +++ b/app/kubernetes/components/datatables/node-applications-datatable/nodeApplicationsDatatableController.js @@ -0,0 +1,54 @@ +import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesNodeApplicationsDatatableController', [ + '$scope', + '$controller', + 'KubernetesNamespaceHelper', + 'DatatableService', + function ($scope, $controller, KubernetesNamespaceHelper, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool); + }; + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + this.$onInit = function () { + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html new file mode 100644 index 000000000..7d90d2379 --- /dev/null +++ b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html @@ -0,0 +1,164 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Role + + + + + + Status + + + + + + CPU + + + + + + Memory + + + + + + Version + + + + + + IP Address + + + +
+ + {{ item.Name }} + + + {{ item.Name }} + {{ item.Role }}{{ item.Status }}{{ item.CPU }}{{ item.Memory | humansize }}{{ item.Version }}{{ item.IPAddress }}
Loading...
No node available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js new file mode 100644 index 000000000..3c1312302 --- /dev/null +++ b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesNodesDatatable', { + templateUrl: './nodesDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + refreshCallback: '<', + isAdmin: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html new file mode 100644 index 000000000..c08cd865b --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.html @@ -0,0 +1,145 @@ +
+ + +
+
+ + {{ $ctrl.titleText }} +
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + + + + + Stack + + + + + + Image + + + + + + CPU reservation + + + + + + Memory reservation + + + +
+ {{ item.Name }} + external + {{ item.StackName }}{{ item.Image }}{{ item.CPU | kubernetesApplicationCPUValue }}{{ item.Memory | humansize }}
Loading...
No application available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.js b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.js new file mode 100644 index 000000000..02dba5e79 --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolApplicationsDatatable', { + templateUrl: './resourcePoolApplicationsDatatable.html', + controller: 'KubernetesResourcePoolApplicationsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatableController.js b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatableController.js new file mode 100644 index 000000000..e97cf734d --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pool-applications-datatable/resourcePoolApplicationsDatatableController.js @@ -0,0 +1,47 @@ +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +angular.module('portainer.docker').controller('KubernetesResourcePoolApplicationsDatatableController', [ + '$scope', + '$controller', + 'DatatableService', + function ($scope, $controller, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + this.isExternalApplication = function (item) { + return KubernetesApplicationHelper.isExternalApplication(item); + }; + + this.$onInit = function () { + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html new file mode 100644 index 000000000..088f737df --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.html @@ -0,0 +1,160 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Quota + + + + + + Created + + + + + Actions +
+ + + + + {{ item.Namespace.Name }} + system + {{ item.Quota ? 'Yes' : 'No' }} {{ item.CreationDate | getisodate }} {{ item.Namespace.ResourcePoolOwner ? 'by ' + item.Namespace.ResourcePoolOwner : '' }} + + Manage access + + - +
Loading...
No resource pool available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js new file mode 100644 index 000000000..8023b8518 --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatable', { + templateUrl: './resourcePoolsDatatable.html', + controller: 'KubernetesResourcePoolsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + removeAction: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js new file mode 100644 index 000000000..b3181bfe9 --- /dev/null +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js @@ -0,0 +1,77 @@ +angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableController', [ + '$scope', + '$controller', + 'Authentication', + 'KubernetesNamespaceHelper', + 'DatatableService', + function ($scope, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + + var ctrl = this; + + this.settings = Object.assign(this.settings, { + showSystem: false, + }); + + this.onSettingsShowSystemChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.canManageAccess = function (item) { + return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item); + }; + + this.disableRemove = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name) || item.Namespace.Name === 'default'; + }; + + this.isSystemNamespace = function (item) { + return KubernetesNamespaceHelper.isSystemNamespace(item.Namespace.Name); + }; + + this.isDisplayed = function (item) { + return !ctrl.isSystemNamespace(item) || (ctrl.settings.showSystem && ctrl.isAdmin); + }; + + /** + * Do not allow system namespaces to be selected + */ + this.allowSelection = function (item) { + return !this.disableRemove(item); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html new file mode 100644 index 000000000..7955c6ed2 --- /dev/null +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html @@ -0,0 +1,192 @@ +
+ + +
+
{{ $ctrl.titleText }}
+ + + System resources are hidden, this can be changed in the table settings. + +
+ + Table settings + + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Resource pool + + + + + + Used by + + + + + + Storage + + + + + + Size + + + + + + Created + + + +
+ + + + + {{ + item.PersistentVolumeClaim.Name + }} + system + external + unused + + {{ item.ResourcePool.Namespace.Name }} + + {{ item.Applications[0].Name }} + - + + {{ item.PersistentVolumeClaim.StorageClass.Name }} + + {{ item.PersistentVolumeClaim.Storage }} + + {{ item.PersistentVolumeClaim.CreationDate | getisodate }} + {{ item.PersistentVolumeClaim.ApplicationOwner ? 'by ' + item.PersistentVolumeClaim.ApplicationOwner : '' }} +
Loading...
No volume available.
+
+ +
+
+
diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.js b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.js new file mode 100644 index 000000000..ddbdacf7f --- /dev/null +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.js @@ -0,0 +1,14 @@ +angular.module('portainer.kubernetes').component('kubernetesVolumesDatatable', { + templateUrl: './volumesDatatable.html', + controller: 'KubernetesVolumesDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + removeAction: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js new file mode 100644 index 000000000..e9dc9e478 --- /dev/null +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatableController.js @@ -0,0 +1,91 @@ +import angular from 'angular'; +import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; + +// TODO: review - refactor to use `extends GenericDatatableController` +class KubernetesVolumesDatatableController { + /* @ngInject */ + constructor($async, $controller, Authentication, KubernetesNamespaceHelper, DatatableService) { + this.$async = $async; + this.$controller = $controller; + this.Authentication = Authentication; + this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + this.DatatableService = DatatableService; + + this.onInit = this.onInit.bind(this); + this.allowSelection = this.allowSelection.bind(this); + this.isDisplayed = this.isDisplayed.bind(this); + } + + onSettingsShowSystemChange() { + this.DatatableService.setDataTableSettings(this.tableKey, this.settings); + } + + disableRemove(item) { + return this.isSystemNamespace(item) || this.isUsed(item); + } + + isUsed(item) { + return KubernetesVolumeHelper.isUsed(item); + } + + isSystemNamespace(item) { + return this.KubernetesNamespaceHelper.isSystemNamespace(item.ResourcePool.Namespace.Name); + } + + isDisplayed(item) { + return !this.isSystemNamespace(item) || this.showSystem; + } + + isExternalVolume(item) { + return KubernetesVolumeHelper.isExternalVolume(item); + } + + allowSelection(item) { + return !this.disableRemove(item); + } + + async onInit() { + this.setDefaults(); + this.prepareTableFromDataset(); + this.isAdmin = this.Authentication.isAdmin(); + + this.state.orderBy = this.orderBy; + var storedOrder = this.DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = this.DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = this.DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = this.DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + + this.settings.showSystem = false; + } + + $onInit() { + const ctrl = angular.extend({}, this.$controller('GenericDatatableController'), this); + angular.extend(this, ctrl); + return this.$async(this.onInit); + } +} + +export default KubernetesVolumesDatatableController; +angular.module('portainer.kubernetes').controller('KubernetesVolumesDatatableController', KubernetesVolumesDatatableController); diff --git a/app/kubernetes/components/feedback-panel/feedbackPanel.html b/app/kubernetes/components/feedback-panel/feedbackPanel.html new file mode 100644 index 000000000..115a175ea --- /dev/null +++ b/app/kubernetes/components/feedback-panel/feedbackPanel.html @@ -0,0 +1,9 @@ + + +

+ + Kubernetes support in Portainer is now in RC stage. Contribute and share your feedback in + our official repository. +

+
+
diff --git a/app/kubernetes/components/feedback-panel/feedbackPanel.js b/app/kubernetes/components/feedback-panel/feedbackPanel.js new file mode 100644 index 000000000..9bfe07254 --- /dev/null +++ b/app/kubernetes/components/feedback-panel/feedbackPanel.js @@ -0,0 +1,3 @@ +angular.module('portainer.kubernetes').component('kubernetesFeedbackPanel', { + templateUrl: './feedbackPanel.html', +}); diff --git a/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html new file mode 100644 index 000000000..a7db2fc8f --- /dev/null +++ b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.html @@ -0,0 +1,97 @@ + +
+ Data +
+ +
+ +
+ + Switch to advanced mode to copy and paste multiple key/values +
+
+ + Generate a configuration entry per line, use YAML format +
+
+ +
+
+ + +
+
+ +
+
+ +
+ +
+
+ +

This field is required.

+
+

This key is already defined.

+
+
+ +
+ +
+ +
+
+ +

This field is required.

+
+
+
+ +
+
+
+ +
+
+
+ +
+ + +
+
diff --git a/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.js b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.js new file mode 100644 index 000000000..c5f717839 --- /dev/null +++ b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationData.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesConfigurationData', { + templateUrl: './kubernetesConfigurationData.html', + controller: 'KubernetesConfigurationDataController', + bindings: { + formValues: '=', + isValid: '=', + }, +}); diff --git a/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationDataController.js b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationDataController.js new file mode 100644 index 000000000..1dcc43ce8 --- /dev/null +++ b/app/kubernetes/components/kubernetes-configuration-data/kubernetesConfigurationDataController.js @@ -0,0 +1,68 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import { KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues'; +import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; + +class KubernetesConfigurationDataController { + /* @ngInject */ + constructor($async) { + this.$async = $async; + + this.editorUpdate = this.editorUpdate.bind(this); + this.editorUpdateAsync = this.editorUpdateAsync.bind(this); + this.onFileLoad = this.onFileLoad.bind(this); + this.onFileLoadAsync = this.onFileLoadAsync.bind(this); + } + + onChangeKey() { + this.state.duplicateKeys = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.Data, (data) => data.Key)); + this.isValid = Object.keys(this.state.duplicateKeys).length === 0; + } + + addEntry() { + this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry()); + } + + removeEntry(index) { + this.formValues.Data.splice(index, 1); + this.onChangeKey(); + } + + async editorUpdateAsync(cm) { + this.formValues.DataYaml = cm.getValue(); + } + + editorUpdate(cm) { + return this.$async(this.editorUpdateAsync, cm); + } + + async onFileLoadAsync(event) { + const entry = new KubernetesConfigurationFormValuesDataEntry(); + entry.Key = event.target.fileName; + entry.Value = event.target.result; + this.formValues.Data.push(entry); + this.onChangeKey(); + } + + onFileLoad(event) { + return this.$async(this.onFileLoadAsync, event); + } + + addEntryFromFile(file) { + if (file) { + const temporaryFileReader = new FileReader(); + temporaryFileReader.fileName = file.name; + temporaryFileReader.onload = this.onFileLoad; + temporaryFileReader.readAsText(file); + } + } + + $onInit() { + this.state = { + duplicateKeys: {}, + }; + } +} + +export default KubernetesConfigurationDataController; +angular.module('portainer.kubernetes').controller('KubernetesConfigurationDataController', KubernetesConfigurationDataController); diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html new file mode 100644 index 000000000..354ba3760 --- /dev/null +++ b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js new file mode 100644 index 000000000..c12568fa7 --- /dev/null +++ b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js @@ -0,0 +1,6 @@ +angular.module('portainer.kubernetes').component('kubernetesSidebarContent', { + templateUrl: './kubernetesSidebarContent.html', + bindings: { + adminAccess: '<', + }, +}); diff --git a/app/kubernetes/components/resource-reservation/resourceReservation.html b/app/kubernetes/components/resource-reservation/resourceReservation.html new file mode 100644 index 000000000..e03e62ace --- /dev/null +++ b/app/kubernetes/components/resource-reservation/resourceReservation.html @@ -0,0 +1,32 @@ +
+
+ Resource reservation +
+
+ +

+ {{ $ctrl.description }} +

+
+
+
+ +
+ + {{ $ctrl.memory }} / {{ $ctrl.memoryLimit }} MB - {{ $ctrl.memoryUsage }}% + +
+
+
+ +
+ + {{ $ctrl.cpu | kubernetesApplicationCPUValue }} / {{ $ctrl.cpuLimit }} - {{ $ctrl.cpuUsage }}% + +
+
+
diff --git a/app/kubernetes/components/resource-reservation/resourceReservation.js b/app/kubernetes/components/resource-reservation/resourceReservation.js new file mode 100644 index 000000000..617f70079 --- /dev/null +++ b/app/kubernetes/components/resource-reservation/resourceReservation.js @@ -0,0 +1,11 @@ +angular.module('portainer.kubernetes').component('kubernetesResourceReservation', { + templateUrl: './resourceReservation.html', + controller: 'KubernetesResourceReservationController', + bindings: { + description: '@', + cpu: '<', + cpuLimit: '<', + memory: '<', + memoryLimit: '<', + }, +}); diff --git a/app/kubernetes/components/resource-reservation/resourceReservationController.js b/app/kubernetes/components/resource-reservation/resourceReservationController.js new file mode 100644 index 000000000..f55a014a1 --- /dev/null +++ b/app/kubernetes/components/resource-reservation/resourceReservationController.js @@ -0,0 +1,23 @@ +import angular from 'angular'; + +class KubernetesResourceReservationController { + usageValues() { + if (this.cpuLimit) { + this.cpuUsage = Math.round((this.cpu / this.cpuLimit) * 100); + } + if (this.memoryLimit) { + this.memoryUsage = Math.round((this.memory / this.memoryLimit) * 100); + } + } + + $onInit() { + this.usageValues(); + } + + $onChanges() { + this.usageValues(); + } +} + +export default KubernetesResourceReservationController; +angular.module('portainer.kubernetes').controller('KubernetesResourceReservationController', KubernetesResourceReservationController); diff --git a/app/kubernetes/components/view-header/viewHeader.html b/app/kubernetes/components/view-header/viewHeader.html new file mode 100644 index 000000000..79ede2708 --- /dev/null +++ b/app/kubernetes/components/view-header/viewHeader.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/kubernetes/components/view-header/viewHeader.js b/app/kubernetes/components/view-header/viewHeader.js new file mode 100644 index 000000000..f1fdbbb6b --- /dev/null +++ b/app/kubernetes/components/view-header/viewHeader.js @@ -0,0 +1,9 @@ +angular.module('portainer.kubernetes').component('kubernetesViewHeader', { + templateUrl: './viewHeader.html', + transclude: true, + bindings: { + viewReady: '<', + title: '@', + state: '@', + }, +}); diff --git a/app/kubernetes/components/view-loading/viewLoading.html b/app/kubernetes/components/view-loading/viewLoading.html new file mode 100644 index 000000000..edf23b89e --- /dev/null +++ b/app/kubernetes/components/view-loading/viewLoading.html @@ -0,0 +1,8 @@ +
+
+
+
+
+
+
+
diff --git a/app/kubernetes/components/view-loading/viewLoading.js b/app/kubernetes/components/view-loading/viewLoading.js new file mode 100644 index 000000000..0c4724b04 --- /dev/null +++ b/app/kubernetes/components/view-loading/viewLoading.js @@ -0,0 +1,6 @@ +angular.module('portainer.kubernetes').component('kubernetesViewLoading', { + templateUrl: './viewLoading.html', + bindings: { + viewReady: '<', + }, +}); diff --git a/app/kubernetes/components/yaml-inspector/yamlInspector.html b/app/kubernetes/components/yaml-inspector/yamlInspector.html new file mode 100644 index 000000000..1f2627e88 --- /dev/null +++ b/app/kubernetes/components/yaml-inspector/yamlInspector.html @@ -0,0 +1,7 @@ +
+ +
+ Copy to clipboard + +
+
diff --git a/app/kubernetes/components/yaml-inspector/yamlInspector.js b/app/kubernetes/components/yaml-inspector/yamlInspector.js new file mode 100644 index 000000000..c84bd8828 --- /dev/null +++ b/app/kubernetes/components/yaml-inspector/yamlInspector.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesYamlInspector', { + templateUrl: './yamlInspector.html', + controller: 'KubernetesYamlInspectorController', + bindings: { + key: '@', + data: '<', + }, +}); diff --git a/app/kubernetes/components/yaml-inspector/yamlInspectorController.js b/app/kubernetes/components/yaml-inspector/yamlInspectorController.js new file mode 100644 index 000000000..82b009316 --- /dev/null +++ b/app/kubernetes/components/yaml-inspector/yamlInspectorController.js @@ -0,0 +1,17 @@ +import angular from 'angular'; + +class KubernetesYamlInspectorController { + /* @ngInject */ + + constructor(clipboard) { + this.clipboard = clipboard; + } + + copyYAML() { + this.clipboard.copyText(this.data); + $('#copyNotificationYAML').show().fadeOut(2500); + } +} + +export default KubernetesYamlInspectorController; +angular.module('portainer.kubernetes').controller('KubernetesYamlInspectorController', KubernetesYamlInspectorController); diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js new file mode 100644 index 000000000..5c7cf7476 --- /dev/null +++ b/app/kubernetes/converters/application.js @@ -0,0 +1,302 @@ +import _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; + +import { + KubernetesApplication, + KubernetesApplicationConfigurationVolume, + KubernetesApplicationDataAccessPolicies, + KubernetesApplicationDeploymentTypes, + KubernetesApplicationPersistedFolder, + KubernetesApplicationPublishingTypes, + KubernetesApplicationTypes, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationNote, + KubernetesPortainerApplicationOwnerLabel, + KubernetesPortainerApplicationStackNameLabel, +} from 'Kubernetes/models/application/models'; +import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +import KubernetesDeploymentConverter from 'Kubernetes/converters/deployment'; +import KubernetesDaemonSetConverter from 'Kubernetes/converters/daemonSet'; +import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet'; +import KubernetesServiceConverter from 'Kubernetes/converters/service'; +import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim'; +import PortainerError from 'Portainer/error'; + +class KubernetesApplicationConverter { + static applicationCommon(res, data, service) { + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-'; + res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] || '' : ''; + res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : ''; + res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name; + res.ResourcePool = data.metadata.namespace; + res.Image = data.spec.template.spec.containers[0].image; + res.CreationDate = data.metadata.creationTimestamp; + res.Pods = data.Pods; + res.Env = data.spec.template.spec.containers[0].env; + const limits = { + Cpu: 0, + Memory: 0, + }; + res.Limits = _.reduce( + data.spec.template.spec.containers, + (acc, item) => { + if (item.resources.limits && item.resources.limits.cpu) { + acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.limits.cpu); + } + if (item.resources.limits && item.resources.limits.memory) { + acc.Memory += filesizeParser(item.resources.limits.memory, { base: 10 }); + } + return acc; + }, + limits + ); + + const requests = { + Cpu: 0, + Memory: 0, + }; + res.Requests = _.reduce( + data.spec.template.spec.containers, + (acc, item) => { + if (item.resources.requests && item.resources.requests.cpu) { + acc.Cpu += KubernetesResourceReservationHelper.parseCPU(item.resources.requests.cpu); + } + if (item.resources.requests && item.resources.requests.memory) { + acc.Memory += filesizeParser(item.resources.requests.memory, { base: 10 }); + } + return acc; + }, + requests + ); + + if (service) { + const serviceType = service.spec.type; + res.ServiceType = serviceType; + res.ServiceId = service.metadata.uid; + res.ServiceName = service.metadata.name; + + if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) { + if (service.status.loadBalancer.ingress && service.status.loadBalancer.ingress.length > 0) { + res.LoadBalancerIPAddress = service.status.loadBalancer.ingress[0].ip || service.status.loadBalancer.ingress[0].hostname; + } + } + + const ports = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports)); + res.PublishedPorts = service.spec.ports; + _.forEach(res.PublishedPorts, (publishedPort) => { + if (isNaN(publishedPort.targetPort)) { + const targetPort = _.find(ports, { name: publishedPort.targetPort }); + if (targetPort) { + publishedPort.targetPort = targetPort.containerPort; + } + } + }); + } + + res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : []; + + // TODO: review + // this if() fixs direct use of PVC reference inside spec.template.spec.containers[0].volumeMounts + // instead of referencing the PVC the "good way" using spec.template.spec.volumes array + // Basically it creates an "in-memory" reference for the PVC, as if it was saved in + // spec.template.spec.volumes and retrieved from here. + // + // FIX FOR SFS ONLY ; as far as we know it's not possible to do this with DEPLOYMENTS/DAEMONSETS + // + // This may lead to destructing behaviours when we will allow external apps to be edited. + // E.G. if we try to generate the formValues and patch the app, SFS reference will be created under + // spec.template.spec.volumes and not be referenced directly inside spec.template.spec.containers[0].volumeMounts + // As we preserve original SFS name and try to build around it, it SHOULD be fine, but we definitely need to test this + // before allowing external apps modification + if (data.spec.volumeClaimTemplates) { + const vcTemplates = _.map(data.spec.volumeClaimTemplates, (vc) => { + return { + name: vc.metadata.name, + persistentVolumeClaim: { claimName: vc.metadata.name }, + }; + }); + const inexistingPVC = _.filter(vcTemplates, (vc) => { + return !_.find(res.Volumes, { persistentVolumeClaim: { claimName: vc.persistentVolumeClaim.claimName } }); + }); + res.Volumes = _.concat(res.Volumes, inexistingPVC); + } + + const persistedFolders = _.filter(res.Volumes, (volume) => volume.persistentVolumeClaim || volume.hostPath); + + res.PersistedFolders = _.map(persistedFolders, (volume) => { + const matchingVolumeMount = _.find(data.spec.template.spec.containers[0].volumeMounts, { name: volume.name }); + + if (matchingVolumeMount) { + const persistedFolder = new KubernetesApplicationPersistedFolder(); + persistedFolder.MountPath = matchingVolumeMount.mountPath; + + if (volume.persistentVolumeClaim) { + persistedFolder.PersistentVolumeClaimName = volume.persistentVolumeClaim.claimName; + } else { + persistedFolder.HostPath = volume.hostPath.path; + } + + return persistedFolder; + } + }); + + res.PersistedFolders = _.without(res.PersistedFolders, undefined); + + res.ConfigurationVolumes = _.reduce( + data.spec.template.spec.volumes, + (acc, volume) => { + if (volume.configMap || volume.secret) { + const matchingVolumeMount = _.find(data.spec.template.spec.containers[0].volumeMounts, { name: volume.name }); + + if (matchingVolumeMount) { + let items = []; + let configurationName = ''; + + if (volume.configMap) { + items = volume.configMap.items; + configurationName = volume.configMap.name; + } else { + items = volume.secret.items; + configurationName = volume.secret.secretName; + } + + if (!items) { + const configurationVolume = new KubernetesApplicationConfigurationVolume(); + configurationVolume.fileMountPath = matchingVolumeMount.mountPath; + configurationVolume.rootMountPath = matchingVolumeMount.mountPath; + configurationVolume.configurationName = configurationName; + + acc.push(configurationVolume); + } else { + _.forEach(items, (item) => { + const configurationVolume = new KubernetesApplicationConfigurationVolume(); + configurationVolume.fileMountPath = matchingVolumeMount.mountPath + '/' + item.path; + configurationVolume.rootMountPath = matchingVolumeMount.mountPath; + configurationVolume.configurationKey = item.key; + configurationVolume.configurationName = configurationName; + + acc.push(configurationVolume); + }); + } + } + } + + return acc; + }, + [] + ); + } + + static apiDeploymentToApplication(data, service) { + const res = new KubernetesApplication(); + KubernetesApplicationConverter.applicationCommon(res, data, service); + res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT; + res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED; + res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED; + res.RunningPodsCount = data.status.availableReplicas || data.status.replicas - data.status.unavailableReplicas || 0; + res.TotalPodsCount = data.spec.replicas; + return res; + } + + static apiDaemonSetToApplication(data, service) { + const res = new KubernetesApplication(); + KubernetesApplicationConverter.applicationCommon(res, data, service); + res.ApplicationType = KubernetesApplicationTypes.DAEMONSET; + res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL; + res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED; + res.RunningPodsCount = data.status.numberAvailable || data.status.desiredNumberScheduled - data.status.numberUnavailable || 0; + res.TotalPodsCount = data.status.desiredNumberScheduled; + return res; + } + + static apiStatefulSetToapplication(data, service) { + const res = new KubernetesApplication(); + KubernetesApplicationConverter.applicationCommon(res, data, service); + res.ApplicationType = KubernetesApplicationTypes.STATEFULSET; + res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED; + res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED; + res.RunningPodsCount = data.status.readyReplicas || 0; + res.TotalPodsCount = data.spec.replicas; + res.HeadlessServiceName = data.spec.serviceName; + return res; + } + + static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims) { + const res = new KubernetesApplicationFormValues(); + res.ApplicationType = app.ApplicationType; + res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]); + res.Name = app.Name; + res.StackName = app.StackName; + res.ApplicationOwner = app.ApplicationOwner; + res.Image = app.Image; + res.ReplicaCount = app.TotalPodsCount; + res.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(app.Limits.Memory); + res.CpuLimit = app.Limits.Cpu; + res.DeploymentType = app.DeploymentType; + res.DataAccessPolicy = app.DataAccessPolicy; + res.EnvironmentVariables = KubernetesApplicationHelper.generateEnvVariablesFromEnv(app.Env); + res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders + res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations); + + if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) { + res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER; + } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) { + res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER; + } else { + res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL; + } + res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts); + return res; + } + + static applicationFormValuesToApplication(formValues) { + const claims = KubernetesPersistentVolumeClaimConverter.applicationFormValuesToVolumeClaims(formValues); + const rwx = _.find(claims, (item) => _.includes(item.StorageClass.AccessModes, 'RWX')) !== undefined; + + const deployment = + (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED && + (claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.SHARED))) || + formValues.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT; + + const statefulSet = + (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.REPLICATED && + claims.length > 0 && + formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.ISOLATED) || + formValues.ApplicationType === KubernetesApplicationTypes.STATEFULSET; + + const daemonSet = + (formValues.DeploymentType === KubernetesApplicationDeploymentTypes.GLOBAL && + (claims.length === 0 || (claims.length > 0 && formValues.DataAccessPolicy === KubernetesApplicationDataAccessPolicies.SHARED && rwx))) || + formValues.ApplicationType === KubernetesApplicationTypes.DAEMONSET; + + let app; + if (deployment) { + app = KubernetesDeploymentConverter.applicationFormValuesToDeployment(formValues, claims); + } else if (statefulSet) { + app = KubernetesStatefulSetConverter.applicationFormValuesToStatefulSet(formValues, claims); + } else if (daemonSet) { + app = KubernetesDaemonSetConverter.applicationFormValuesToDaemonSet(formValues, claims); + } else { + throw new PortainerError('Unable to determine which association to use'); + } + + let headlessService; + if (statefulSet) { + headlessService = KubernetesServiceConverter.applicationFormValuesToHeadlessService(formValues); + } + + let service = KubernetesServiceConverter.applicationFormValuesToService(formValues); + if (!service.Ports.length) { + service = undefined; + } + return [app, headlessService, service, claims]; + } +} + +export default KubernetesApplicationConverter; diff --git a/app/kubernetes/converters/configMap.js b/app/kubernetes/converters/configMap.js new file mode 100644 index 000000000..36025c506 --- /dev/null +++ b/app/kubernetes/converters/configMap.js @@ -0,0 +1,81 @@ +import _ from 'lodash-es'; +import YAML from 'yaml'; +import { KubernetesConfigMap } from 'Kubernetes/models/config-map/models'; +import { KubernetesConfigMapCreatePayload, KubernetesConfigMapUpdatePayload } from 'Kubernetes/models/config-map/payloads'; +import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models'; + +class KubernetesConfigMapConverter { + /** + * API ConfigMap to front ConfigMap + */ + static apiToConfigMap(data, yaml) { + const res = new KubernetesConfigMap(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.Namespace = data.metadata.namespace; + res.ConfigurationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : ''; + res.CreationDate = data.metadata.creationTimestamp; + res.Yaml = yaml ? yaml.data : ''; + res.Data = data.data; + return res; + } + + /** + * Generate a default ConfigMap Model + * with ID = 0 (showing it's a default) + * but setting his Namespace and Name + */ + static defaultConfigMap(namespace, name) { + const res = new KubernetesConfigMap(); + res.Name = name; + res.Namespace = namespace; + return res; + } + + /** + * CREATE payload + */ + static createPayload(data) { + const res = new KubernetesConfigMapCreatePayload(); + res.metadata.name = data.Name; + res.metadata.namespace = data.Namespace; + res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = data.ConfigurationOwner; + res.data = data.Data; + return res; + } + + /** + * UPDATE payload + */ + static updatePayload(data) { + const res = new KubernetesConfigMapUpdatePayload(); + res.metadata.uid = data.Id; + res.metadata.name = data.Name; + res.metadata.namespace = data.Namespace; + res.data = data.Data; + return res; + } + + static configurationFormValuesToConfigMap(formValues) { + const res = new KubernetesConfigMap(); + res.Id = formValues.Id; + res.Name = formValues.Name; + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.ConfigurationOwner = formValues.ConfigurationOwner; + if (formValues.IsSimple) { + res.Data = _.reduce( + formValues.Data, + (acc, entry) => { + acc[entry.Key] = entry.Value; + return acc; + }, + {} + ); + } else { + res.Data = YAML.parse(formValues.DataYaml); + } + return res; + } +} + +export default KubernetesConfigMapConverter; diff --git a/app/kubernetes/converters/configuration.js b/app/kubernetes/converters/configuration.js new file mode 100644 index 000000000..232fe5b79 --- /dev/null +++ b/app/kubernetes/converters/configuration.js @@ -0,0 +1,31 @@ +import { KubernetesConfiguration, KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +class KubernetesConfigurationConverter { + static secretToConfiguration(secret) { + const res = new KubernetesConfiguration(); + res.Type = KubernetesConfigurationTypes.SECRET; + res.Id = secret.Id; + res.Name = secret.Name; + res.Namespace = secret.Namespace; + res.CreationDate = secret.CreationDate; + res.Yaml = secret.Yaml; + res.Data = secret.Data; + res.ConfigurationOwner = secret.ConfigurationOwner; + return res; + } + + static configMapToConfiguration(configMap) { + const res = new KubernetesConfiguration(); + res.Type = KubernetesConfigurationTypes.CONFIGMAP; + res.Id = configMap.Id; + res.Name = configMap.Name; + res.Namespace = configMap.Namespace; + res.CreationDate = configMap.CreationDate; + res.Yaml = configMap.Yaml; + res.Data = configMap.Data; + res.ConfigurationOwner = configMap.ConfigurationOwner; + return res; + } +} + +export default KubernetesConfigurationConverter; diff --git a/app/kubernetes/converters/daemonSet.js b/app/kubernetes/converters/daemonSet.js new file mode 100644 index 000000000..fa74b6bf1 --- /dev/null +++ b/app/kubernetes/converters/daemonSet.js @@ -0,0 +1,79 @@ +import * as JsonPatch from 'fast-json-patch'; + +import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; +import { KubernetesDaemonSetCreatePayload } from 'Kubernetes/models/daemon-set/payloads'; +import { + KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationNote, + KubernetesPortainerApplicationOwnerLabel, +} from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; + +class KubernetesDaemonSetConverter { + /** + * Generate KubernetesDaemonSet from KubenetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToDaemonSet(formValues, volumeClaims) { + const res = new KubernetesDaemonSet(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + res.Image = formValues.Image; + res.CpuLimit = formValues.CpuLimit; + res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); + KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims); + KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations); + return res; + } + + /** + * Generate CREATE payload from DaemonSet + * @param {KubernetesDaemonSetPayload} model DaemonSet to genereate payload from + */ + static createPayload(daemonSet) { + const payload = new KubernetesDaemonSetCreatePayload(); + payload.metadata.name = daemonSet.Name; + payload.metadata.namespace = daemonSet.Namespace; + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = daemonSet.StackName; + payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName; + payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = daemonSet.ApplicationOwner; + payload.metadata.annotations[KubernetesPortainerApplicationNote] = daemonSet.Note; + payload.spec.replicas = daemonSet.ReplicaCount; + payload.spec.selector.matchLabels.app = daemonSet.Name; + payload.spec.template.metadata.labels.app = daemonSet.Name; + payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = daemonSet.ApplicationName; + payload.spec.template.spec.containers[0].name = daemonSet.Name; + payload.spec.template.spec.containers[0].image = daemonSet.Image; + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', daemonSet.Env); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', daemonSet.VolumeMounts); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', daemonSet.Volumes); + if (daemonSet.MemoryLimit) { + payload.spec.template.spec.containers[0].resources.limits.memory = daemonSet.MemoryLimit; + payload.spec.template.spec.containers[0].resources.requests.memory = daemonSet.MemoryLimit; + } + if (daemonSet.CpuLimit) { + payload.spec.template.spec.containers[0].resources.limits.cpu = daemonSet.CpuLimit; + payload.spec.template.spec.containers[0].resources.requests.cpu = daemonSet.CpuLimit; + } + if (!daemonSet.CpuLimit && !daemonSet.MemoryLimit) { + delete payload.spec.template.spec.containers[0].resources; + } + return payload; + } + + static patchPayload(oldDaemonSet, newDaemonSet) { + const oldPayload = KubernetesDaemonSetConverter.createPayload(oldDaemonSet); + const newPayload = KubernetesDaemonSetConverter.createPayload(newDaemonSet); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesDaemonSetConverter; diff --git a/app/kubernetes/converters/deployment.js b/app/kubernetes/converters/deployment.js new file mode 100644 index 000000000..a38126d5e --- /dev/null +++ b/app/kubernetes/converters/deployment.js @@ -0,0 +1,80 @@ +import * as JsonPatch from 'fast-json-patch'; +import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; +import { KubernetesDeploymentCreatePayload } from 'Kubernetes/models/deployment/payloads'; +import { + KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationOwnerLabel, + KubernetesPortainerApplicationNote, +} from 'Kubernetes/models/application/models'; + +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; + +class KubernetesDeploymentConverter { + /** + * Generate KubernetesDeployment from KubernetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToDeployment(formValues, volumeClaims) { + const res = new KubernetesDeployment(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + res.ReplicaCount = formValues.ReplicaCount; + res.Image = formValues.Image; + res.CpuLimit = formValues.CpuLimit; + res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); + KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims); + KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations); + return res; + } + + /** + * Generate CREATE payload from Deployment + * @param {KubernetesDeploymentPayload} model Deployment to genereate payload from + */ + static createPayload(deployment) { + const payload = new KubernetesDeploymentCreatePayload(); + payload.metadata.name = deployment.Name; + payload.metadata.namespace = deployment.Namespace; + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = deployment.StackName; + payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName; + payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = deployment.ApplicationOwner; + payload.metadata.annotations[KubernetesPortainerApplicationNote] = deployment.Note; + payload.spec.replicas = deployment.ReplicaCount; + payload.spec.selector.matchLabels.app = deployment.Name; + payload.spec.template.metadata.labels.app = deployment.Name; + payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = deployment.ApplicationName; + payload.spec.template.spec.containers[0].name = deployment.Name; + payload.spec.template.spec.containers[0].image = deployment.Image; + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', deployment.Env); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', deployment.VolumeMounts); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', deployment.Volumes); + if (deployment.MemoryLimit) { + payload.spec.template.spec.containers[0].resources.limits.memory = deployment.MemoryLimit; + payload.spec.template.spec.containers[0].resources.requests.memory = deployment.MemoryLimit; + } + if (deployment.CpuLimit) { + payload.spec.template.spec.containers[0].resources.limits.cpu = deployment.CpuLimit; + payload.spec.template.spec.containers[0].resources.requests.cpu = deployment.CpuLimit; + } + if (!deployment.CpuLimit && !deployment.MemoryLimit) { + delete payload.spec.template.spec.containers[0].resources; + } + return payload; + } + + static patchPayload(oldDeployment, newDeployment) { + const oldPayload = KubernetesDeploymentConverter.createPayload(oldDeployment); + const newPayload = KubernetesDeploymentConverter.createPayload(newDeployment); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesDeploymentConverter; diff --git a/app/kubernetes/converters/event.js b/app/kubernetes/converters/event.js new file mode 100644 index 000000000..5dd3c9d2f --- /dev/null +++ b/app/kubernetes/converters/event.js @@ -0,0 +1,15 @@ +import { KubernetesEvent } from 'Kubernetes/models/event/models'; + +class KubernetesEventConverter { + static apiToEvent(data) { + const res = new KubernetesEvent(); + res.Id = data.metadata.uid; + res.Date = data.lastTimestamp || data.eventTime; + res.Type = data.type; + res.Message = data.message; + res.Involved = data.involvedObject; + return res; + } +} + +export default KubernetesEventConverter; diff --git a/app/kubernetes/converters/namespace.js b/app/kubernetes/converters/namespace.js new file mode 100644 index 000000000..2a95a4c95 --- /dev/null +++ b/app/kubernetes/converters/namespace.js @@ -0,0 +1,29 @@ +import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; +import { KubernetesNamespaceCreatePayload } from 'Kubernetes/models/namespace/payloads'; +import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models'; + +class KubernetesNamespaceConverter { + static apiToNamespace(data, yaml) { + const res = new KubernetesNamespace(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.CreationDate = data.metadata.creationTimestamp; + res.Status = data.status.phase; + res.Yaml = yaml ? yaml.data : ''; + res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : ''; + res.ResourcePoolOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] : ''; + return res; + } + + static createPayload(namespace) { + const res = new KubernetesNamespaceCreatePayload(); + res.metadata.name = namespace.Name; + res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = namespace.ResourcePoolName; + if (namespace.ResourcePoolOwner) { + res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = namespace.ResourcePoolOwner; + } + return res; + } +} + +export default KubernetesNamespaceConverter; diff --git a/app/kubernetes/converters/node.js b/app/kubernetes/converters/node.js new file mode 100644 index 000000000..9a10f00c8 --- /dev/null +++ b/app/kubernetes/converters/node.js @@ -0,0 +1,65 @@ +import _ from 'lodash-es'; + +import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/models/node/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; + +class KubernetesNodeConverter { + static apiToNode(data, res) { + if (!res) { + res = new KubernetesNode(); + } + res.Id = data.metadata.uid; + const hostName = _.find(data.status.addresses, { type: 'Hostname' }); + res.Name = hostName ? hostName.address : data.metadata.Name; + res.Role = _.has(data.metadata.labels, 'node-role.kubernetes.io/master') ? 'Manager' : 'Worker'; + + const ready = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.READY }); + const memoryPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.MEMORY_PRESSURE }); + const PIDPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.PID_PRESSURE }); + const diskPressure = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.DISK_PRESSURE }); + const networkUnavailable = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.NETWORK_UNAVAILABLE }); + + res.Conditions = { + MemoryPressure: memoryPressure && memoryPressure.status === 'True', + PIDPressure: PIDPressure && PIDPressure.status === 'True', + DiskPressure: diskPressure && diskPressure.status === 'True', + NetworkUnavailable: networkUnavailable && networkUnavailable.status === 'True', + }; + + if (ready.status === 'False') { + res.Status = 'Unhealthy'; + } else if (ready.status === 'Unknown' || res.Conditions.MemoryPressure || res.Conditions.PIDPressure || res.Conditions.DiskPressure || res.Conditions.NetworkUnavailable) { + res.Status = 'Warning'; + } else { + res.Status = 'Ready'; + } + + res.CPU = KubernetesResourceReservationHelper.parseCPU(data.status.allocatable.cpu); + res.Memory = data.status.allocatable.memory; + res.Version = data.status.nodeInfo.kubeletVersion; + const internalIP = _.find(data.status.addresses, { type: 'InternalIP' }); + res.IPAddress = internalIP ? internalIP.address : '-'; + return res; + } + + static apiToNodeDetails(data, yaml) { + let res = new KubernetesNodeDetails(); + res = KubernetesNodeConverter.apiToNode(data, res); + res.CreationDate = data.metadata.creationTimestamp; + res.OS.Architecture = data.status.nodeInfo.architecture; + res.OS.Platform = data.status.nodeInfo.operatingSystem; + res.OS.Image = data.status.nodeInfo.osImage; + res.Yaml = yaml ? yaml.data : ''; + return res; + } +} + +export const KubernetesNodeConditionTypes = Object.freeze({ + READY: 'Ready', + MEMORY_PRESSURE: 'MemoryPressure', + PID_PRESSURE: 'PIDPressure', + DISK_PRESSURE: 'DiskPressure', + NETWORK_UNAVAILABLE: 'NetworkUnavailable', +}); + +export default KubernetesNodeConverter; diff --git a/app/kubernetes/converters/persistentVolumeClaim.js b/app/kubernetes/converters/persistentVolumeClaim.js new file mode 100644 index 000000000..29838a86c --- /dev/null +++ b/app/kubernetes/converters/persistentVolumeClaim.js @@ -0,0 +1,68 @@ +import _ from 'lodash-es'; +import * as JsonPatch from 'fast-json-patch'; + +import { KubernetesPersistentVolumeClaim } from 'Kubernetes/models/volume/models'; +import { KubernetesPersistentVolumClaimCreatePayload } from 'Kubernetes/models/volume/payloads'; +import { KubernetesPortainerApplicationOwnerLabel, KubernetesPortainerApplicationNameLabel } from 'Kubernetes/models/application/models'; + +class KubernetesPersistentVolumeClaimConverter { + static apiToPersistentVolumeClaim(data, storageClasses, yaml) { + const res = new KubernetesPersistentVolumeClaim(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.Namespace = data.metadata.namespace; + res.CreationDate = data.metadata.creationTimestamp; + res.Storage = data.spec.resources.requests.storage.replace('i', '') + 'B'; + res.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName }); + res.Yaml = yaml ? yaml.data : ''; + res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : ''; + res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] : ''; + return res; + } + + /** + * Generate KubernetesPersistentVolumeClaim list from KubernetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToVolumeClaims(formValues) { + _.remove(formValues.PersistedFolders, (item) => item.NeedsDeletion); + const res = _.map(formValues.PersistedFolders, (item) => { + const pvc = new KubernetesPersistentVolumeClaim(); + if (item.PersistentVolumeClaimName) { + pvc.Name = item.PersistentVolumeClaimName; + pvc.PreviousName = item.PersistentVolumeClaimName; + } else { + pvc.Name = formValues.Name + '-' + pvc.Name; + } + pvc.MountPath = item.ContainerPath; + pvc.Namespace = formValues.ResourcePool.Namespace.Name; + pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i'; + pvc.StorageClass = item.StorageClass; + pvc.ApplicationOwner = formValues.ApplicationOwner; + pvc.ApplicationName = formValues.Name; + return pvc; + }); + return res; + } + + static createPayload(pvc) { + const res = new KubernetesPersistentVolumClaimCreatePayload(); + res.metadata.name = pvc.Name; + res.metadata.namespace = pvc.Namespace; + res.spec.resources.requests.storage = pvc.Storage; + res.spec.storageClassName = pvc.StorageClass.Name; + res.metadata.labels.app = pvc.ApplicationName; + res.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = pvc.ApplicationOwner; + res.metadata.labels[KubernetesPortainerApplicationNameLabel] = pvc.ApplicationName; + return res; + } + + static patchPayload(oldPVC, newPVC) { + const oldPayload = KubernetesPersistentVolumeClaimConverter.createPayload(oldPVC); + const newPayload = KubernetesPersistentVolumeClaimConverter.createPayload(newPVC); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesPersistentVolumeClaimConverter; diff --git a/app/kubernetes/converters/pod.js b/app/kubernetes/converters/pod.js new file mode 100644 index 000000000..af3d7713b --- /dev/null +++ b/app/kubernetes/converters/pod.js @@ -0,0 +1,32 @@ +import _ from 'lodash-es'; +import { KubernetesPod } from 'Kubernetes/models/pod/models'; +class KubernetesPodConverter { + static computeStatus(statuses) { + const containerStatuses = _.map(statuses, 'state'); + const running = _.filter(containerStatuses, (s) => s.running).length; + const waiting = _.filter(containerStatuses, (s) => s.waiting).length; + if (waiting) { + return 'Waiting'; + } else if (!running) { + return 'Terminated'; + } + return 'Running'; + } + + static apiToPod(data) { + const res = new KubernetesPod(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.Namespace = data.metadata.namespace; + res.Images = _.map(data.spec.containers, 'image'); + res.Status = KubernetesPodConverter.computeStatus(data.status.containerStatuses); + res.Restarts = _.sumBy(data.status.containerStatuses, 'restartCount'); + res.Node = data.spec.nodeName; + res.CreationDate = data.status.startTime; + res.Containers = data.spec.containers; + res.Labels = data.metadata.labels; + return res; + } +} + +export default KubernetesPodConverter; diff --git a/app/kubernetes/converters/resourcePool.js b/app/kubernetes/converters/resourcePool.js new file mode 100644 index 000000000..daf2f002d --- /dev/null +++ b/app/kubernetes/converters/resourcePool.js @@ -0,0 +1,12 @@ +import { KubernetesResourcePool } from 'Kubernetes/models/resource-pool/models'; + +class KubernetesResourcePoolConverter { + static apiToResourcePool(namespace) { + const res = new KubernetesResourcePool(); + res.Namespace = namespace; + res.Yaml = namespace.Yaml; + return res; + } +} + +export default KubernetesResourcePoolConverter; diff --git a/app/kubernetes/converters/resourceQuota.js b/app/kubernetes/converters/resourceQuota.js new file mode 100644 index 000000000..5ba76276f --- /dev/null +++ b/app/kubernetes/converters/resourceQuota.js @@ -0,0 +1,87 @@ +import filesizeParser from 'filesize-parser'; + +import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; +import { KubernetesResourceQuotaCreatePayload, KubernetesResourceQuotaUpdatePayload } from 'Kubernetes/models/resource-quota/payloads'; +import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; +import { KubernetesPortainerResourcePoolNameLabel, KubernetesPortainerResourcePoolOwnerLabel } from 'Kubernetes/models/resource-pool/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; + +class KubernetesResourceQuotaConverter { + static apiToResourceQuota(data, yaml) { + const res = new KubernetesResourceQuota(); + res.Id = data.metadata.uid; + res.Namespace = data.metadata.namespace; + res.Name = data.metadata.name; + res.CpuLimit = 0; + res.MemoryLimit = 0; + if (data.spec.hard && data.spec.hard['limits.cpu']) { + res.CpuLimit = KubernetesResourceReservationHelper.parseCPU(data.spec.hard['limits.cpu']); + } + if (data.spec.hard && data.spec.hard['limits.memory']) { + res.MemoryLimit = filesizeParser(data.spec.hard['limits.memory'], { base: 10 }); + } + + res.MemoryLimitUsed = 0; + if (data.status.used && data.status.used['limits.memory']) { + res.MemoryLimitUsed = filesizeParser(data.status.used['limits.memory'], { base: 10 }); + } + + res.CpuLimitUsed = 0; + if (data.status.used && data.status.used['limits.cpu']) { + res.CpuLimitUsed = KubernetesResourceReservationHelper.parseCPU(data.status.used['limits.cpu']); + } + res.Yaml = yaml ? yaml.data : ''; + res.ResourcePoolName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolNameLabel] : ''; + res.ResourcePoolOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] : ''; + return res; + } + + static createPayload(quota) { + const res = new KubernetesResourceQuotaCreatePayload(); + res.metadata.name = KubernetesResourceQuotaHelper.generateResourceQuotaName(quota.Namespace); + res.metadata.namespace = quota.Namespace; + res.spec.hard['requests.cpu'] = quota.CpuLimit; + res.spec.hard['requests.memory'] = quota.MemoryLimit; + res.spec.hard['limits.cpu'] = quota.CpuLimit; + res.spec.hard['limits.memory'] = quota.MemoryLimit; + res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName; + if (quota.ResourcePoolOwner) { + res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner; + } + if (!quota.CpuLimit || quota.CpuLimit === 0) { + delete res.spec.hard['requests.cpu']; + delete res.spec.hard['limits.cpu']; + } + if (!quota.MemoryLimit || quota.MemoryLimit === 0) { + delete res.spec.hard['requests.memory']; + delete res.spec.hard['limits.memory']; + } + return res; + } + + static updatePayload(quota) { + const res = new KubernetesResourceQuotaUpdatePayload(); + res.metadata.name = quota.Name; + res.metadata.namespace = quota.Namespace; + res.metadata.uid = quota.Id; + res.spec.hard['requests.cpu'] = quota.CpuLimit; + res.spec.hard['requests.memory'] = quota.MemoryLimit; + res.spec.hard['limits.cpu'] = quota.CpuLimit; + res.spec.hard['limits.memory'] = quota.MemoryLimit; + res.metadata.labels[KubernetesPortainerResourcePoolNameLabel] = quota.ResourcePoolName; + if (quota.ResourcePoolOwner) { + res.metadata.labels[KubernetesPortainerResourcePoolOwnerLabel] = quota.ResourcePoolOwner; + } + if (!quota.CpuLimit || quota.CpuLimit === 0) { + delete res.spec.hard['requests.cpu']; + delete res.spec.hard['limits.cpu']; + } + if (!quota.MemoryLimit || quota.MemoryLimit === 0) { + delete res.spec.hard['requests.memory']; + delete res.spec.hard['limits.memory']; + } + return res; + } +} + +export default KubernetesResourceQuotaConverter; diff --git a/app/kubernetes/converters/secret.js b/app/kubernetes/converters/secret.js new file mode 100644 index 000000000..2c85352f0 --- /dev/null +++ b/app/kubernetes/converters/secret.js @@ -0,0 +1,58 @@ +import { KubernetesSecretCreatePayload, KubernetesSecretUpdatePayload } from 'Kubernetes/models/secret/payloads'; +import { KubernetesApplicationSecret } from 'Kubernetes/models/secret/models'; +import YAML from 'yaml'; +import _ from 'lodash-es'; +import { KubernetesPortainerConfigurationOwnerLabel } from 'Kubernetes/models/configuration/models'; + +class KubernetesSecretConverter { + static createPayload(secret) { + const res = new KubernetesSecretCreatePayload(); + res.metadata.name = secret.Name; + res.metadata.namespace = secret.Namespace; + res.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] = secret.ConfigurationOwner; + res.stringData = secret.Data; + return res; + } + + static updatePayload(secret) { + const res = new KubernetesSecretUpdatePayload(); + res.metadata.name = secret.Name; + res.metadata.namespace = secret.Namespace; + res.stringData = secret.Data; + return res; + } + + static apiToSecret(payload, yaml) { + const res = new KubernetesApplicationSecret(); + res.Id = payload.metadata.uid; + res.Name = payload.metadata.name; + res.Namespace = payload.metadata.namespace; + res.ConfigurationOwner = payload.metadata.labels ? payload.metadata.labels[KubernetesPortainerConfigurationOwnerLabel] : ''; + res.CreationDate = payload.metadata.creationTimestamp; + res.Yaml = yaml ? yaml.data : ''; + res.Data = payload.data; + return res; + } + + static configurationFormValuesToSecret(formValues) { + const res = new KubernetesApplicationSecret(); + res.Name = formValues.Name; + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.ConfigurationOwner = formValues.ConfigurationOwner; + if (formValues.IsSimple) { + res.Data = _.reduce( + formValues.Data, + (acc, entry) => { + acc[entry.Key] = entry.Value; + return acc; + }, + {} + ); + } else { + res.Data = YAML.parse(formValues.DataYaml); + } + return res; + } +} + +export default KubernetesSecretConverter; diff --git a/app/kubernetes/converters/service.js b/app/kubernetes/converters/service.js new file mode 100644 index 000000000..a04c04ef0 --- /dev/null +++ b/app/kubernetes/converters/service.js @@ -0,0 +1,88 @@ +import _ from 'lodash-es'; +import * as JsonPatch from 'fast-json-patch'; + +import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads'; +import { + KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationOwnerLabel, +} from 'Kubernetes/models/application/models'; +import { KubernetesServiceHeadlessClusterIP, KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models'; +import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; + +class KubernetesServiceConverter { + static publishedPortToServicePort(name, publishedPort, type) { + const res = new KubernetesServicePort(); + res.name = _.toLower(name + '-' + publishedPort.ContainerPort + '-' + publishedPort.Protocol); + res.port = type === KubernetesServiceTypes.LOAD_BALANCER ? publishedPort.LoadBalancerPort : publishedPort.ContainerPort; + res.targetPort = publishedPort.ContainerPort; + res.protocol = publishedPort.Protocol; + if (type === KubernetesServiceTypes.NODE_PORT && publishedPort.NodePort) { + res.nodePort = publishedPort.NodePort; + } else if (type === KubernetesServiceTypes.LOAD_BALANCER && publishedPort.LoadBalancerNodePort) { + res.nodePort = publishedPort.LoadBalancerNodePort; + } else { + delete res.nodePort; + } + return res; + } + + /** + * Generate KubernetesService from KubernetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToService(formValues) { + const res = new KubernetesService(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) { + res.Type = KubernetesServiceTypes.NODE_PORT; + } else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) { + res.Type = KubernetesServiceTypes.LOAD_BALANCER; + } + res.Ports = _.map(formValues.PublishedPorts, (item) => KubernetesServiceConverter.publishedPortToServicePort(formValues.Name, item, res.Type)); + return res; + } + + static applicationFormValuesToHeadlessService(formValues) { + const res = KubernetesServiceConverter.applicationFormValuesToService(formValues); + res.Name = KubernetesServiceHelper.generateHeadlessServiceName(formValues.Name); + res.Headless = true; + return res; + } + + /** + * Generate CREATE payload from Service + * @param {KubernetesService} model Service to genereate payload from + */ + static createPayload(service) { + const payload = new KubernetesServiceCreatePayload(); + payload.metadata.name = service.Name; + payload.metadata.namespace = service.Namespace; + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName; + payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName; + payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner; + payload.spec.ports = service.Ports; + payload.spec.selector.app = service.ApplicationName; + if (service.Headless) { + payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP; + delete payload.spec.ports; + } else if (service.Type) { + payload.spec.type = service.Type; + } + return payload; + } + + static patchPayload(oldService, newService) { + const oldPayload = KubernetesServiceConverter.createPayload(oldService); + const newPayload = KubernetesServiceConverter.createPayload(newService); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesServiceConverter; diff --git a/app/kubernetes/converters/statefulSet.js b/app/kubernetes/converters/statefulSet.js new file mode 100644 index 000000000..7867af0ee --- /dev/null +++ b/app/kubernetes/converters/statefulSet.js @@ -0,0 +1,84 @@ +import _ from 'lodash-es'; +import * as JsonPatch from 'fast-json-patch'; + +import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; +import { KubernetesStatefulSetCreatePayload } from 'Kubernetes/models/stateful-set/payloads'; +import { + KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationNameLabel, + KubernetesPortainerApplicationOwnerLabel, + KubernetesPortainerApplicationNote, +} from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; +import KubernetesPersistentVolumeClaimConverter from './persistentVolumeClaim'; + +class KubernetesStatefulSetConverter { + /** + * Generate KubernetesStatefulSet from KubernetesApplicationFormValues + * @param {KubernetesApplicationFormValues} formValues + */ + static applicationFormValuesToStatefulSet(formValues, volumeClaims) { + const res = new KubernetesStatefulSet(); + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.Name = formValues.Name; + res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; + res.ApplicationOwner = formValues.ApplicationOwner; + res.ApplicationName = formValues.Name; + res.ReplicaCount = formValues.ReplicaCount; + res.Image = formValues.Image; + res.CpuLimit = formValues.CpuLimit; + res.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + res.Env = KubernetesApplicationHelper.generateEnvFromEnvVariables(formValues.EnvironmentVariables); + KubernetesApplicationHelper.generateVolumesFromPersistentVolumClaims(res, volumeClaims); + KubernetesApplicationHelper.generateEnvOrVolumesFromConfigurations(res, formValues.Configurations); + return res; + } + + /** + * Generate CREATE payload from StatefulSet + * @param {KubernetesStatefulSetPayload} model StatefulSet to genereate payload from + */ + static createPayload(statefulSet) { + const payload = new KubernetesStatefulSetCreatePayload(); + payload.metadata.name = statefulSet.Name; + payload.metadata.namespace = statefulSet.Namespace; + payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = statefulSet.StackName; + payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName; + payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = statefulSet.ApplicationOwner; + payload.metadata.annotations[KubernetesPortainerApplicationNote] = statefulSet.Note; + payload.spec.replicas = statefulSet.ReplicaCount; + payload.spec.serviceName = statefulSet.ServiceName; + payload.spec.selector.matchLabels.app = statefulSet.Name; + payload.spec.volumeClaimTemplates = _.map(statefulSet.VolumeClaims, (item) => KubernetesPersistentVolumeClaimConverter.createPayload(item)); + payload.spec.template.metadata.labels.app = statefulSet.Name; + payload.spec.template.metadata.labels[KubernetesPortainerApplicationNameLabel] = statefulSet.ApplicationName; + payload.spec.template.spec.containers[0].name = statefulSet.Name; + payload.spec.template.spec.containers[0].image = statefulSet.Image; + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].env', statefulSet.Env); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.containers[0].volumeMounts', statefulSet.VolumeMounts); + KubernetesCommonHelper.assignOrDeleteIfEmpty(payload, 'spec.template.spec.volumes', statefulSet.Volumes); + if (statefulSet.MemoryLimit) { + payload.spec.template.spec.containers[0].resources.limits.memory = statefulSet.MemoryLimit; + payload.spec.template.spec.containers[0].resources.requests.memory = statefulSet.MemoryLimit; + } + if (statefulSet.CpuLimit) { + payload.spec.template.spec.containers[0].resources.limits.cpu = statefulSet.CpuLimit; + payload.spec.template.spec.containers[0].resources.requests.cpu = statefulSet.CpuLimit; + } + if (!statefulSet.CpuLimit && !statefulSet.MemoryLimit) { + delete payload.spec.template.spec.containers[0].resources; + } + return payload; + } + + static patchPayload(oldSFS, newSFS) { + const oldPayload = KubernetesStatefulSetConverter.createPayload(oldSFS); + const newPayload = KubernetesStatefulSetConverter.createPayload(newSFS); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } +} + +export default KubernetesStatefulSetConverter; diff --git a/app/kubernetes/converters/storageClass.js b/app/kubernetes/converters/storageClass.js new file mode 100644 index 000000000..2aad0f02e --- /dev/null +++ b/app/kubernetes/converters/storageClass.js @@ -0,0 +1,14 @@ +import { KubernetesStorageClass } from 'Kubernetes/models/storage-class/models'; + +class KubernetesStorageClassConverter { + /** + * API StorageClass to front StorageClass + */ + static apiToStorageClass(data) { + const res = new KubernetesStorageClass(); + res.Name = data.metadata.name; + return res; + } +} + +export default KubernetesStorageClassConverter; diff --git a/app/kubernetes/converters/volume.js b/app/kubernetes/converters/volume.js new file mode 100644 index 000000000..215c8437e --- /dev/null +++ b/app/kubernetes/converters/volume.js @@ -0,0 +1,12 @@ +import { KubernetesVolume } from 'Kubernetes/models/volume/models'; + +class KubernetesVolumeConverter { + static pvcToVolume(claim, pool) { + const res = new KubernetesVolume(); + res.PersistentVolumeClaim = claim; + res.ResourcePool = pool; + return res; + } +} + +export default KubernetesVolumeConverter; diff --git a/app/kubernetes/filters/applicationFilters.js b/app/kubernetes/filters/applicationFilters.js new file mode 100644 index 000000000..968218e06 --- /dev/null +++ b/app/kubernetes/filters/applicationFilters.js @@ -0,0 +1,72 @@ +import _ from 'lodash-es'; +import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models'; + +angular + .module('portainer.kubernetes') + .filter('kubernetesApplicationServiceTypeIcon', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'loadbalancer': + return 'fa-project-diagram'; + case 'clusterip': + return 'fa-list-alt'; + case 'nodeport': + return 'fa-list'; + } + }; + }) + .filter('kubernetesApplicationServiceTypeText', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'loadbalancer': + return 'Load balancer'; + case 'clusterip': + return 'Internal'; + case 'nodeport': + return 'Cluster'; + } + }; + }) + .filter('kubernetesApplicationCPUValue', function () { + 'use strict'; + return function (value) { + return _.round(value, 2); + }; + }) + .filter('kubernetesApplicationDataAccessPolicyIcon', function () { + 'use strict'; + return function (value) { + switch (value) { + case KubernetesApplicationDataAccessPolicies.ISOLATED: + return 'fa-cubes'; + case KubernetesApplicationDataAccessPolicies.SHARED: + return 'fa-cube'; + } + }; + }) + .filter('kubernetesApplicationDataAccessPolicyText', function () { + 'use strict'; + return function (value) { + switch (value) { + case KubernetesApplicationDataAccessPolicies.ISOLATED: + return 'Isolated'; + case KubernetesApplicationDataAccessPolicies.SHARED: + return 'Shared'; + } + }; + }) + .filter('kubernetesApplicationDataAccessPolicyTooltip', function () { + 'use strict'; + return function (value) { + switch (value) { + case KubernetesApplicationDataAccessPolicies.ISOLATED: + return 'All the instances of this application are using their own data.'; + case KubernetesApplicationDataAccessPolicies.SHARED: + return 'All the instances of this application are sharing the same data.'; + } + }; + }); diff --git a/app/kubernetes/filters/configurationFilters.js b/app/kubernetes/filters/configurationFilters.js new file mode 100644 index 000000000..9ba1e1d3a --- /dev/null +++ b/app/kubernetes/filters/configurationFilters.js @@ -0,0 +1,13 @@ +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +angular.module('portainer.kubernetes').filter('kubernetesConfigurationTypeText', function () { + 'use strict'; + return function (type) { + switch (type) { + case KubernetesConfigurationTypes.SECRET: + return 'Sensitive'; + case KubernetesConfigurationTypes.CONFIGMAP: + return 'Non-sensitive'; + } + }; +}); diff --git a/app/kubernetes/filters/eventFilters.js b/app/kubernetes/filters/eventFilters.js new file mode 100644 index 000000000..84148945e --- /dev/null +++ b/app/kubernetes/filters/eventFilters.js @@ -0,0 +1,16 @@ +import _ from 'lodash-es'; + +angular.module('portainer.kubernetes').filter('kubernetesEventTypeColor', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'normal': + return 'info'; + case 'warning': + return 'warning'; + default: + return 'danger'; + } + }; +}); diff --git a/app/kubernetes/filters/filters.js b/app/kubernetes/filters/filters.js new file mode 100644 index 000000000..6fcb031b1 --- /dev/null +++ b/app/kubernetes/filters/filters.js @@ -0,0 +1,11 @@ +angular.module('portainer.kubernetes').filter('kubernetesUsageLevelInfo', function () { + return function (usage) { + if (usage >= 80) { + return 'danger'; + } else if (usage > 50 && usage < 80) { + return 'warning'; + } else { + return 'success'; + } + }; +}); diff --git a/app/kubernetes/filters/nodeFilters.js b/app/kubernetes/filters/nodeFilters.js new file mode 100644 index 000000000..d848a9515 --- /dev/null +++ b/app/kubernetes/filters/nodeFilters.js @@ -0,0 +1,35 @@ +import _ from 'lodash-es'; + +angular + .module('portainer.kubernetes') + .filter('kubernetesNodeStatusColor', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'ready': + return 'success'; + case 'warning': + return 'warning'; + default: + return 'danger'; + } + }; + }) + .filter('kubernetesNodeConditionsMessage', function () { + 'use strict'; + return function (conditions) { + if (conditions.MemoryPressure) { + return 'Node memory is running low'; + } + if (conditions.PIDPressure) { + return 'Too many processes running on the node'; + } + if (conditions.DiskPressure) { + return 'Node disk capacity is running low'; + } + if (conditions.NetworkUnavailable) { + return 'Incorrect node network configuration'; + } + }; + }); diff --git a/app/kubernetes/filters/podFilters.js b/app/kubernetes/filters/podFilters.js new file mode 100644 index 000000000..6d740636d --- /dev/null +++ b/app/kubernetes/filters/podFilters.js @@ -0,0 +1,84 @@ +import _ from 'lodash-es'; + +angular + .module('portainer.kubernetes') + .filter('kubernetesPodStatusColor', function () { + 'use strict'; + return function (text) { + var status = _.toLower(text); + switch (status) { + case 'running': + return 'success'; + case 'waiting': + return 'warning'; + case 'terminated': + return 'info'; + default: + return 'danger'; + } + }; + }) + .filter('kubernetesPodConditionStatusBadge', function () { + 'use strict'; + return function (status, type) { + switch (type) { + case 'Unschedulable': + switch (status) { + case 'True': + return 'fa-exclamation-triangle red-icon'; + case 'False': + return 'fa-check green-icon'; + case 'Unknown': + return 'fa-exclamation-circle orange-icon'; + } + break; + case 'PodScheduled': + case 'Ready': + case 'Initialized': + case 'ContainersReady': + switch (status) { + case 'True': + return 'fa-check green-icon'; + case 'False': + return 'fa-exclamation-triangle red-icon'; + case 'Unknown': + return 'fa-exclamation-circle orange-icon'; + } + break; + default: + return 'fa-question-circle red-icon'; + } + }; + }) + .filter('kubernetesPodConditionStatusText', function () { + 'use strict'; + return function (status, type) { + switch (type) { + case 'Unschedulable': + switch (status) { + case 'True': + return 'Alert'; + case 'False': + return 'OK'; + case 'Unknown': + return 'Warning'; + } + break; + case 'PodScheduled': + case 'Ready': + case 'Initialized': + case 'ContainersReady': + switch (status) { + case 'True': + return 'Ok'; + case 'False': + return 'Alert'; + case 'Unknown': + return 'Warning'; + } + break; + default: + return 'Unknown'; + } + }; + }); diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js new file mode 100644 index 000000000..ab5c43b78 --- /dev/null +++ b/app/kubernetes/helpers/application/index.js @@ -0,0 +1,273 @@ +import _ from 'lodash-es'; +import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models'; +import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; +import { + KubernetesApplicationConfigurationFormValueOverridenKeyTypes, + KubernetesApplicationEnvironmentVariableFormValue, + KubernetesApplicationConfigurationFormValue, + KubernetesApplicationConfigurationFormValueOverridenKey, + KubernetesApplicationPersistedFolderFormValue, + KubernetesApplicationPublishedPortFormValue, +} from 'Kubernetes/models/application/formValues'; +import { + KubernetesApplicationEnvConfigMapPayload, + KubernetesApplicationEnvPayload, + KubernetesApplicationEnvSecretPayload, + KubernetesApplicationVolumeConfigMapPayload, + KubernetesApplicationVolumeEntryPayload, + KubernetesApplicationVolumeMountPayload, + KubernetesApplicationVolumePersistentPayload, + KubernetesApplicationVolumeSecretPayload, +} from 'Kubernetes/models/application/payloads'; +import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; + +class KubernetesApplicationHelper { + static associatePodsAndApplication(pods, app) { + return _.filter(pods, { Labels: app.spec.selector.matchLabels }); + } + + static portMappingsFromApplications(applications) { + const res = _.reduce( + applications, + (acc, app) => { + if (app.PublishedPorts.length > 0) { + const mapping = new KubernetesPortMapping(); + mapping.Name = app.Name; + mapping.ResourcePool = app.ResourcePool; + mapping.ServiceType = app.ServiceType; + mapping.LoadBalancerIPAddress = app.LoadBalancerIPAddress; + mapping.ApplicationOwner = app.ApplicationOwner; + + mapping.Ports = _.map(app.PublishedPorts, (item) => { + const port = new KubernetesPortMappingPort(); + port.Port = mapping.ServiceType === KubernetesServiceTypes.NODE_PORT ? item.nodePort : item.port; + port.TargetPort = item.targetPort; + port.Protocol = item.protocol; + return port; + }); + acc.push(mapping); + } + return acc; + }, + [] + ); + return res; + } + + /** + * FORMVALUES TO APPLICATION FUNCTIONS + */ + static generateEnvFromEnvVariables(envVariables) { + _.remove(envVariables, (item) => item.NeedsDeletion); + const env = _.map(envVariables, (item) => { + const res = new KubernetesApplicationEnvPayload(); + res.name = item.Name; + res.value = item.Value; + return res; + }); + return env; + } + + static generateEnvOrVolumesFromConfigurations(app, configurations) { + let finalEnv = []; + let finalVolumes = []; + let finalMounts = []; + + _.forEach(configurations, (config) => { + const isBasic = config.SelectedConfiguration.Type === KubernetesConfigurationTypes.CONFIGMAP; + + if (!config.Overriden) { + const envKeys = _.keys(config.SelectedConfiguration.Data); + _.forEach(envKeys, (item) => { + const res = isBasic ? new KubernetesApplicationEnvConfigMapPayload() : new KubernetesApplicationEnvSecretPayload(); + res.name = item; + if (isBasic) { + res.valueFrom.configMapKeyRef.name = config.SelectedConfiguration.Name; + res.valueFrom.configMapKeyRef.key = item; + } else { + res.valueFrom.secretKeyRef.name = config.SelectedConfiguration.Name; + res.valueFrom.secretKeyRef.key = item; + } + finalEnv.push(res); + }); + } else { + const envKeys = _.filter(config.OverridenKeys, (item) => item.Type === KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT); + _.forEach(envKeys, (item) => { + const res = isBasic ? new KubernetesApplicationEnvConfigMapPayload() : new KubernetesApplicationEnvSecretPayload(); + res.name = item.Key; + if (isBasic) { + res.valueFrom.configMapKeyRef.name = config.SelectedConfiguration.Name; + res.valueFrom.configMapKeyRef.key = item.Key; + } else { + res.valueFrom.secretKeyRef.name = config.SelectedConfiguration.Name; + res.valueFrom.secretKeyRef.key = item.Key; + } + finalEnv.push(res); + }); + + const volKeys = _.filter(config.OverridenKeys, (item) => item.Type === KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM); + const groupedVolKeys = _.groupBy(volKeys, 'Path'); + _.forEach(groupedVolKeys, (items, path) => { + const volumeName = KubernetesVolumeHelper.generatedApplicationConfigVolumeName(app.Name); + const configurationName = config.SelectedConfiguration.Name; + const itemsMap = _.map(items, (item) => { + const entry = new KubernetesApplicationVolumeEntryPayload(); + entry.key = item.Key; + entry.path = item.Key; + return entry; + }); + + const mount = isBasic ? new KubernetesApplicationVolumeMountPayload() : new KubernetesApplicationVolumeMountPayload(true); + const volume = isBasic ? new KubernetesApplicationVolumeConfigMapPayload() : new KubernetesApplicationVolumeSecretPayload(); + + mount.name = volumeName; + mount.mountPath = path; + volume.name = volumeName; + if (isBasic) { + volume.configMap.name = configurationName; + volume.configMap.items = itemsMap; + } else { + volume.secret.secretName = configurationName; + volume.secret.items = itemsMap; + } + + finalMounts.push(mount); + finalVolumes.push(volume); + }); + } + }); + app.Env = _.concat(app.Env, finalEnv); + app.Volumes = _.concat(app.Volumes, finalVolumes); + app.VolumeMounts = _.concat(app.VolumeMounts, finalMounts); + return app; + } + + static generateVolumesFromPersistentVolumClaims(app, volumeClaims) { + app.VolumeMounts = []; + app.Volumes = []; + _.forEach(volumeClaims, (item) => { + const volumeMount = new KubernetesApplicationVolumeMountPayload(); + const name = item.Name; + volumeMount.name = name; + volumeMount.mountPath = item.MountPath; + app.VolumeMounts.push(volumeMount); + + const volume = new KubernetesApplicationVolumePersistentPayload(); + volume.name = name; + volume.persistentVolumeClaim.claimName = name; + app.Volumes.push(volume); + }); + } + /** + * !FORMVALUES TO APPLICATION FUNCTIONS + */ + + /** + * APPLICATION TO FORMVALUES FUNCTIONS + */ + static generateEnvVariablesFromEnv(env) { + const envVariables = _.map(env, (item) => { + if (!item.value) { + return; + } + const res = new KubernetesApplicationEnvironmentVariableFormValue(); + res.Name = item.name; + res.Value = item.value; + res.IsNew = false; + return res; + }); + return _.without(envVariables, undefined); + } + + static generateConfigurationFormValuesFromEnvAndVolumes(env, volumes, configurations) { + const finalRes = _.flatMap(configurations, (cfg) => { + const filterCondition = cfg.Type === KubernetesConfigurationTypes.CONFIGMAP ? 'valueFrom.configMapKeyRef.name' : 'valueFrom.secretKeyRef.name'; + + const cfgEnv = _.filter(env, [filterCondition, cfg.Name]); + const cfgVol = _.filter(volumes, { configurationName: cfg.Name }); + if (!cfgEnv.length && !cfgVol.length) { + return; + } + const keys = _.reduce( + _.keys(cfg.Data), + (acc, k) => { + const keyEnv = _.filter(cfgEnv, { name: k }); + const keyVol = _.filter(cfgVol, { configurationKey: k }); + const key = { + Key: k, + Count: keyEnv.length + keyVol.length, + Sum: _.concat(keyEnv, keyVol), + EnvCount: keyEnv.length, + VolCount: keyVol.length, + }; + acc.push(key); + return acc; + }, + [] + ); + + const max = _.max(_.map(keys, 'Count')); + const overrideThreshold = max - _.max(_.map(keys, 'VolCount')); + const res = _.map(new Array(max), () => new KubernetesApplicationConfigurationFormValue()); + _.forEach(res, (item, index) => { + item.SelectedConfiguration = cfg; + const overriden = index >= overrideThreshold; + if (overriden) { + item.Overriden = true; + item.OverridenKeys = _.map(keys, (k) => { + const fvKey = new KubernetesApplicationConfigurationFormValueOverridenKey(); + fvKey.Key = k.Key; + if (index < k.EnvCount) { + fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT; + } else { + fvKey.Type = KubernetesApplicationConfigurationFormValueOverridenKeyTypes.FILESYSTEM; + fvKey.Path = k.Sum[index].rootMountPath; + } + return fvKey; + }); + } + }); + return res; + }); + return _.without(finalRes, undefined); + } + + static generatePersistedFoldersFormValuesFromPersistedFolders(persistedFolders, persistentVolumeClaims) { + const finalRes = _.map(persistedFolders, (folder) => { + const pvc = _.find(persistentVolumeClaims, (item) => _.startsWith(item.Name, folder.PersistentVolumeClaimName)); + const res = new KubernetesApplicationPersistedFolderFormValue(pvc.StorageClass); + res.PersistentVolumeClaimName = folder.PersistentVolumeClaimName; + res.Size = parseInt(pvc.Storage.slice(0, -2)); + res.SizeUnit = pvc.Storage.slice(-2); + res.ContainerPath = folder.MountPath; + return res; + }); + return finalRes; + } + + static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) { + const finalRes = _.map(publishedPorts, (port) => { + const res = new KubernetesApplicationPublishedPortFormValue(); + res.Protocol = port.protocol; + res.ContainerPort = port.targetPort; + if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) { + res.LoadBalancerPort = port.port; + res.LoadBalancerNodePort = port.nodePort; + } else if (serviceType === KubernetesServiceTypes.NODE_PORT) { + res.NodePort = port.nodePort; + } + return res; + }); + return finalRes; + } + + /** + * !APPLICATION TO FORMVALUES FUNCTIONS + */ + + static isExternalApplication(application) { + return !application.ApplicationOwner; + } +} +export default KubernetesApplicationHelper; diff --git a/app/kubernetes/helpers/application/rollback.js b/app/kubernetes/helpers/application/rollback.js new file mode 100644 index 000000000..f593e0cf5 --- /dev/null +++ b/app/kubernetes/helpers/application/rollback.js @@ -0,0 +1,76 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; + +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; +import { KubernetesSystem_DefaultDeploymentUniqueLabelKey, KubernetesSystem_AnnotationsToSkip } from 'Kubernetes/models/history/models'; + +class KubernetesApplicationRollbackHelper { + static getPatchPayload(application, targetRevision) { + let result; + + switch (application.ApplicationType) { + case KubernetesApplicationTypes.DEPLOYMENT: + result = KubernetesApplicationRollbackHelper._getDeploymentPayload(application, targetRevision); + break; + case KubernetesApplicationTypes.DAEMONSET: + result = KubernetesApplicationRollbackHelper._getDaemonSetPayload(application, targetRevision); + break; + case KubernetesApplicationTypes.STATEFULSET: + result = KubernetesApplicationRollbackHelper._getStatefulSetPayload(application, targetRevision); + break; + default: + throw new PortainerError('Unable to determine which association to use'); + } + return result; + } + + static _getDeploymentPayload(deploymentApp, targetRevision) { + const target = angular.copy(targetRevision); + const deployment = deploymentApp.Raw; + + // remove hash label before patching back into the deployment + delete target.spec.template.metadata.labels[KubernetesSystem_DefaultDeploymentUniqueLabelKey]; + + // compute deployment annotations + const annotations = {}; + _.forEach(KubernetesSystem_AnnotationsToSkip, (_, k) => { + const v = deployment.metadata.annotations[k]; + if (v) { + annotations[k] = v; + } + }); + _.forEach(target.metadata.annotations, (v, k) => { + if (!KubernetesSystem_AnnotationsToSkip[k]) { + annotations[k] = v; + } + }); + // Create a patch of the Deployment that replaces spec.template + const patch = [ + { + op: 'replace', + path: '/spec/template', + value: target.spec.template, + }, + { + op: 'replace', + path: '/metadata/annotations', + value: annotations, + }, + ]; + + return patch; + } + + static _getDaemonSetPayload(daemonSet, targetRevision) { + void daemonSet; + return targetRevision.data; + } + + static _getStatefulSetPayload(statefulSet, targetRevision) { + void statefulSet; + return targetRevision.data; + } +} + +export default KubernetesApplicationRollbackHelper; diff --git a/app/kubernetes/helpers/commonHelper.js b/app/kubernetes/helpers/commonHelper.js new file mode 100644 index 000000000..af461775f --- /dev/null +++ b/app/kubernetes/helpers/commonHelper.js @@ -0,0 +1,12 @@ +import _ from 'lodash-es'; + +class KubernetesCommonHelper { + static assignOrDeleteIfEmpty(obj, path, value) { + if (!value || (value instanceof Array && !value.length)) { + _.unset(obj, path); + } else { + _.set(obj, path, value); + } + } +} +export default KubernetesCommonHelper; diff --git a/app/kubernetes/helpers/configMapHelper.js b/app/kubernetes/helpers/configMapHelper.js new file mode 100644 index 000000000..6e4eead4e --- /dev/null +++ b/app/kubernetes/helpers/configMapHelper.js @@ -0,0 +1,36 @@ +import _ from 'lodash-es'; + +import { KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models'; +import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access'; + +class KubernetesConfigMapHelper { + static parseJSONData(configMap) { + _.forIn(configMap.Data, (value, key) => { + try { + configMap.Data[key] = JSON.parse(value); + } catch (err) { + configMap.Data[key] = value; + } + }); + return configMap; + } + + static modifiyNamespaceAccesses(configMap, namespace, accesses) { + configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace] = { + UserAccessPolicies: {}, + TeamAccessPolicies: {}, + }; + _.forEach(accesses, (item) => { + if (item instanceof UserAccessViewModel) { + configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace].UserAccessPolicies[item.Id] = { RoleId: 0 }; + } else if (item instanceof TeamAccessViewModel) { + configMap.Data[KubernetesPortainerConfigMapAccessKey][namespace].TeamAccessPolicies[item.Id] = { RoleId: 0 }; + } + }); + _.forIn(configMap.Data, (value, key) => { + configMap.Data[key] = JSON.stringify(value); + }); + return configMap; + } +} +export default KubernetesConfigMapHelper; diff --git a/app/kubernetes/helpers/configurationHelper.js b/app/kubernetes/helpers/configurationHelper.js new file mode 100644 index 000000000..ee8acb9e4 --- /dev/null +++ b/app/kubernetes/helpers/configurationHelper.js @@ -0,0 +1,40 @@ +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; +import _ from 'lodash-es'; + +class KubernetesConfigurationHelper { + static getUsingApplications(config, applications) { + return _.filter(applications, (app) => { + let envFind; + let volumeFind; + if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) { + envFind = _.find(app.Env, { valueFrom: { configMapKeyRef: { name: config.Name } } }); + volumeFind = _.find(app.Volumes, { configMap: { name: config.Name } }); + } else { + envFind = _.find(app.Env, { valueFrom: { secretKeyRef: { name: config.Name } } }); + volumeFind = _.find(app.Volumes, { secret: { secretName: config.Name } }); + } + return envFind || volumeFind; + }); + } + + static isSystemToken(config) { + return _.startsWith(config.Name, 'default-token-'); + } + + static setConfigurationUsed(config) { + config.Used = config.Applications && config.Applications.length !== 0; + } + + static setConfigurationsUsed(configurations, applications) { + _.forEach(configurations, (config) => { + config.Applications = KubernetesConfigurationHelper.getUsingApplications(config, applications); + KubernetesConfigurationHelper.setConfigurationUsed(config); + }); + } + + static isExternalConfiguration(configuration) { + return !configuration.ConfigurationOwner; + } +} + +export default KubernetesConfigurationHelper; diff --git a/app/kubernetes/helpers/eventHelper.js b/app/kubernetes/helpers/eventHelper.js new file mode 100644 index 000000000..7cc0cc865 --- /dev/null +++ b/app/kubernetes/helpers/eventHelper.js @@ -0,0 +1,10 @@ +import _ from 'lodash-es'; + +class KubernetesEventHelper { + static warningCount(events) { + const warnings = _.filter(events, (event) => event.Type === 'Warning'); + return warnings.length; + } +} + +export default KubernetesEventHelper; diff --git a/app/kubernetes/helpers/formValidationHelper.js b/app/kubernetes/helpers/formValidationHelper.js new file mode 100644 index 000000000..cb506e366 --- /dev/null +++ b/app/kubernetes/helpers/formValidationHelper.js @@ -0,0 +1,15 @@ +import _ from 'lodash-es'; + +class KubernetesFormValidationHelper { + static getDuplicates(names) { + const groupped = _.groupBy(names); + const res = {}; + _.forEach(names, (name, index) => { + if (groupped[name].length > 1 && name) { + res[index] = name; + } + }); + return res; + } +} +export default KubernetesFormValidationHelper; diff --git a/app/kubernetes/helpers/history/daemonset.js b/app/kubernetes/helpers/history/daemonset.js new file mode 100644 index 000000000..506a0f0b1 --- /dev/null +++ b/app/kubernetes/helpers/history/daemonset.js @@ -0,0 +1,27 @@ +import _ from 'lodash-es'; + +class KubernetesDaemonSetHistoryHelper { + static _isControlledBy(daemonSet) { + return (item) => _.find(item.metadata.ownerReferences, { uid: daemonSet.metadata.uid }) !== undefined; + } + + static filterOwnedRevisions(crList, daemonSet) { + // filter ControllerRevisions that has the same selector as the DaemonSet + // NOTE : this should be done in HTTP request based on daemonSet.spec.selector.matchLabels + // instead of getting all CR and filtering them here + const sameLabelsCR = _.filter(crList, ['metadata.labels', daemonSet.spec.selector.matchLabels]); + // Only include the RS whose ControllerRef matches the DaemonSet. + const controlledCR = _.filter(sameLabelsCR, KubernetesDaemonSetHistoryHelper._isControlledBy(daemonSet)); + // sorts the list of ControllerRevisions by revision, using the creationTimestamp as a tie breaker (old to new) + const sortedList = _.sortBy(controlledCR, ['revision', 'metadata.creationTimestamp']); + return sortedList; + } + + // getCurrentRS returns the newest CR the given daemonSet targets (latest version) + static getCurrentRevision(crList) { + const current = _.last(crList); + return current; + } +} + +export default KubernetesDaemonSetHistoryHelper; diff --git a/app/kubernetes/helpers/history/deployment.js b/app/kubernetes/helpers/history/deployment.js new file mode 100644 index 000000000..96974eedc --- /dev/null +++ b/app/kubernetes/helpers/history/deployment.js @@ -0,0 +1,56 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import { KubernetesSystem_DefaultDeploymentUniqueLabelKey, KubernetesSystem_RevisionAnnotation } from 'Kubernetes/models/history/models'; + +class KubernetesDeploymentHistoryHelper { + static _isControlledBy(deployment) { + return (item) => _.find(item.metadata.ownerReferences, { uid: deployment.metadata.uid }) !== undefined; + } + + static filterOwnedRevisions(rsList, deployment) { + // filter RS that has the same selector as the Deployment + // NOTE : this should be done in HTTP request based on deployment.spec.selector + // instead of getting all RS and filtering them here + const sameLabelsRS = _.filter(rsList, ['spec.selector', deployment.spec.selector]); + // Only include the RS whose ControllerRef matches the Deployment. + const controlledRS = _.filter(sameLabelsRS, KubernetesDeploymentHistoryHelper._isControlledBy(deployment)); + // sorts the list of ReplicaSet by creation timestamp, using the names as a tie breaker (old to new) + const sortedList = _.sortBy(controlledRS, ['metadata.creationTimestamp', 'metadata.name']); + return sortedList; + } + + // getCurrentRS returns the new RS the given deployment targets (the one with the same pod template). + static getCurrentRevision(rsListOriginal, deployment) { + const rsList = angular.copy(rsListOriginal); + + // In rare cases, such as after cluster upgrades, Deployment may end up with + // having more than one new ReplicaSets that have the same template as its template, + // see https://github.com/kubernetes/kubernetes/issues/40415 + // We deterministically choose the oldest new ReplicaSet (first match) + const current = _.find(rsList, (item) => { + // returns true if two given template.spec are equal, ignoring the diff in value of Labels[pod-template-hash] + // We ignore pod-template-hash because: + // 1. The hash result would be different upon podTemplateSpec API changes + // (e.g. the addition of a new field will cause the hash code to change) + // 2. The deployment template won't have hash labels + delete item.spec.template.metadata.labels[KubernetesSystem_DefaultDeploymentUniqueLabelKey]; + return _.isEqual(deployment.spec.template, item.spec.template); + }); + current.revision = current.metadata.annotations[KubernetesSystem_RevisionAnnotation]; + return current; + } + + // filters the RSList to drop all RS that have never been a version of the Deployment + // also add the revision as a field inside the RS + // Note: this should not impact rollback process as we only patch + // metadata.annotations and spec.template + static filterVersionedRevisions(rsList) { + const filteredRS = _.filter(rsList, (item) => item.metadata.annotations[KubernetesSystem_RevisionAnnotation] !== undefined); + return _.map(filteredRS, (item) => { + item.revision = item.metadata.annotations[KubernetesSystem_RevisionAnnotation]; + return item; + }); + } +} + +export default KubernetesDeploymentHistoryHelper; diff --git a/app/kubernetes/helpers/history/index.js b/app/kubernetes/helpers/history/index.js new file mode 100644 index 000000000..d611644a0 --- /dev/null +++ b/app/kubernetes/helpers/history/index.js @@ -0,0 +1,50 @@ +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; + +import KubernetesDeploymentHistoryHelper from 'Kubernetes/helpers/history/deployment'; +import KubernetesDaemonSetHistoryHelper from 'Kubernetes/helpers/history/daemonset'; +import KubernetesStatefulSetHistoryHelper from 'Kubernetes/helpers/history/statefulset'; +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; + +class KubernetesHistoryHelper { + static getRevisions(rawRevisions, application) { + let currentRevision, revisionsList; + + switch (application.ApplicationType) { + case KubernetesApplicationTypes.DEPLOYMENT: + [currentRevision, revisionsList] = KubernetesHistoryHelper._getDeploymentRevisions(rawRevisions, application.Raw); + break; + case KubernetesApplicationTypes.DAEMONSET: + [currentRevision, revisionsList] = KubernetesHistoryHelper._getDaemonSetRevisions(rawRevisions, application.Raw); + break; + case KubernetesApplicationTypes.STATEFULSET: + [currentRevision, revisionsList] = KubernetesHistoryHelper._getStatefulSetRevisions(rawRevisions, application.Raw); + break; + default: + throw new PortainerError('Unable to determine which association to use'); + } + revisionsList = _.sortBy(revisionsList, 'revision'); + return [currentRevision, revisionsList]; + } + + static _getDeploymentRevisions(rsList, deployment) { + const appRS = KubernetesDeploymentHistoryHelper.filterOwnedRevisions(rsList, deployment); + const currentRS = KubernetesDeploymentHistoryHelper.getCurrentRevision(appRS, deployment); + const versionedRS = KubernetesDeploymentHistoryHelper.filterVersionedRevisions(appRS); + return [currentRS, versionedRS]; + } + + static _getDaemonSetRevisions(crList, daemonSet) { + const appCR = KubernetesDaemonSetHistoryHelper.filterOwnedRevisions(crList, daemonSet); + const currentCR = KubernetesDaemonSetHistoryHelper.getCurrentRevision(appCR, daemonSet); + return [currentCR, appCR]; + } + + static _getStatefulSetRevisions(crList, statefulSet) { + const appCR = KubernetesStatefulSetHistoryHelper.filterOwnedRevisions(crList, statefulSet); + const currentCR = KubernetesStatefulSetHistoryHelper.getCurrentRevision(appCR, statefulSet); + return [currentCR, appCR]; + } +} + +export default KubernetesHistoryHelper; diff --git a/app/kubernetes/helpers/history/statefulset.js b/app/kubernetes/helpers/history/statefulset.js new file mode 100644 index 000000000..829cf0181 --- /dev/null +++ b/app/kubernetes/helpers/history/statefulset.js @@ -0,0 +1,27 @@ +import _ from 'lodash-es'; + +class KubernetesStatefulSetHistoryHelper { + static _isControlledBy(statefulSet) { + return (item) => _.find(item.metadata.ownerReferences, { uid: statefulSet.metadata.uid }) !== undefined; + } + + static filterOwnedRevisions(crList, statefulSet) { + // filter ControllerRevisions that has the same selector as the StatefulSet + // NOTE : this should be done in HTTP request based on statefulSet.spec.selector.matchLabels + // instead of getting all CR and filtering them here + const sameLabelsCR = _.filter(crList, ['metadata.labels', statefulSet.spec.selector.matchLabels]); + // Only include the RS whose ControllerRef matches the StatefulSet. + const controlledCR = _.filter(sameLabelsCR, KubernetesStatefulSetHistoryHelper._isControlledBy(statefulSet)); + // sorts the list of ControllerRevisions by revision, using the creationTimestamp as a tie breaker (old to new) + const sortedList = _.sortBy(controlledCR, ['revision', 'metadata.creationTimestamp']); + return sortedList; + } + + // getCurrentRS returns the newest CR the given statefulSet targets (latest version) + static getCurrentRevision(crList) { + const current = _.last(crList); + return current; + } +} + +export default KubernetesStatefulSetHistoryHelper; diff --git a/app/kubernetes/helpers/namespaceHelper.js b/app/kubernetes/helpers/namespaceHelper.js new file mode 100644 index 000000000..5050590d7 --- /dev/null +++ b/app/kubernetes/helpers/namespaceHelper.js @@ -0,0 +1,15 @@ +import _ from 'lodash-es'; +import angular from 'angular'; + +class KubernetesNamespaceHelper { + constructor(KUBERNETES_SYSTEM_NAMESPACES) { + this.KUBERNETES_SYSTEM_NAMESPACES = KUBERNETES_SYSTEM_NAMESPACES; + } + + isSystemNamespace(namespace) { + return _.includes(this.KUBERNETES_SYSTEM_NAMESPACES, namespace); + } +} + +export default KubernetesNamespaceHelper; +angular.module('portainer.app').service('KubernetesNamespaceHelper', KubernetesNamespaceHelper); diff --git a/app/kubernetes/helpers/resourceQuotaHelper.js b/app/kubernetes/helpers/resourceQuotaHelper.js new file mode 100644 index 000000000..add667dd7 --- /dev/null +++ b/app/kubernetes/helpers/resourceQuotaHelper.js @@ -0,0 +1,9 @@ +import { KubernetesPortainerResourceQuotaPrefix } from 'Kubernetes/models/resource-quota/models'; + +class KubernetesResourceQuotaHelper { + static generateResourceQuotaName(name) { + return KubernetesPortainerResourceQuotaPrefix + name; + } +} + +export default KubernetesResourceQuotaHelper; diff --git a/app/kubernetes/helpers/resourceReservationHelper.js b/app/kubernetes/helpers/resourceReservationHelper.js new file mode 100644 index 000000000..df24ee494 --- /dev/null +++ b/app/kubernetes/helpers/resourceReservationHelper.js @@ -0,0 +1,44 @@ +import _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; +import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models'; + +class KubernetesResourceReservationHelper { + static computeResourceReservation(pods) { + const containers = _.reduce(pods, (acc, pod) => _.concat(acc, pod.Containers), []); + + return _.reduce( + containers, + (acc, container) => { + if (container.resources && container.resources.requests) { + if (container.resources.requests.memory) { + acc.Memory += filesizeParser(container.resources.requests.memory, { base: 10 }); + } + + if (container.resources.requests.cpu) { + acc.CPU += KubernetesResourceReservationHelper.parseCPU(container.resources.requests.cpu); + } + } + + return acc; + }, + new KubernetesResourceReservation() + ); + } + + static parseCPU(cpu) { + let res = parseInt(cpu); + if (_.endsWith(cpu, 'm')) { + res /= 1000; + } + return res; + } + + static megaBytesValue(value) { + return Math.floor(filesizeParser(value) / 1000 / 1000); + } + + static bytesValue(mem) { + return filesizeParser(mem) * 1000 * 1000; + } +} +export default KubernetesResourceReservationHelper; diff --git a/app/kubernetes/helpers/serviceHelper.js b/app/kubernetes/helpers/serviceHelper.js new file mode 100644 index 000000000..c4b35050d --- /dev/null +++ b/app/kubernetes/helpers/serviceHelper.js @@ -0,0 +1,13 @@ +import _ from 'lodash-es'; +import { KubernetesServiceHeadlessPrefix } from 'Kubernetes/models/service/models'; + +class KubernetesServiceHelper { + static generateHeadlessServiceName(name) { + return KubernetesServiceHeadlessPrefix + name; + } + + static findApplicationBoundService(services, rawApp) { + return _.find(services, (item) => _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); + } +} +export default KubernetesServiceHelper; diff --git a/app/kubernetes/helpers/stackHelper.js b/app/kubernetes/helpers/stackHelper.js new file mode 100644 index 000000000..73955862b --- /dev/null +++ b/app/kubernetes/helpers/stackHelper.js @@ -0,0 +1,26 @@ +import _ from 'lodash-es'; +import { KubernetesStack } from 'Kubernetes/models/stack/models'; + +class KubernetesStackHelper { + static stacksFromApplications(applications) { + const res = _.reduce( + applications, + (acc, app) => { + if (app.StackName !== '-') { + let stack = _.find(acc, { Name: app.StackName, ResourcePool: app.ResourcePool }); + if (!stack) { + stack = new KubernetesStack(); + stack.Name = app.StackName; + stack.ResourcePool = app.ResourcePool; + acc.push(stack); + } + stack.Applications.push(app); + } + return acc; + }, + [] + ); + return res; + } +} +export default KubernetesStackHelper; diff --git a/app/kubernetes/helpers/volumeHelper.js b/app/kubernetes/helpers/volumeHelper.js new file mode 100644 index 000000000..c357ffb14 --- /dev/null +++ b/app/kubernetes/helpers/volumeHelper.js @@ -0,0 +1,37 @@ +import _ from 'lodash-es'; +import uuidv4 from 'uuid/v4'; +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; + +class KubernetesVolumeHelper { + // TODO: review + // the following condition + // && (app.ApplicationType === KubernetesApplicationTypes.STATEFULSET ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true); + // is made to enforce finding the good SFS when multiple SFS in the same namespace + // are referencing an internal PVC using the same internal name + // (PVC are not exposed to other apps so they can have the same name in differents SFS) + static getUsingApplications(volume, applications) { + return _.filter(applications, (app) => { + const names = _.without(_.map(app.Volumes, 'persistentVolumeClaim.claimName'), undefined); + const matchingNames = _.filter(names, (name) => _.startsWith(volume.PersistentVolumeClaim.Name, name)); + return ( + volume.ResourcePool.Namespace.Name === app.ResourcePool && + matchingNames.length && + (app.ApplicationType === KubernetesApplicationTypes.STATEFULSET ? _.includes(volume.PersistentVolumeClaim.Name, app.Name) : true) + ); + }); + } + + static isUsed(item) { + return item.Applications.length !== 0; + } + + static generatedApplicationConfigVolumeName(name) { + return 'config-' + name + '-' + uuidv4(); + } + + static isExternalVolume(volume) { + return !volume.PersistentVolumeClaim.ApplicationOwner; + } +} + +export default KubernetesVolumeHelper; diff --git a/app/kubernetes/horizontal-pod-auto-scaler/converter.js b/app/kubernetes/horizontal-pod-auto-scaler/converter.js new file mode 100644 index 000000000..4c5ccea75 --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/converter.js @@ -0,0 +1,23 @@ +import { KubernetesHorizontalPodAutoScaler } from './models'; + +export class KubernetesHorizontalPodAutoScalerConverter { + /** + * Convert API data to KubernetesHorizontalPodAutoScaler model + */ + static apiToModel(data, yaml) { + const res = new KubernetesHorizontalPodAutoScaler(); + res.Id = data.metadata.uid; + res.Namespace = data.metadata.namespace; + res.Name = data.metadata.name; + res.MinReplicas = data.spec.minReplicas; + res.MaxReplicas = data.spec.maxReplicas; + res.TargetCPUUtilizationPercentage = data.spec.targetCPUUtilizationPercentage; + if (data.spec.scaleTargetRef) { + res.TargetEntity.ApiVersion = data.spec.scaleTargetRef.apiVersion; + res.TargetEntity.Kind = data.spec.scaleTargetRef.kind; + res.TargetEntity.Name = data.spec.scaleTargetRef.name; + } + res.Yaml = yaml ? yaml.data : ''; + return res; + } +} diff --git a/app/kubernetes/horizontal-pod-auto-scaler/helper.js b/app/kubernetes/horizontal-pod-auto-scaler/helper.js new file mode 100644 index 000000000..af8061b85 --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/helper.js @@ -0,0 +1,26 @@ +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import { KubernetesApplication, KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models'; +import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; +import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; +import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; + +function _getApplicationTypeString(app) { + if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT) || app instanceof KubernetesDeployment) { + return KubernetesApplicationTypeStrings.DEPLOYMENT; + } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET) || app instanceof KubernetesDaemonSet) { + return KubernetesApplicationTypeStrings.DAEMONSET; + } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) { + return KubernetesApplicationTypeStrings.STATEFULSET; + // } else if () { ---> TODO: refactor - handle bare pod type ! + } else { + throw new PortainerError('Unable to determine application type'); + } +} + +export class KubernetesHorizontalPodAutoScalerHelper { + static findApplicationBoundScaler(sList, app) { + const kind = _getApplicationTypeString(app); + return _.find(sList, (item) => item.TargetEntity.Kind === kind && item.TargetEntity.Name === app.Name); + } +} diff --git a/app/kubernetes/horizontal-pod-auto-scaler/models.js b/app/kubernetes/horizontal-pod-auto-scaler/models.js new file mode 100644 index 000000000..1cba85d7d --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/models.js @@ -0,0 +1,23 @@ +/** + * KubernetesHorizontalPodAutoScaler Model + */ +const _KubernetesHorizontalPodAutoScaler = Object.freeze({ + Id: '', + Namespace: '', + Name: '', + MinReplicas: 1, + MaxReplicas: 1, + TargetCPUUtilizationPercentage: undefined, + TargetEntity: { + ApiVersion: '', + Kind: '', + Name: '', + }, + Yaml: '', +}); + +export class KubernetesHorizontalPodAutoScaler { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScaler))); + } +} diff --git a/app/kubernetes/horizontal-pod-auto-scaler/rest.js b/app/kubernetes/horizontal-pod-auto-scaler/rest.js new file mode 100644 index 000000000..a6cdc4cb1 --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/rest.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesHorizontalPodAutoScalers', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesHorizontalPodAutoScalersFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/autoscaling/v1' + (namespace ? '/namespaces/:namespace' : '') + '/horizontalpodautoscalers/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/horizontal-pod-auto-scaler/service.js b/app/kubernetes/horizontal-pod-auto-scaler/service.js new file mode 100644 index 000000000..df33457ce --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/service.js @@ -0,0 +1,135 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import { KubernetesHorizontalPodAutoScalerConverter } from './converter'; + +class KubernetesHorizontalPodAutoScalerService { + /* @ngInject */ + constructor($async, KubernetesHorizontalPodAutoScalers) { + this.$async = $async; + this.KubernetesHorizontalPodAutoScalers = KubernetesHorizontalPodAutoScalers; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + // this.createAsync = this.createAsync.bind(this); + // this.patchAsync = this.patchAsync.bind(this); + // this.rollbackAsync = this.rollbackAsync.bind(this); + // this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([ + this.KubernetesHorizontalPodAutoScalers(namespace).get(params).$promise, + this.KubernetesHorizontalPodAutoScalers(namespace).getYaml(params).$promise, + ]); + const res = KubernetesHorizontalPodAutoScalerConverter.apiToModel(raw, yaml); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve HorizontalPodAutoScaler', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesHorizontalPodAutoScalers(namespace).get().$promise; + const res = _.map(data.items, (item) => KubernetesHorizontalPodAutoScalerConverter.apiToModel(item)); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve HorizontalPodAutoScalers', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + // /** + // * CREATE + // */ + // async createAsync(horizontalPodAutoScaler) { + // try { + // const params = {}; + // const payload = KubernetesHorizontalPodAutoScalerConverter.createPayload(horizontalPodAutoScaler); + // const namespace = payload.metadata.namespace; + // const data = await this.KubernetesHorizontalPodAutoScalers(namespace).create(params, payload).$promise; + // return data; + // } catch (err) { + // throw new PortainerError('Unable to create horizontalPodAutoScaler', err); + // } + // } + + // create(horizontalPodAutoScaler) { + // return this.$async(this.createAsync, horizontalPodAutoScaler); + // } + + // /** + // * PATCH + // */ + // async patchAsync(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { + // try { + // const params = new KubernetesCommonParams(); + // params.id = newHorizontalPodAutoScaler.Name; + // const namespace = newHorizontalPodAutoScaler.Namespace; + // const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); + // if (!payload.length) { + // return; + // } + // const data = await this.KubernetesHorizontalPodAutoScalers(namespace).patch(params, payload).$promise; + // return data; + // } catch (err) { + // throw new PortainerError('Unable to patch horizontalPodAutoScaler', err); + // } + // } + + // patch(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { + // return this.$async(this.patchAsync, oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); + // } + + // /** + // * DELETE + // */ + // async deleteAsync(horizontalPodAutoScaler) { + // try { + // const params = new KubernetesCommonParams(); + // params.id = horizontalPodAutoScaler.Name; + // const namespace = horizontalPodAutoScaler.Namespace; + // await this.KubernetesHorizontalPodAutoScalers(namespace).delete(params).$promise; + // } catch (err) { + // throw new PortainerError('Unable to remove horizontalPodAutoScaler', err); + // } + // } + + // delete(horizontalPodAutoScaler) { + // return this.$async(this.deleteAsync, horizontalPodAutoScaler); + // } + + // /** + // * ROLLBACK + // */ + // async rollbackAsync(namespace, name, payload) { + // try { + // const params = new KubernetesCommonParams(); + // params.id = name; + // await this.KubernetesHorizontalPodAutoScalers(namespace).rollback(params, payload).$promise; + // } catch (err) { + // throw new PortainerError('Unable to rollback horizontalPodAutoScaler', err); + // } + // } + + // rollback(namespace, name, payload) { + // return this.$async(this.rollbackAsync, namespace, name, payload); + // } +} + +export default KubernetesHorizontalPodAutoScalerService; +angular.module('portainer.kubernetes').service('KubernetesHorizontalPodAutoScalerService', KubernetesHorizontalPodAutoScalerService); diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js new file mode 100644 index 000000000..72af5bbf2 --- /dev/null +++ b/app/kubernetes/models/application/formValues.js @@ -0,0 +1,118 @@ +import { KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationDataAccessPolicies } from './models'; + +/** + * KubernetesApplicationFormValues Model + */ +const _KubernetesApplicationFormValues = Object.freeze({ + ApplicationType: undefined, // will only exist for formValues generated from Application (app edit situation) + ResourcePool: {}, + Name: '', + StackName: '', + ApplicationOwner: '', + Image: '', + ReplicaCount: 1, + Note: '', + EnvironmentVariables: [], // KubernetesApplicationEnvironmentVariableFormValue list + PersistedFolders: [], // KubernetesApplicationPersistedFolderFormValue list + PublishedPorts: [], // KubernetesApplicationPublishedPortFormValue list + MemoryLimit: 0, + CpuLimit: 0, + DeploymentType: KubernetesApplicationDeploymentTypes.REPLICATED, + PublishingType: KubernetesApplicationPublishingTypes.INTERNAL, + DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED, + Configurations: [], // KubernetesApplicationConfigurationFormValue list +}); + +export class KubernetesApplicationFormValues { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationFormValues))); + } +} + +export const KubernetesApplicationConfigurationFormValueOverridenKeyTypes = Object.freeze({ + ENVIRONMENT: 1, + FILESYSTEM: 2, +}); + +/** + * KubernetesApplicationConfigurationFormValueOverridenKey Model + */ +const _KubernetesApplicationConfigurationFormValueOverridenKey = Object.freeze({ + Key: '', + Path: '', + Type: KubernetesApplicationConfigurationFormValueOverridenKeyTypes.ENVIRONMENT, +}); + +export class KubernetesApplicationConfigurationFormValueOverridenKey { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationFormValueOverridenKey))); + } +} + +/** + * KubernetesApplicationConfigurationFormValue Model + */ +const _KubernetesApplicationConfigurationFormValue = Object.freeze({ + SelectedConfiguration: undefined, + Overriden: false, + OverridenKeys: [], // KubernetesApplicationConfigurationFormValueOverridenKey list +}); + +export class KubernetesApplicationConfigurationFormValue { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationFormValue))); + } +} + +/** + * KubernetesApplicationEnvironmentVariableFormValue Model + */ +const _KubernetesApplicationEnvironmentVariableFormValue = Object.freeze({ + Name: '', + Value: '', + IsSecret: false, + NeedsDeletion: false, + IsNew: true, +}); + +export class KubernetesApplicationEnvironmentVariableFormValue { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvironmentVariableFormValue))); + } +} + +/** + * KubernetesApplicationPersistedFolderFormValue Model + */ +const _KubernetesApplicationPersistedFolderFormValue = Object.freeze({ + PersistentVolumeClaimName: '', // will be empty for new volumes (create/edit app) and filled for existing ones (edit) + NeedsDeletion: false, + ContainerPath: '', + Size: '', + SizeUnit: 'GB', + StorageClass: {}, +}); + +export class KubernetesApplicationPersistedFolderFormValue { + constructor(storageClass) { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPersistedFolderFormValue))); + this.StorageClass = storageClass; + } +} + +/** + * KubernetesApplicationPublishedPortFormValue Model + */ +const _KubernetesApplicationPublishedPortFormValue = Object.freeze({ + ContainerPort: '', + NodePort: '', + LoadBalancerPort: '', + LoadBalancerNodePort: undefined, // only filled to save existing loadbalancer nodePort and drop it when moving app exposure from LB to Internal/NodePort + Protocol: 'TCP', +}); + +export class KubernetesApplicationPublishedPortFormValue { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPublishedPortFormValue))); + } +} diff --git a/app/kubernetes/models/application/models.js b/app/kubernetes/models/application/models.js new file mode 100644 index 000000000..3589a5ceb --- /dev/null +++ b/app/kubernetes/models/application/models.js @@ -0,0 +1,114 @@ +export const KubernetesApplicationDeploymentTypes = Object.freeze({ + REPLICATED: 1, + GLOBAL: 2, +}); + +export const KubernetesApplicationDataAccessPolicies = Object.freeze({ + SHARED: 1, + ISOLATED: 2, +}); + +export const KubernetesApplicationTypes = Object.freeze({ + DEPLOYMENT: 1, + DAEMONSET: 2, + STATEFULSET: 3, +}); + +export const KubernetesApplicationTypeStrings = Object.freeze({ + DEPLOYMENT: 'Deployment', + DAEMONSET: 'DaemonSet', + STATEFULSET: 'StatefulSet', +}); + +export const KubernetesApplicationPublishingTypes = Object.freeze({ + INTERNAL: 1, + CLUSTER: 2, + LOAD_BALANCER: 3, +}); + +export const KubernetesApplicationQuotaDefaults = { + CpuLimit: 0.1, + MemoryLimit: 64, // MB +}; + +export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack'; + +export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name'; + +export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner'; + +export const KubernetesPortainerApplicationNote = 'io.portainer.kubernetes.application.note'; + +/** + * KubernetesApplication Model (Composite) + */ +const _KubernetesApplication = Object.freeze({ + Id: '', + Name: '', + StackName: '', + ApplicationOwner: '', + ApplicationName: '', + ResourcePool: '', + Image: '', + CreationDate: 0, + Pods: [], + Limits: {}, + ServiceType: '', + ServiceId: '', + ServiceName: '', + HeadlessServiceName: undefined, // only used for StatefulSet + LoadBalancerIPAddress: undefined, // only filled when bound service is LoadBalancer and state is available + PublishedPorts: [], + Volumes: [], + Env: [], + PersistedFolders: [], // KubernetesApplicationPersistedFolder list + ConfigurationVolumes: [], // KubernetesApplicationConfigurationVolume list + DeploymentType: 'Unknown', + DataAccessPolicy: 'Unknown', + ApplicationType: 'Unknown', + RunningPodsCount: 0, + TotalPodsCount: 0, + Yaml: '', + Note: '', + Revisions: undefined, + CurrentRevision: undefined, + Raw: undefined, // only filled when inspecting app details / create / edit view (never filled in multiple-apps views) + AutoScaler: undefined, // only filled if the application has an HorizontalPodAutoScaler bound to it +}); + +export class KubernetesApplication { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplication))); + } +} + +/** + * KubernetesApplicationPersistedFolder Model + */ +const _KubernetesApplicationPersistedFolder = Object.freeze({ + MountPath: '', + PersistentVolumeClaimName: '', + HostPath: '', +}); + +export class KubernetesApplicationPersistedFolder { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPersistedFolder))); + } +} + +/** + * KubernetesApplicationConfigurationVolume Model + */ +const _KubernetesApplicationConfigurationVolume = Object.freeze({ + fileMountPath: '', + rootMountPath: '', + configurationKey: '', + configurationName: '', +}); + +export class KubernetesApplicationConfigurationVolume { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationVolume))); + } +} diff --git a/app/kubernetes/models/application/payloads.js b/app/kubernetes/models/application/payloads.js new file mode 100644 index 000000000..4671e9bf9 --- /dev/null +++ b/app/kubernetes/models/application/payloads.js @@ -0,0 +1,142 @@ +/////////////////////////// VOLUME MOUNT /////////////////////////////// +/** + * KubernetesApplicationVolumeMount Model + */ +const _KubernetesApplicationVolumeMount = Object.freeze({ + name: '', + mountPath: '', + readOnly: false, +}); + +export class KubernetesApplicationVolumeMountPayload { + constructor(readOnly) { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeMount))); + if (readOnly) { + this.readOnly = true; + } else { + delete this.readOnly; + } + } +} + +///////////////////////////////// PVC ///////////////////////////////// +/** + * KubernetesApplicationVolumePersistentPayload Model + */ +const _KubernetesApplicationVolumePersistentPayload = Object.freeze({ + name: '', + persistentVolumeClaim: { + claimName: '', + }, +}); + +export class KubernetesApplicationVolumePersistentPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumePersistentPayload))); + } +} + +/////////////////////////////// CONFIG AS VOLUME //////////////////////// +/** + * KubernetesApplicationVolumeConfigMapPayload Model + */ +const _KubernetesApplicationVolumeConfigMapPayload = Object.freeze({ + name: '', + configMap: { + name: '', + items: [], // KubernetesApplicationVolumeEntryPayload + }, +}); + +export class KubernetesApplicationVolumeConfigMapPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeConfigMapPayload))); + } +} + +//////////////////// SECRET AS VOLUME ///////////////////////////////////// +/** + * KubernetesApplicationVolumeSecretPayload Model + */ +const _KubernetesApplicationVolumeSecretPayload = Object.freeze({ + name: '', + secret: { + secretName: '', + items: [], // KubernetesApplicationVolumeEntryPayload + }, +}); + +export class KubernetesApplicationVolumeSecretPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeSecretPayload))); + } +} + +/** + * KubernetesApplicationVolumeEntryPayload Model + */ +const _KubernetesApplicationVolumeEntryPayload = Object.freeze({ + key: '', + path: '', +}); + +export class KubernetesApplicationVolumeEntryPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationVolumeEntryPayload))); + } +} + +//////////////////////////// ENV ENTRY ////////////////////////////// +/** + * KubernetesApplicationEnvPayload Model + */ +const _KubernetesApplicationEnvPayload = Object.freeze({ + name: '', + value: '', +}); + +export class KubernetesApplicationEnvPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvPayload))); + } +} + +///////////////////////// CONFIG AS ENV //////////////////////////////// +/** + * KubernetesApplicationEnvConfigMapPayload Model + */ +const _KubernetesApplicationEnvConfigMapPayload = Object.freeze({ + name: '', + valueFrom: { + configMapKeyRef: { + name: '', + key: '', + }, + }, +}); + +export class KubernetesApplicationEnvConfigMapPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvConfigMapPayload))); + } +} + +//////////////////////// SECRET AS ENV ////////////////////////////////// +/** + * KubernetesApplicationEnvSecretPayload Model + */ +const _KubernetesApplicationEnvSecretPayload = Object.freeze({ + name: '', + valueFrom: { + secretKeyRef: { + name: '', + key: '', + }, + }, +}); + +export class KubernetesApplicationEnvSecretPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationEnvSecretPayload))); + } +} diff --git a/app/kubernetes/models/common/params.js b/app/kubernetes/models/common/params.js new file mode 100644 index 000000000..fd37b8764 --- /dev/null +++ b/app/kubernetes/models/common/params.js @@ -0,0 +1,11 @@ +/** + * Generic params + */ +const _KubernetesCommonParams = Object.freeze({ + id: '', +}); +export class KubernetesCommonParams { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonParams))); + } +} diff --git a/app/kubernetes/models/common/payloads.js b/app/kubernetes/models/common/payloads.js new file mode 100644 index 000000000..28909d338 --- /dev/null +++ b/app/kubernetes/models/common/payloads.js @@ -0,0 +1,15 @@ +/** + * Generic metadata payload + */ +const _KubernetesCommonMetadataPayload = Object.freeze({ + uid: '', + name: '', + namespace: '', + labels: {}, + annotations: {}, +}); +export class KubernetesCommonMetadataPayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesCommonMetadataPayload))); + } +} diff --git a/app/kubernetes/models/config-map/models.js b/app/kubernetes/models/config-map/models.js new file mode 100644 index 000000000..b0463042c --- /dev/null +++ b/app/kubernetes/models/config-map/models.js @@ -0,0 +1,21 @@ +export const KubernetesPortainerConfigMapNamespace = 'portainer'; +export const KubernetesPortainerConfigMapConfigName = 'portainer-config'; +export const KubernetesPortainerConfigMapAccessKey = 'NamespaceAccessPolicies'; + +/** + * ConfigMap Model + */ +const _KubernetesConfigMap = Object.freeze({ + Id: 0, + Name: '', + Namespace: '', + Yaml: '', + ConfigurationOwner: '', + Data: {}, +}); + +export class KubernetesConfigMap { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigMap))); + } +} diff --git a/app/kubernetes/models/config-map/payloads.js b/app/kubernetes/models/config-map/payloads.js new file mode 100644 index 000000000..14e8fe708 --- /dev/null +++ b/app/kubernetes/models/config-map/payloads.js @@ -0,0 +1,27 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * Payload for CREATE + */ +const _KubernetesConfigMapCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + data: {}, +}); +export class KubernetesConfigMapCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigMapCreatePayload))); + } +} + +/** + * Payload for UPDATE + */ +const _KubernetesConfigMapUpdatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + data: {}, +}); +export class KubernetesConfigMapUpdatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigMapUpdatePayload))); + } +} diff --git a/app/kubernetes/models/configuration/formvalues.js b/app/kubernetes/models/configuration/formvalues.js new file mode 100644 index 000000000..859542651 --- /dev/null +++ b/app/kubernetes/models/configuration/formvalues.js @@ -0,0 +1,35 @@ +import { KubernetesConfigurationTypes } from './models'; + +/** + * KubernetesConfigurationFormValues Model + */ +const _KubernetesConfigurationFormValues = Object.freeze({ + Id: '', + ResourcePool: '', + Name: '', + ConfigurationOwner: '', + Type: KubernetesConfigurationTypes.CONFIGMAP, + Data: [], + DataYaml: '', + IsSimple: true, +}); + +export class KubernetesConfigurationFormValues { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigurationFormValues))); + } +} + +/** + * KubernetesConfigurationEntry Model + */ +const _KubernetesConfigurationFormValuesDataEntry = Object.freeze({ + Key: '', + Value: '', +}); + +export class KubernetesConfigurationFormValuesDataEntry { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfigurationFormValuesDataEntry))); + } +} diff --git a/app/kubernetes/models/configuration/models.js b/app/kubernetes/models/configuration/models.js new file mode 100644 index 000000000..08c5ae90a --- /dev/null +++ b/app/kubernetes/models/configuration/models.js @@ -0,0 +1,27 @@ +export const KubernetesPortainerConfigurationOwnerLabel = 'io.portainer.kubernetes.configuration.owner'; + +/** + * Configuration Model (Composite) + */ +const _KubernetesConfiguration = Object.freeze({ + Id: 0, + Name: '', + Type: '', + Namespace: '', + CreationDate: '', + ConfigurationOwner: '', + Used: false, + Applications: [], + Data: {}, +}); + +export class KubernetesConfiguration { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesConfiguration))); + } +} + +export const KubernetesConfigurationTypes = Object.freeze({ + CONFIGMAP: 1, + SECRET: 2, +}); diff --git a/app/kubernetes/models/daemon-set/models.js b/app/kubernetes/models/daemon-set/models.js new file mode 100644 index 000000000..ba0e4d6cd --- /dev/null +++ b/app/kubernetes/models/daemon-set/models.js @@ -0,0 +1,24 @@ +/** + * KubernetesDaemonSet Model + */ +const _KubernetesDaemonSet = Object.freeze({ + Namespace: '', + Name: '', + StackName: '', + Image: '', + Env: [], + CpuLimit: 0, + MemoryLimit: 0, + VoluemMounts: [], + Volumes: [], + Secret: undefined, + ApplicationName: '', + ApplicationOwner: '', + Note: '', +}); + +export class KubernetesDaemonSet { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesDaemonSet))); + } +} diff --git a/app/kubernetes/models/daemon-set/payloads.js b/app/kubernetes/models/daemon-set/payloads.js new file mode 100644 index 000000000..737fbc122 --- /dev/null +++ b/app/kubernetes/models/daemon-set/payloads.js @@ -0,0 +1,50 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesDaemonSetCreatePayload Model + */ +const _KubernetesDaemonSetCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + replicas: 0, + selector: { + matchLabels: { + app: '', + }, + }, + updateStrategy: { + type: 'RollingUpdate', + rollingUpdate: { + maxUnavailable: 1, + }, + }, + template: { + metadata: { + labels: { + app: '', + }, + }, + spec: { + containers: [ + { + name: '', + image: '', + env: [], + resources: { + limits: {}, + requests: {}, + }, + volumeMounts: [], + }, + ], + volumes: [], + }, + }, + }, +}); + +export class KubernetesDaemonSetCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesDaemonSetCreatePayload))); + } +} diff --git a/app/kubernetes/models/deploy.js b/app/kubernetes/models/deploy.js new file mode 100644 index 000000000..d9e0d8363 --- /dev/null +++ b/app/kubernetes/models/deploy.js @@ -0,0 +1,4 @@ +export const KubernetesDeployManifestTypes = Object.freeze({ + KUBERNETES: 1, + COMPOSE: 2, +}); diff --git a/app/kubernetes/models/deployment/models.js b/app/kubernetes/models/deployment/models.js new file mode 100644 index 000000000..05161e929 --- /dev/null +++ b/app/kubernetes/models/deployment/models.js @@ -0,0 +1,25 @@ +/** + * KubernetesDeployment Model + */ +const _KubernetesDeployment = Object.freeze({ + Namespace: '', + Name: '', + StackName: '', + ReplicaCount: 0, + Image: '', + Env: [], + CpuLimit: 0, + MemoryLimit: 0, + VolumeMounts: [], + Volumes: [], + Secret: undefined, + ApplicationName: '', + ApplicationOwner: '', + Note: '', +}); + +export class KubernetesDeployment { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesDeployment))); + } +} diff --git a/app/kubernetes/models/deployment/payloads.js b/app/kubernetes/models/deployment/payloads.js new file mode 100644 index 000000000..89047b08f --- /dev/null +++ b/app/kubernetes/models/deployment/payloads.js @@ -0,0 +1,51 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesDeploymentCreatePayload Model + */ +const _KubernetesDeploymentCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + replicas: 0, + selector: { + matchLabels: { + app: '', + }, + }, + strategy: { + type: 'RollingUpdate', + rollingUpdate: { + maxSurge: 0, + maxUnavailable: '100%', + }, + }, + template: { + metadata: { + labels: { + app: '', + }, + }, + spec: { + containers: [ + { + name: '', + image: '', + env: [], + resources: { + limits: {}, + requests: {}, + }, + volumeMounts: [], + }, + ], + volumes: [], + }, + }, + }, +}); + +export class KubernetesDeploymentCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesDeploymentCreatePayload))); + } +} diff --git a/app/kubernetes/models/event/models.js b/app/kubernetes/models/event/models.js new file mode 100644 index 000000000..f79c8e43b --- /dev/null +++ b/app/kubernetes/models/event/models.js @@ -0,0 +1,16 @@ +/** + * KubernetesEvent Model + */ +const _KubernetesEvent = Object.freeze({ + Id: '', + Date: 0, + Type: '', + Message: '', + Involved: {}, +}); + +export class KubernetesEvent { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesEvent))); + } +} diff --git a/app/kubernetes/models/history/models.js b/app/kubernetes/models/history/models.js new file mode 100644 index 000000000..8a8cad6a7 --- /dev/null +++ b/app/kubernetes/models/history/models.js @@ -0,0 +1,32 @@ +export const KubernetesSystem_DefaultDeploymentUniqueLabelKey = 'pod-template-hash'; +export const KubernetesSystem_RevisionAnnotation = 'deployment.kubernetes.io/revision'; +export const KubernetesSystem_RevisionHistoryAnnotation = 'deployment.kubernetes.io/revision-history'; +export const KubernetesSystem_ChangeCauseAnnotation = 'kubernetes.io/change-cause'; +export const KubernetesSystem_DesiredReplicasAnnotation = 'deployment.kubernetes.io/desired-replicas'; +export const KubernetesSystem_MaxReplicasAnnotation = 'deployment.kubernetes.io/max-replicas'; + +// annotationsToSkip lists the annotations that should be preserved from the deployment and not +// copied from the replicaset when rolling a deployment back +// var annotationsToSkip = map[string]bool{ +// corev1.LastAppliedConfigAnnotation: true, +// deploymentutil.RevisionAnnotation: true, +// deploymentutil.RevisionHistoryAnnotation: true, +// deploymentutil.DesiredReplicasAnnotation: true, +// deploymentutil.MaxReplicasAnnotation: true, +// appsv1.DeprecatedRollbackTo: true, +// } + +// LastAppliedConfigAnnotation is the annotation used to store the previous +// configuration of a resource for use in a three way diff by UpdateApplyAnnotation. +const LastAppliedConfigAnnotation = 'kubectl.kubernetes.io/last-applied-configuration'; + +const DeprecatedRollbackTo = 'deprecated.deployment.rollback.to'; + +export const KubernetesSystem_AnnotationsToSkip = { + [LastAppliedConfigAnnotation]: true, + [KubernetesSystem_RevisionAnnotation]: true, + [KubernetesSystem_RevisionHistoryAnnotation]: true, + [KubernetesSystem_DesiredReplicasAnnotation]: true, + [KubernetesSystem_MaxReplicasAnnotation]: true, + [DeprecatedRollbackTo]: true, +}; diff --git a/app/kubernetes/models/namespace/models.js b/app/kubernetes/models/namespace/models.js new file mode 100644 index 000000000..d4038141a --- /dev/null +++ b/app/kubernetes/models/namespace/models.js @@ -0,0 +1,18 @@ +/** + * KubernetesNamespace Model + */ +const _KubernetesNamespace = Object.freeze({ + Id: '', + Name: '', + CreationDate: '', + Status: '', + Yaml: '', + ResourcePoolName: '', + ResourcePoolOwner: '', +}); + +export class KubernetesNamespace { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNamespace))); + } +} diff --git a/app/kubernetes/models/namespace/payloads.js b/app/kubernetes/models/namespace/payloads.js new file mode 100644 index 000000000..eb0d42fc5 --- /dev/null +++ b/app/kubernetes/models/namespace/payloads.js @@ -0,0 +1,14 @@ +import { KubernetesCommonMetadataPayload } from '../common/payloads'; + +/** + * KubernetesNamespaceCreatePayload Model + */ +const _KubernetesNamespaceCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), +}); + +export class KubernetesNamespaceCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNamespaceCreatePayload))); + } +} diff --git a/app/kubernetes/models/node/models.js b/app/kubernetes/models/node/models.js new file mode 100644 index 000000000..aeee2789b --- /dev/null +++ b/app/kubernetes/models/node/models.js @@ -0,0 +1,40 @@ +/** + * KubernetesNode Model + */ +const _KubernetesNode = Object.freeze({ + Id: '', + Name: '', + Role: '', + Status: '', + CPU: 0, + Memory: '', + Version: '', + IPAddress: '', +}); + +export class KubernetesNode { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNode))); + } +} + +/** + * KubernetesNodeDetails Model + */ +const _KubernetesNodeDetails = Object.freeze({ + CreationDate: '', + OS: { + Architecture: '', + Platform: '', + Image: '', + }, + Conditions: [], + Yaml: '', +}); + +export class KubernetesNodeDetails { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNode))); + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesNodeDetails))); + } +} diff --git a/app/kubernetes/models/pod/models.js b/app/kubernetes/models/pod/models.js new file mode 100644 index 000000000..3989913c6 --- /dev/null +++ b/app/kubernetes/models/pod/models.js @@ -0,0 +1,21 @@ +/** + * KubernetesPod Model + */ +const _KubernetesPod = Object.freeze({ + Id: '', + Name: '', + Namespace: '', + Images: [], + Status: '', + Restarts: 0, + Node: '', + CreationDate: '', + Containers: [], + Labels: [], +}); + +export class KubernetesPod { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPod))); + } +} diff --git a/app/kubernetes/models/port/models.js b/app/kubernetes/models/port/models.js new file mode 100644 index 000000000..1409a1059 --- /dev/null +++ b/app/kubernetes/models/port/models.js @@ -0,0 +1,33 @@ +/** + * PortMappingPort Model + */ +const _KubernetesPortMappingPort = Object.freeze({ + Port: 0, + TargetPort: 0, + Protocol: '', +}); + +export class KubernetesPortMappingPort { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPortMappingPort))); + } +} + +/** + * PortMapping Model + */ +const _KubernetesPortMapping = Object.freeze({ + Expanded: false, + Highlighted: false, + ResourcePool: '', + ServiceType: '', + ApplicationOwner: '', + LoadBalancerIPAddress: '', + Ports: [], +}); + +export class KubernetesPortMapping { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPortMapping))); + } +} diff --git a/app/kubernetes/models/resource-pool/models.js b/app/kubernetes/models/resource-pool/models.js new file mode 100644 index 000000000..57fa71df9 --- /dev/null +++ b/app/kubernetes/models/resource-pool/models.js @@ -0,0 +1,20 @@ +export const KubernetesPortainerResourcePoolNameLabel = 'io.portainer.kubernetes.resourcepool.name'; + +export const KubernetesPortainerResourcePoolOwnerLabel = 'io.portainer.kubernetes.resourcepool.owner'; + +/** + * KubernetesResourcePool Model (Composite) + * ResourcePool is a composite model that includes + * A Namespace and a Quota + */ +const _KubernetesResourcePool = Object.freeze({ + Namespace: {}, // KubernetesNamespace + Quota: undefined, // KubernetesResourceQuota + Yaml: '', +}); + +export class KubernetesResourcePool { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourcePool))); + } +} diff --git a/app/kubernetes/models/resource-quota/models.js b/app/kubernetes/models/resource-quota/models.js new file mode 100644 index 000000000..63190b760 --- /dev/null +++ b/app/kubernetes/models/resource-quota/models.js @@ -0,0 +1,34 @@ +import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; + +export const KubernetesPortainerResourceQuotaPrefix = 'portainer-rq-'; + +export const KubernetesResourceQuotaDefaults = { + CpuLimit: 0, + MemoryLimit: 0, +}; + +/** + * KubernetesResourceQuota Model + */ +const _KubernetesResourceQuota = Object.freeze({ + Id: '', + Namespace: '', + Name: '', + CpuLimit: KubernetesResourceQuotaDefaults.CpuLimit, + MemoryLimit: KubernetesResourceQuotaDefaults.MemoryLimit, + CpuLimitUsed: KubernetesResourceQuotaDefaults.CpuLimit, + MemoryLimitUsed: KubernetesResourceQuotaDefaults.MemoryLimit, + Yaml: '', + ResourcePoolName: '', + ResourcePoolOwner: '', +}); + +export class KubernetesResourceQuota { + constructor(namespace) { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuota))); + if (namespace) { + this.Name = KubernetesResourceQuotaHelper.generateResourceQuotaName(namespace); + this.Namespace = namespace; + } + } +} diff --git a/app/kubernetes/models/resource-quota/payloads.js b/app/kubernetes/models/resource-quota/payloads.js new file mode 100644 index 000000000..9a04dbf6c --- /dev/null +++ b/app/kubernetes/models/resource-quota/payloads.js @@ -0,0 +1,43 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesResourceQuotaCreatePayload Model + */ +const _KubernetesResourceQuotaCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + hard: { + 'requests.cpu': 0, + 'requests.memory': 0, + 'limits.cpu': 0, + 'limits.memory': 0, + }, + }, +}); + +export class KubernetesResourceQuotaCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuotaCreatePayload))); + } +} + +/** + * KubernetesResourceQuotaUpdatePayload Model + */ +const _KubernetesResourceQuotaUpdatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + hard: { + 'requests.cpu': 0, + 'requests.memory': 0, + 'limits.cpu': 0, + 'limits.memory': 0, + }, + }, +}); + +export class KubernetesResourceQuotaUpdatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceQuotaUpdatePayload))); + } +} diff --git a/app/kubernetes/models/resource-reservation/models.js b/app/kubernetes/models/resource-reservation/models.js new file mode 100644 index 000000000..ad13f6c70 --- /dev/null +++ b/app/kubernetes/models/resource-reservation/models.js @@ -0,0 +1,13 @@ +/** + * KubernetesResourceReservation Model + */ +const _KubernetesResourceReservation = Object.freeze({ + Memory: 0, + CPU: 0, +}); + +export class KubernetesResourceReservation { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourceReservation))); + } +} diff --git a/app/kubernetes/models/secret/models.js b/app/kubernetes/models/secret/models.js new file mode 100644 index 000000000..ae47b8b2e --- /dev/null +++ b/app/kubernetes/models/secret/models.js @@ -0,0 +1,18 @@ +/** + * KubernetesApplicationSecret Model + */ +const _KubernetesApplicationSecret = Object.freeze({ + Id: 0, + Name: '', + Namespace: '', + CreationDate: '', + ConfigurationOwner: '', + Yaml: '', + Data: {}, +}); + +export class KubernetesApplicationSecret { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationSecret))); + } +} diff --git a/app/kubernetes/models/secret/payloads.js b/app/kubernetes/models/secret/payloads.js new file mode 100644 index 000000000..2ce761d04 --- /dev/null +++ b/app/kubernetes/models/secret/payloads.js @@ -0,0 +1,31 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesSecretCreatePayload Model + */ +const _KubernetesSecretCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + type: 'Opaque', + data: {}, +}); + +export class KubernetesSecretCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesSecretCreatePayload))); + } +} + +/** + * KubernetesSecretUpdatePayload Model + */ +const _KubernetesSecretUpdatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + type: 'Opaque', + data: {}, +}); + +export class KubernetesSecretUpdatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesSecretUpdatePayload))); + } +} diff --git a/app/kubernetes/models/service/models.js b/app/kubernetes/models/service/models.js new file mode 100644 index 000000000..5c9abb61c --- /dev/null +++ b/app/kubernetes/models/service/models.js @@ -0,0 +1,45 @@ +export const KubernetesServiceHeadlessPrefix = 'headless-'; +export const KubernetesServiceHeadlessClusterIP = 'None'; +export const KubernetesServiceTypes = Object.freeze({ + LOAD_BALANCER: 'LoadBalancer', + NODE_PORT: 'NodePort', +}); + +/** + * KubernetesService Model + */ +const _KubernetesService = Object.freeze({ + Headless: false, + Namespace: '', + Name: '', + StackName: '', + Ports: [], + Type: '', + ClusterIP: '', + ApplicationName: '', + ApplicationOwner: '', + Note: '', +}); + +export class KubernetesService { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesService))); + } +} + +/** + * KubernetesServicePort Model + */ +const _KubernetesServicePort = Object.freeze({ + name: '', + port: 0, + targetPort: 0, + protocol: '', + nodePort: 0, +}); + +export class KubernetesServicePort { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesServicePort))); + } +} diff --git a/app/kubernetes/models/service/payloads.js b/app/kubernetes/models/service/payloads.js new file mode 100644 index 000000000..a5a3c2d5e --- /dev/null +++ b/app/kubernetes/models/service/payloads.js @@ -0,0 +1,22 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesServiceCreatePayload Model + */ +const _KubernetesServiceCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + ports: [], + selector: { + app: '', + }, + type: '', + clusterIP: '', + }, +}); + +export class KubernetesServiceCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesServiceCreatePayload))); + } +} diff --git a/app/kubernetes/models/stack/models.js b/app/kubernetes/models/stack/models.js new file mode 100644 index 000000000..1c9c27494 --- /dev/null +++ b/app/kubernetes/models/stack/models.js @@ -0,0 +1,14 @@ +/** + * Stack Model + */ +const _KubernetesStack = Object.freeze({ + Name: '', + ResourcePool: '', + Applications: [], +}); + +export class KubernetesStack { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStack))); + } +} diff --git a/app/kubernetes/models/stateful-set/models.js b/app/kubernetes/models/stateful-set/models.js new file mode 100644 index 000000000..7520ec116 --- /dev/null +++ b/app/kubernetes/models/stateful-set/models.js @@ -0,0 +1,27 @@ +/** + * KubernetesStatefulSet Model + */ +const _KubernetesStatefulSet = Object.freeze({ + Namespace: '', + Name: '', + StackName: '', + ReplicaCount: 0, + Image: '', + Env: [], + CpuLimit: '', + MemoryLimit: '', + VolumeMounts: [], + Volumes: [], + Secret: undefined, + VolumeClaims: [], + ServiceName: '', + ApplicationName: '', + ApplicationOwner: '', + Note: '', +}); + +export class KubernetesStatefulSet { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStatefulSet))); + } +} diff --git a/app/kubernetes/models/stateful-set/payloads.js b/app/kubernetes/models/stateful-set/payloads.js new file mode 100644 index 000000000..b5bd40329 --- /dev/null +++ b/app/kubernetes/models/stateful-set/payloads.js @@ -0,0 +1,52 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesStatefulSetCreatePayload Model + */ +const _KubernetesStatefulSetCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + replicas: 0, + serviceName: '', + selector: { + matchLabels: { + app: '', + }, + }, + volumeClaimTemplates: [], + updateStrategy: { + type: 'RollingUpdate', + rollingUpdate: { + partition: 0, + }, + }, + template: { + metadata: { + labels: { + app: '', + }, + }, + spec: { + containers: [ + { + name: '', + image: '', + env: [], + resources: { + limits: {}, + requests: {}, + }, + volumeMounts: [], + }, + ], + volumes: [], + }, + }, + }, +}); + +export class KubernetesStatefulSetCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStatefulSetCreatePayload))); + } +} diff --git a/app/kubernetes/models/storage-class/models.js b/app/kubernetes/models/storage-class/models.js new file mode 100644 index 000000000..6ede8946a --- /dev/null +++ b/app/kubernetes/models/storage-class/models.js @@ -0,0 +1,33 @@ +/** + * KubernetesStorageClassAccessPolicies Model + */ +const _KubernetesStorageClassAccessPolicies = Object.freeze([ + { + Name: 'RWO', + Description: 'Allow read-write from a single pod only (RWO)', + selected: true, + }, + { + Name: 'RWX', + Description: 'Allow read-write access from one or more pods concurrently (RWX)', + selected: false, + }, +]); + +export function KubernetesStorageClassAccessPolicies() { + return JSON.parse(JSON.stringify(_KubernetesStorageClassAccessPolicies)); +} + +/** + * KubernetesStorageClass Model + */ +const _KubernetesStorageClass = Object.freeze({ + Name: '', + AccessModes: [], +}); + +export class KubernetesStorageClass { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStorageClass))); + } +} diff --git a/app/kubernetes/models/volume/models.js b/app/kubernetes/models/volume/models.js new file mode 100644 index 000000000..aaaee4054 --- /dev/null +++ b/app/kubernetes/models/volume/models.js @@ -0,0 +1,39 @@ +import uuidv4 from 'uuid/v4'; +/** + * KubernetesPersistentVolumeClaim Model + */ +const _KubernetesPersistentVolumeClaim = Object.freeze({ + Id: '', + Name: '', + PreviousName: '', + Namespace: '', + Storage: 0, + StorageClass: {}, // KubernetesStorageClass + CreationDate: '', + ApplicationOwner: '', + ApplicationName: '', + MountPath: '', // used for Application creation from ApplicationFormValues | not used from API conversion + Yaml: '', +}); + +export class KubernetesPersistentVolumeClaim { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPersistentVolumeClaim))); + this.Name = uuidv4(); + } +} + +/** + * KubernetesVolume Model (Composite) + */ +const _KubernetesVolume = Object.freeze({ + ResourcePool: {}, // KubernetesResourcePool + PersistentVolumeClaim: {}, // KubernetesPersistentVolumeClaim + Applications: [], // KubernetesApplication +}); + +export class KubernetesVolume { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesVolume))); + } +} diff --git a/app/kubernetes/models/volume/payloads.js b/app/kubernetes/models/volume/payloads.js new file mode 100644 index 000000000..3fbd14936 --- /dev/null +++ b/app/kubernetes/models/volume/payloads.js @@ -0,0 +1,23 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesPersistentVolumClaimCreatePayload Model + */ +const _KubernetesPersistentVolumClaimCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + spec: { + accessModes: ['ReadWriteOnce'], + resources: { + requests: { + storage: '', + }, + }, + storageClassName: '', + }, +}); + +export class KubernetesPersistentVolumClaimCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPersistentVolumClaimCreatePayload))); + } +} diff --git a/app/kubernetes/rest/configMap.js b/app/kubernetes/rest/configMap.js new file mode 100644 index 000000000..62ad844dd --- /dev/null +++ b/app/kubernetes/rest/configMap.js @@ -0,0 +1,38 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesConfigMaps', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesConfigMapsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/configmaps/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/controllerRevision.js b/app/kubernetes/rest/controllerRevision.js new file mode 100644 index 000000000..fb8892c32 --- /dev/null +++ b/app/kubernetes/rest/controllerRevision.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesControllerRevisions', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesControllerRevisionsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/controllerrevisions/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/daemonSet.js b/app/kubernetes/rest/daemonSet.js new file mode 100644 index 000000000..f2cfa025d --- /dev/null +++ b/app/kubernetes/rest/daemonSet.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesDaemonSets', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesDaemonSetsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/daemonsets/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/strategic-merge-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/deployment.js b/app/kubernetes/rest/deployment.js new file mode 100644 index 000000000..44b2eb662 --- /dev/null +++ b/app/kubernetes/rest/deployment.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesDeployments', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesDeploymentsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/deployments/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/event.js b/app/kubernetes/rest/event.js new file mode 100644 index 000000000..54f9b07a5 --- /dev/null +++ b/app/kubernetes/rest/event.js @@ -0,0 +1,38 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesEvents', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesEventsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/events/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/health.js b/app/kubernetes/rest/health.js new file mode 100644 index 000000000..012798057 --- /dev/null +++ b/app/kubernetes/rest/health.js @@ -0,0 +1,17 @@ +angular.module('portainer.kubernetes').factory('KubernetesHealth', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesHealthFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return $resource( + API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/healthz', + { + endpointId: EndpointProvider.endpointID, + }, + { + ping: { method: 'GET', timeout: 15000 }, + } + ); + }, +]); diff --git a/app/kubernetes/rest/namespace.js b/app/kubernetes/rest/namespace.js new file mode 100644 index 000000000..843893d58 --- /dev/null +++ b/app/kubernetes/rest/namespace.js @@ -0,0 +1,42 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesNamespaces', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesNamespacesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function () { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1/namespaces/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + status: { + method: 'GET', + params: { action: 'status' }, + ignoreLoadingBar: true, + }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/node.js b/app/kubernetes/rest/node.js new file mode 100644 index 000000000..5e27a53eb --- /dev/null +++ b/app/kubernetes/rest/node.js @@ -0,0 +1,37 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesNodes', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesNodesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function () { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1/nodes/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/persistentVolumeClaim.js b/app/kubernetes/rest/persistentVolumeClaim.js new file mode 100644 index 000000000..25dd65177 --- /dev/null +++ b/app/kubernetes/rest/persistentVolumeClaim.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesPersistentVolumeClaims', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesPersistentVolumeClaimsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/persistentvolumeclaims/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/pod.js b/app/kubernetes/rest/pod.js new file mode 100644 index 000000000..2726eadd3 --- /dev/null +++ b/app/kubernetes/rest/pod.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; +import { logsHandler } from 'Docker/rest/response/handlers'; + +angular.module('portainer.kubernetes').factory('KubernetesPods', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesPodsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/pods/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + logs: { + method: 'GET', + params: { action: 'log' }, + transformResponse: logsHandler, + }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/replicaSet.js b/app/kubernetes/rest/replicaSet.js new file mode 100644 index 000000000..45dbf6c2c --- /dev/null +++ b/app/kubernetes/rest/replicaSet.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesReplicaSets', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesReplicaSetsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/replicasets/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/resourceQuota.js b/app/kubernetes/rest/resourceQuota.js new file mode 100644 index 000000000..c1eae1036 --- /dev/null +++ b/app/kubernetes/rest/resourceQuota.js @@ -0,0 +1,38 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesResourceQuotas', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesResourceQuotasFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/resourcequotas/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/response/transform.js b/app/kubernetes/rest/response/transform.js new file mode 100644 index 000000000..d4a0db053 --- /dev/null +++ b/app/kubernetes/rest/response/transform.js @@ -0,0 +1,7 @@ +// Returns the raw response without JSON parsing +export function rawResponse(data) { + const response = { + data: data, + }; + return response; +} diff --git a/app/kubernetes/rest/secret.js b/app/kubernetes/rest/secret.js new file mode 100644 index 000000000..1767871aa --- /dev/null +++ b/app/kubernetes/rest/secret.js @@ -0,0 +1,38 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesSecrets', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesSecretsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/secrets/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/service.js b/app/kubernetes/rest/service.js new file mode 100644 index 000000000..c483f13f2 --- /dev/null +++ b/app/kubernetes/rest/service.js @@ -0,0 +1,44 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesServices', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesServicesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/services/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/statefulSet.js b/app/kubernetes/rest/statefulSet.js new file mode 100644 index 000000000..3fa962aa0 --- /dev/null +++ b/app/kubernetes/rest/statefulSet.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesStatefulSets', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesStatefulSetsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/apps/v1' + (namespace ? '/namespaces/:namespace' : '') + '/statefulsets/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/strategic-merge-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/rest/storage.js b/app/kubernetes/rest/storage.js new file mode 100644 index 000000000..44e6406bf --- /dev/null +++ b/app/kubernetes/rest/storage.js @@ -0,0 +1,37 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesStorage', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesStorageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function () { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/storage.k8s.io/v1/storageclasses/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js new file mode 100644 index 000000000..ea99d26e1 --- /dev/null +++ b/app/kubernetes/services/applicationService.js @@ -0,0 +1,343 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import KubernetesApplicationRollbackHelper from 'Kubernetes/helpers/application/rollback'; +import KubernetesApplicationConverter from 'Kubernetes/converters/application'; +import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; +import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; +import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; +import { KubernetesApplication } from 'Kubernetes/models/application/models'; +import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; +import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper'; + +class KubernetesApplicationService { + /* @ngInject */ + constructor( + $async, + Authentication, + KubernetesDeploymentService, + KubernetesDaemonSetService, + KubernetesStatefulSetService, + KubernetesServiceService, + KubernetesSecretService, + KubernetesPersistentVolumeClaimService, + KubernetesNamespaceService, + KubernetesPodService, + KubernetesHistoryService, + KubernetesHorizontalPodAutoScalerService + ) { + this.$async = $async; + this.Authentication = Authentication; + this.KubernetesDeploymentService = KubernetesDeploymentService; + this.KubernetesDaemonSetService = KubernetesDaemonSetService; + this.KubernetesStatefulSetService = KubernetesStatefulSetService; + this.KubernetesServiceService = KubernetesServiceService; + this.KubernetesSecretService = KubernetesSecretService; + this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; + this.KubernetesNamespaceService = KubernetesNamespaceService; + this.KubernetesPodService = KubernetesPodService; + this.KubernetesHistoryService = KubernetesHistoryService; + this.KubernetesHorizontalPodAutoScalerService = KubernetesHorizontalPodAutoScalerService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.patchPartialAsync = this.patchPartialAsync.bind(this); + this.rollbackAsync = this.rollbackAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * UTILS + */ + _getApplicationApiService(app) { + let apiService; + if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) { + apiService = this.KubernetesDeploymentService; + } else if (app instanceof KubernetesDaemonSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET)) { + apiService = this.KubernetesDaemonSetService; + } else if (app instanceof KubernetesStatefulSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET)) { + apiService = this.KubernetesStatefulSetService; + } else { + throw new PortainerError('Unable to determine which association to use'); + } + return apiService; + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const [deployment, daemonSet, statefulSet, pods, autoScalers] = await Promise.allSettled([ + this.KubernetesDeploymentService.get(namespace, name), + this.KubernetesDaemonSetService.get(namespace, name), + this.KubernetesStatefulSetService.get(namespace, name), + this.KubernetesPodService.get(namespace), + this.KubernetesHorizontalPodAutoScalerService.get(namespace), + ]); + + let rootItem; + let converterFunction; + if (deployment.status === 'fulfilled') { + rootItem = deployment; + converterFunction = KubernetesApplicationConverter.apiDeploymentToApplication; + } else if (daemonSet.status === 'fulfilled') { + rootItem = daemonSet; + converterFunction = KubernetesApplicationConverter.apiDaemonSetToApplication; + } else if (statefulSet.status === 'fulfilled') { + rootItem = statefulSet; + converterFunction = KubernetesApplicationConverter.apiStatefulSetToapplication; + } else { + throw new PortainerError('Unable to determine which association to use'); + } + + const services = await this.KubernetesServiceService.get(namespace); + const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw); + const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {}; + + const application = converterFunction(rootItem.value.Raw, service.Raw); + application.Yaml = rootItem.value.Yaml; + application.Raw = rootItem.value.Raw; + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods.value, application.Raw); + + const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application); + const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined; + application.AutoScaler = scaler; + + await this.KubernetesHistoryService.get(application); + + if (service.Yaml) { + application.Yaml += '---\n' + service.Yaml; + } + if (scaler && scaler.Yaml) { + application.Yaml += '---\n' + scaler.Yaml; + } + return application; + } catch (err) { + throw err; + } + } + + async getAllAsync(namespace) { + try { + const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); + const res = await Promise.all( + _.map(namespaces, async (ns) => { + const [deployments, daemonSets, statefulSets, services, pods] = await Promise.all([ + this.KubernetesDeploymentService.get(ns), + this.KubernetesDaemonSetService.get(ns), + this.KubernetesStatefulSetService.get(ns), + this.KubernetesServiceService.get(ns), + this.KubernetesPodService.get(ns), + ]); + const deploymentApplications = _.map(deployments, (item) => { + const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const application = KubernetesApplicationConverter.apiDeploymentToApplication(item, service); + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); + return application; + }); + const daemonSetApplications = _.map(daemonSets, (item) => { + const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const application = KubernetesApplicationConverter.apiDaemonSetToApplication(item, service); + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); + return application; + }); + const statefulSetApplications = _.map(statefulSets, (item) => { + const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const application = KubernetesApplicationConverter.apiStatefulSetToapplication(item, service); + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); + return application; + }); + return _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications); + }) + ); + return _.flatten(res); + } catch (err) { + throw err; + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + // TODO: review + // resource creation flow + // should we keep formValues > Resource_1 || Resource_2 + // or should we switch to formValues > Composite > Resource_1 || Resource_2 + async createAsync(formValues) { + try { + let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); + + if (service) { + await this.KubernetesServiceService.create(service); + } + + const apiService = this._getApplicationApiService(app); + + if (app instanceof KubernetesStatefulSet) { + app.VolumeClaims = claims; + headlessService = await this.KubernetesServiceService.create(headlessService); + app.ServiceName = headlessService.metadata.name; + } else { + const claimPromises = _.map(claims, (item) => { + if (!item.PreviousName) { + return this.KubernetesPersistentVolumeClaimService.create(item); + } + }); + await Promise.all(_.without(claimPromises, undefined)); + } + + await apiService.create(app); + } catch (err) { + throw err; + } + } + + create(formValues) { + return this.$async(this.createAsync, formValues); + } + + /** + * PATCH + */ + // this function accepts KubernetesApplicationFormValues as parameters + async patchAsync(oldFormValues, newFormValues) { + try { + const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); + const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues); + const oldApiService = this._getApplicationApiService(oldApp); + const newApiService = this._getApplicationApiService(newApp); + + if (oldApiService !== newApiService) { + await this.delete(oldApp); + if (oldService) { + await this.KubernetesServiceService.delete(oldService); + } + return await this.create(newFormValues); + } + + if (newApp instanceof KubernetesStatefulSet) { + await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService); + } else { + const claimPromises = _.map(newClaims, (newClaim) => { + if (!newClaim.PreviousName) { + return this.KubernetesPersistentVolumeClaimService.create(newClaim); + } + const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName }); + return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim); + }); + await Promise.all(claimPromises); + } + + await newApiService.patch(oldApp, newApp); + + if (oldService && newService) { + await this.KubernetesServiceService.patch(oldService, newService); + } else if (!oldService && newService) { + await this.KubernetesServiceService.create(newService); + } else if (oldService && !newService) { + await this.KubernetesServiceService.delete(oldService); + } + } catch (err) { + throw err; + } + } + + // this function accepts KubernetesApplication as parameters + async patchPartialAsync(oldApp, newApp) { + try { + const oldAppPayload = { + Name: oldApp.Name, + Namespace: oldApp.ResourcePool, + StackName: oldApp.StackName, + Note: oldApp.Note, + }; + const newAppPayload = { + Name: newApp.Name, + Namespace: newApp.ResourcePool, + StackName: newApp.StackName, + Note: newApp.Note, + }; + const apiService = this._getApplicationApiService(oldApp); + await apiService.patch(oldAppPayload, newAppPayload); + } catch (err) { + throw err; + } + } + + // accept either formValues or applications as parameters + // depending on partial value + // true = KubernetesApplication + // false = KubernetesApplicationFormValues + patch(oldValues, newValues, partial = false) { + if (partial) { + return this.$async(this.patchPartialAsync, oldValues, newValues); + } + return this.$async(this.patchAsync, oldValues, newValues); + } + + /** + * DELETE + */ + async deleteAsync(application) { + try { + const payload = { + Namespace: application.ResourcePool || application.Namespace, + Name: application.Name, + }; + const servicePayload = angular.copy(payload); + servicePayload.Name = application.Name; + + const apiService = this._getApplicationApiService(application); + await apiService.delete(payload); + + if (apiService === this.KubernetesStatefulSetService) { + const headlessServicePayload = angular.copy(payload); + headlessServicePayload.Name = application instanceof KubernetesStatefulSet ? application.ServiceName : application.HeadlessServiceName; + await this.KubernetesServiceService.delete(headlessServicePayload); + } + + if (application.ServiceType) { + await this.KubernetesServiceService.delete(servicePayload); + } + } catch (err) { + throw err; + } + } + + delete(application) { + return this.$async(this.deleteAsync, application); + } + + /** + * ROLLBACK + */ + async rollbackAsync(application, targetRevision) { + try { + const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision); + const apiService = this._getApplicationApiService(application); + await apiService.rollback(application.ResourcePool, application.Name, payload); + } catch (err) { + throw err; + } + } + + rollback(application, targetRevision) { + return this.$async(this.rollbackAsync, application, targetRevision); + } +} + +export default KubernetesApplicationService; +angular.module('portainer.kubernetes').service('KubernetesApplicationService', KubernetesApplicationService); diff --git a/app/kubernetes/services/configMapService.js b/app/kubernetes/services/configMapService.js new file mode 100644 index 000000000..247af7497 --- /dev/null +++ b/app/kubernetes/services/configMapService.js @@ -0,0 +1,115 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; + +class KubernetesConfigMapService { + /* @ngInject */ + constructor($async, KubernetesConfigMaps) { + this.$async = $async; + this.KubernetesConfigMaps = KubernetesConfigMaps; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesConfigMaps(namespace).get(params).$promise, this.KubernetesConfigMaps(namespace).getYaml(params).$promise]); + const configMap = KubernetesConfigMapConverter.apiToConfigMap(raw, yaml); + return configMap; + } catch (err) { + if (err.status === 404) { + return KubernetesConfigMapConverter.defaultConfigMap(namespace, name); + } + throw new PortainerError('Unable to retrieve config map', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesConfigMaps(namespace).get().$promise; + return _.map(data.items, (item) => KubernetesConfigMapConverter.apiToConfigMap(item)); + } catch (err) { + throw new PortainerError('Unable to retrieve config maps', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(config) { + try { + const payload = KubernetesConfigMapConverter.createPayload(config); + const params = {}; + const namespace = payload.metadata.namespace; + const data = await this.KubernetesConfigMaps(namespace).create(params, payload).$promise; + return KubernetesConfigMapConverter.apiToConfigMap(data); + } catch (err) { + throw new PortainerError('Unable to create config map', err); + } + } + + create(config) { + return this.$async(this.createAsync, config); + } + + /** + * UPDATE + */ + async updateAsync(config) { + try { + if (!config.Id) { + return await this.create(config); + } + const payload = KubernetesConfigMapConverter.updatePayload(config); + const params = new KubernetesCommonParams(); + params.id = payload.metadata.name; + const namespace = payload.metadata.namespace; + const data = await this.KubernetesConfigMaps(namespace).update(params, payload).$promise; + return KubernetesConfigMapConverter.apiToConfigMap(data); + } catch (err) { + throw new PortainerError('Unable to update config map', err); + } + } + update(config) { + return this.$async(this.updateAsync, config); + } + + /** + * DELETE + */ + async deleteAsync(config) { + try { + const params = new KubernetesCommonParams(); + params.id = config.Name; + const namespace = config.Namespace; + await this.KubernetesConfigMaps(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete config map', err); + } + } + + delete(config) { + return this.$async(this.deleteAsync, config); + } +} + +export default KubernetesConfigMapService; +angular.module('portainer.kubernetes').service('KubernetesConfigMapService', KubernetesConfigMapService); diff --git a/app/kubernetes/services/configurationService.js b/app/kubernetes/services/configurationService.js new file mode 100644 index 000000000..1f421bfbd --- /dev/null +++ b/app/kubernetes/services/configurationService.js @@ -0,0 +1,128 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import KubernetesConfigurationConverter from 'Kubernetes/converters/configuration'; +import KubernetesConfigMapConverter from 'Kubernetes/converters/configMap'; +import KubernetesSecretConverter from 'Kubernetes/converters/secret'; +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +class KubernetesConfigurationService { + /* @ngInject */ + constructor($async, Authentication, KubernetesNamespaceService, KubernetesConfigMapService, KubernetesSecretService) { + this.$async = $async; + this.Authentication = Authentication; + this.KubernetesNamespaceService = KubernetesNamespaceService; + this.KubernetesConfigMapService = KubernetesConfigMapService; + this.KubernetesSecretService = KubernetesSecretService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const [configMap, secret] = await Promise.allSettled([this.KubernetesConfigMapService.get(namespace, name), this.KubernetesSecretService.get(namespace, name)]); + let configuration; + if (secret.status === 'fulfilled') { + configuration = KubernetesConfigurationConverter.secretToConfiguration(secret.value); + return configuration; + } + configuration = KubernetesConfigurationConverter.configMapToConfiguration(configMap.value); + return configuration; + } catch (err) { + throw err; + } + } + + async getAllAsync(namespace) { + try { + const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); + const res = await Promise.all( + _.map(namespaces, async (ns) => { + const [configMaps, secrets] = await Promise.all([this.KubernetesConfigMapService.get(ns), this.KubernetesSecretService.get(ns)]); + const secretsConfigurations = _.map(secrets, (secret) => KubernetesConfigurationConverter.secretToConfiguration(secret)); + const configMapsConfigurations = _.map(configMaps, (configMap) => KubernetesConfigurationConverter.configMapToConfiguration(configMap)); + return _.concat(configMapsConfigurations, secretsConfigurations); + }) + ); + return _.flatten(res); + } catch (err) { + throw err; + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(formValues) { + try { + if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { + const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); + await this.KubernetesConfigMapService.create(configMap); + } else { + const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues); + await this.KubernetesSecretService.create(secret); + } + } catch (err) { + throw err; + } + } + + create(formValues) { + return this.$async(this.createAsync, formValues); + } + + /** + * UPDATE + */ + async updateAsync(formValues) { + try { + if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) { + const configMap = KubernetesConfigMapConverter.configurationFormValuesToConfigMap(formValues); + await this.KubernetesConfigMapService.update(configMap); + } else { + const secret = KubernetesSecretConverter.configurationFormValuesToSecret(formValues); + await this.KubernetesSecretService.update(secret); + } + } catch (err) { + throw err; + } + } + update(config) { + return this.$async(this.updateAsync, config); + } + + /** + * DELETE + */ + async deleteAsync(config) { + try { + if (config.Type === KubernetesConfigurationTypes.CONFIGMAP) { + await this.KubernetesConfigMapService.delete(config); + } else { + await this.KubernetesSecretService.delete(config); + } + } catch (err) { + throw err; + } + } + + delete(config) { + return this.$async(this.deleteAsync, config); + } +} + +export default KubernetesConfigurationService; +angular.module('portainer.kubernetes').service('KubernetesConfigurationService', KubernetesConfigurationService); diff --git a/app/kubernetes/services/controllerRevisionService.js b/app/kubernetes/services/controllerRevisionService.js new file mode 100644 index 000000000..0f5d38fc9 --- /dev/null +++ b/app/kubernetes/services/controllerRevisionService.js @@ -0,0 +1,31 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +class KubernetesControllerRevisionService { + /* @ngInject */ + constructor($async, KubernetesControllerRevisions) { + this.$async = $async; + this.KubernetesControllerRevisions = KubernetesControllerRevisions; + + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAllAsync(namespace) { + try { + const data = await this.KubernetesControllerRevisions(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve ControllerRevisions', err); + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesControllerRevisionService; +angular.module('portainer.kubernetes').service('KubernetesControllerRevisionService', KubernetesControllerRevisionService); diff --git a/app/kubernetes/services/daemonSetService.js b/app/kubernetes/services/daemonSetService.js new file mode 100644 index 000000000..6fbc38e9c --- /dev/null +++ b/app/kubernetes/services/daemonSetService.js @@ -0,0 +1,133 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesDaemonSetConverter from 'Kubernetes/converters/daemonSet'; + +class KubernetesDaemonSetService { + /* @ngInject */ + constructor($async, KubernetesDaemonSets) { + this.$async = $async; + this.KubernetesDaemonSets = KubernetesDaemonSets; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.rollbackAsync = this.rollbackAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesDaemonSets(namespace).get(params).$promise, this.KubernetesDaemonSets(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve DaemonSet', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesDaemonSets(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve DaemonSets', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(daemonSet) { + try { + const params = {}; + const payload = KubernetesDaemonSetConverter.createPayload(daemonSet); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesDaemonSets(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create daemonset', err); + } + } + + create(daemonSet) { + return this.$async(this.createAsync, daemonSet); + } + + /** + * PATCH + */ + async patchAsync(oldDaemonSet, newDaemonSet) { + try { + const params = new KubernetesCommonParams(); + params.id = newDaemonSet.Name; + const namespace = newDaemonSet.Namespace; + const payload = KubernetesDaemonSetConverter.patchPayload(oldDaemonSet, newDaemonSet); + if (!payload.length) { + return; + } + const data = await this.KubernetesDaemonSets(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch daemonSet', err); + } + } + + patch(oldDaemonSet, newDaemonSet) { + return this.$async(this.patchAsync, oldDaemonSet, newDaemonSet); + } + + /** + * DELETE + */ + async deleteAsync(daemonSet) { + try { + const params = new KubernetesCommonParams(); + params.id = daemonSet.Name; + const namespace = daemonSet.Namespace; + await this.KubernetesDaemonSets(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove daemonset', err); + } + } + + delete(daemonSet) { + return this.$async(this.deleteAsync, daemonSet); + } + + /** + * ROLLBACK + */ + async rollbackAsync(namespace, name, payload) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + await this.KubernetesDaemonSets(namespace).rollback(params, payload).$promise; + } catch (err) { + throw new PortainerError('Unable to rollback daemonset', err); + } + } + + rollback(namespace, name, payload) { + return this.$async(this.rollbackAsync, namespace, name, payload); + } +} + +export default KubernetesDaemonSetService; +angular.module('portainer.kubernetes').service('KubernetesDaemonSetService', KubernetesDaemonSetService); diff --git a/app/kubernetes/services/deploymentService.js b/app/kubernetes/services/deploymentService.js new file mode 100644 index 000000000..fe0f4ac3b --- /dev/null +++ b/app/kubernetes/services/deploymentService.js @@ -0,0 +1,133 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesDeploymentConverter from 'Kubernetes/converters/deployment'; + +class KubernetesDeploymentService { + /* @ngInject */ + constructor($async, KubernetesDeployments) { + this.$async = $async; + this.KubernetesDeployments = KubernetesDeployments; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.rollbackAsync = this.rollbackAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesDeployments(namespace).get(params).$promise, this.KubernetesDeployments(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve Deployment', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesDeployments(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve Deployments', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(deployment) { + try { + const params = {}; + const payload = KubernetesDeploymentConverter.createPayload(deployment); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesDeployments(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create deployment', err); + } + } + + create(deployment) { + return this.$async(this.createAsync, deployment); + } + + /** + * PATCH + */ + async patchAsync(oldDeployment, newDeployment) { + try { + const params = new KubernetesCommonParams(); + params.id = newDeployment.Name; + const namespace = newDeployment.Namespace; + const payload = KubernetesDeploymentConverter.patchPayload(oldDeployment, newDeployment); + if (!payload.length) { + return; + } + const data = await this.KubernetesDeployments(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch deployment', err); + } + } + + patch(oldDeployment, newDeployment) { + return this.$async(this.patchAsync, oldDeployment, newDeployment); + } + + /** + * DELETE + */ + async deleteAsync(deployment) { + try { + const params = new KubernetesCommonParams(); + params.id = deployment.Name; + const namespace = deployment.Namespace; + await this.KubernetesDeployments(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove deployment', err); + } + } + + delete(deployment) { + return this.$async(this.deleteAsync, deployment); + } + + /** + * ROLLBACK + */ + async rollbackAsync(namespace, name, payload) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + await this.KubernetesDeployments(namespace).rollback(params, payload).$promise; + } catch (err) { + throw new PortainerError('Unable to rollback deployment', err); + } + } + + rollback(namespace, name, payload) { + return this.$async(this.rollbackAsync, namespace, name, payload); + } +} + +export default KubernetesDeploymentService; +angular.module('portainer.kubernetes').service('KubernetesDeploymentService', KubernetesDeploymentService); diff --git a/app/kubernetes/services/eventService.js b/app/kubernetes/services/eventService.js new file mode 100644 index 000000000..8d38eba0d --- /dev/null +++ b/app/kubernetes/services/eventService.js @@ -0,0 +1,34 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import KubernetesEventConverter from 'Kubernetes/converters/event'; + +class KubernetesEventService { + /* @ngInject */ + constructor($async, KubernetesEvents) { + this.$async = $async; + this.KubernetesEvents = KubernetesEvents; + + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAllAsync(namespace) { + try { + const data = await this.KubernetesEvents(namespace).get().$promise; + const res = _.map(data.items, (item) => KubernetesEventConverter.apiToEvent(item)); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve events', err); + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesEventService; +angular.module('portainer.kubernetes').service('KubernetesEventService', KubernetesEventService); diff --git a/app/kubernetes/services/healthService.js b/app/kubernetes/services/healthService.js new file mode 100644 index 000000000..977ffae56 --- /dev/null +++ b/app/kubernetes/services/healthService.js @@ -0,0 +1,30 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +class KubernetesHealthService { + /* @ngInject */ + constructor($async, KubernetesHealth) { + this.$async = $async; + this.KubernetesHealth = KubernetesHealth; + + this.pingAsync = this.pingAsync.bind(this); + } + + /** + * PING + */ + async pingAsync() { + try { + return await this.KubernetesHealth.ping().$promise; + } catch (err) { + throw new PortainerError('Unable to retrieve environment health', err); + } + } + + ping() { + return this.$async(this.pingAsync); + } +} + +export default KubernetesHealthService; +angular.module('portainer.kubernetes').service('KubernetesHealthService', KubernetesHealthService); diff --git a/app/kubernetes/services/historyService.js b/app/kubernetes/services/historyService.js new file mode 100644 index 000000000..df0284166 --- /dev/null +++ b/app/kubernetes/services/historyService.js @@ -0,0 +1,54 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +import KubernetesHistoryHelper from 'Kubernetes/helpers/history'; +import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; + +class KubernetesHistoryService { + /* @ngInject */ + constructor($async, KubernetesReplicaSetService, KubernetesControllerRevisionService) { + this.$async = $async; + this.KubernetesReplicaSetService = KubernetesReplicaSetService; + this.KubernetesControllerRevisionService = KubernetesControllerRevisionService; + + this.getAsync = this.getAsync.bind(this); + } + + /** + * GET + */ + async getAsync(application) { + try { + const namespace = application.ResourcePool; + let rawRevisions; + + switch (application.ApplicationType) { + case KubernetesApplicationTypes.DEPLOYMENT: + rawRevisions = await this.KubernetesReplicaSetService.get(namespace); + break; + case KubernetesApplicationTypes.DAEMONSET: + rawRevisions = await this.KubernetesControllerRevisionService.get(namespace); + break; + case KubernetesApplicationTypes.STATEFULSET: + rawRevisions = await this.KubernetesControllerRevisionService.get(namespace); + break; + default: + throw new PortainerError('Unable to determine which association to use'); + } + + const [currentRevision, revisionsList] = KubernetesHistoryHelper.getRevisions(rawRevisions, application); + application.CurrentRevision = currentRevision; + application.Revisions = revisionsList; + return application; + } catch (err) { + throw new PortainerError('', err); + } + } + + get(application) { + return this.$async(this.getAsync, application); + } +} + +export default KubernetesHistoryService; +angular.module('portainer.kubernetes').service('KubernetesHistoryService', KubernetesHistoryService); diff --git a/app/kubernetes/services/namespaceService.js b/app/kubernetes/services/namespaceService.js new file mode 100644 index 000000000..891cbd0b5 --- /dev/null +++ b/app/kubernetes/services/namespaceService.js @@ -0,0 +1,96 @@ +import _ from 'lodash-es'; + +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesNamespaceConverter from 'Kubernetes/converters/namespace'; +import $allSettled from 'Portainer/services/allSettled'; + +class KubernetesNamespaceService { + /* @ngInject */ + constructor($async, KubernetesNamespaces) { + this.$async = $async; + this.KubernetesNamespaces = KubernetesNamespaces; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + await this.KubernetesNamespaces().status(params).$promise; + const [raw, yaml] = await Promise.all([this.KubernetesNamespaces().get(params).$promise, this.KubernetesNamespaces().getYaml(params).$promise]); + return KubernetesNamespaceConverter.apiToNamespace(raw, yaml); + } catch (err) { + throw new PortainerError('Unable to retrieve namespace', err); + } + } + + async getAllAsync() { + try { + const data = await this.KubernetesNamespaces().get().$promise; + const promises = _.map(data.items, (item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise); + const namespaces = await $allSettled(promises); + const visibleNamespaces = _.map(namespaces.fulfilled, (item) => { + if (item.status.phase !== 'Terminating') { + return KubernetesNamespaceConverter.apiToNamespace(item); + } + }); + return _.without(visibleNamespaces, undefined); + } catch (err) { + throw new PortainerError('Unable to retrieve namespaces', err); + } + } + + get(name) { + if (name) { + return this.$async(this.getAsync, name); + } + return this.$async(this.getAllAsync); + } + + /** + * CREATE + */ + async createAsync(namespace) { + try { + const payload = KubernetesNamespaceConverter.createPayload(namespace); + const params = {}; + const data = await this.KubernetesNamespaces().create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create namespace', err); + } + } + + create(namespace) { + return this.$async(this.createAsync, namespace); + } + + /** + * DELETE + */ + async deleteAsync(namespace) { + try { + const params = new KubernetesCommonParams(); + params.id = namespace.Name; + await this.KubernetesNamespaces().delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete namespace', err); + } + } + + delete(namespace) { + return this.$async(this.deleteAsync, namespace); + } +} + +export default KubernetesNamespaceService; +angular.module('portainer.kubernetes').service('KubernetesNamespaceService', KubernetesNamespaceService); diff --git a/app/kubernetes/services/nodeService.js b/app/kubernetes/services/nodeService.js new file mode 100644 index 000000000..58b2d787e --- /dev/null +++ b/app/kubernetes/services/nodeService.js @@ -0,0 +1,50 @@ +import angular from 'angular'; +import _ from 'lodash-es'; + +import PortainerError from 'Portainer/error'; +import KubernetesNodeConverter from 'Kubernetes/converters/node'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; + +class KubernetesNodeService { + /* @ngInject */ + constructor($async, KubernetesNodes) { + this.$async = $async; + this.KubernetesNodes = KubernetesNodes; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAsync(name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [details, yaml] = await Promise.all([this.KubernetesNodes().get(params).$promise, this.KubernetesNodes().getYaml(params).$promise]); + return KubernetesNodeConverter.apiToNodeDetails(details, yaml); + } catch (err) { + throw new PortainerError('Unable to retrieve node details', err); + } + } + + async getAllAsync() { + try { + const data = await this.KubernetesNodes().get().$promise; + return _.map(data.items, (item) => KubernetesNodeConverter.apiToNode(item)); + } catch (err) { + throw { msg: 'Unable to retrieve nodes', err: err }; + } + } + + get(name) { + if (name) { + return this.$async(this.getAsync, name); + } + return this.$async(this.getAllAsync); + } +} + +export default KubernetesNodeService; +angular.module('portainer.kubernetes').service('KubernetesNodeService', KubernetesNodeService); diff --git a/app/kubernetes/services/persistentVolumeClaimService.js b/app/kubernetes/services/persistentVolumeClaimService.js new file mode 100644 index 000000000..a19fcdf0d --- /dev/null +++ b/app/kubernetes/services/persistentVolumeClaimService.js @@ -0,0 +1,115 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; + +class KubernetesPersistentVolumeClaimService { + /* @ngInject */ + constructor($async, EndpointProvider, KubernetesPersistentVolumeClaims) { + this.$async = $async; + this.EndpointProvider = EndpointProvider; + this.KubernetesPersistentVolumeClaims = KubernetesPersistentVolumeClaims; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([ + this.KubernetesPersistentVolumeClaims(namespace).get(params).$promise, + this.KubernetesPersistentVolumeClaims(namespace).getYaml(params).$promise, + ]); + const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses; + return KubernetesPersistentVolumeClaimConverter.apiToPersistentVolumeClaim(raw, storageClasses, yaml); + } catch (err) { + throw new PortainerError('Unable to retrieve persistent volume claim', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesPersistentVolumeClaims(namespace).get().$promise; + const storageClasses = this.EndpointProvider.currentEndpoint().Kubernetes.Configuration.StorageClasses; + return _.map(data.items, (item) => KubernetesPersistentVolumeClaimConverter.apiToPersistentVolumeClaim(item, storageClasses)); + } catch (err) { + throw new PortainerError('Unable to retrieve persistent volume claims', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(claim) { + try { + const params = {}; + const payload = KubernetesPersistentVolumeClaimConverter.createPayload(claim); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesPersistentVolumeClaims(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create persistent volume claim', err); + } + } + + create(claim) { + return this.$async(this.createAsync, claim); + } + + /** + * PATCH + */ + async patchAsync(oldPVC, newPVC) { + try { + const params = new KubernetesCommonParams(); + params.id = newPVC.Name; + const namespace = newPVC.Namespace; + const payload = KubernetesPersistentVolumeClaimConverter.patchPayload(oldPVC, newPVC); + if (!payload.length) { + return; + } + const data = await this.KubernetesPersistentVolumeClaims(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch persistent volume claim', err); + } + } + + patch(oldPVC, newPVC) { + return this.$async(this.patchAsync, oldPVC, newPVC); + } + + /** + * DELETE + */ + async deleteAsync(pvc) { + try { + const params = new KubernetesCommonParams(); + params.id = pvc.Name; + const namespace = pvc.Namespace; + await this.KubernetesPersistentVolumeClaims(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete persistent volume claim', err); + } + } + + delete(pvc) { + return this.$async(this.deleteAsync, pvc); + } +} + +export default KubernetesPersistentVolumeClaimService; +angular.module('portainer.kubernetes').service('KubernetesPersistentVolumeClaimService', KubernetesPersistentVolumeClaimService); diff --git a/app/kubernetes/services/podService.js b/app/kubernetes/services/podService.js new file mode 100644 index 000000000..49c733e62 --- /dev/null +++ b/app/kubernetes/services/podService.js @@ -0,0 +1,75 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesPodConverter from 'Kubernetes/converters/pod'; + +class KubernetesPodService { + /* @ngInject */ + constructor($async, KubernetesPods) { + this.$async = $async; + this.KubernetesPods = KubernetesPods; + + this.getAllAsync = this.getAllAsync.bind(this); + this.logsAsync = this.logsAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + /** + * GET ALL + */ + async getAllAsync(namespace) { + try { + const data = await this.KubernetesPods(namespace).get().$promise; + return _.map(data.items, (item) => KubernetesPodConverter.apiToPod(item)); + } catch (err) { + throw new PortainerError('Unable to retrieve pods', err); + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } + + /** + * Logs + * + * @param {string} namespace + * @param {string} podName + */ + async logsAsync(namespace, podName) { + try { + const params = new KubernetesCommonParams(); + params.id = podName; + const data = await this.KubernetesPods(namespace).logs(params).$promise; + return data.logs.length === 0 ? [] : data.logs.split('\n'); + } catch (err) { + throw new PortainerError('Unable to retrieve pod logs', err); + } + } + + logs(namespace, podName) { + return this.$async(this.logsAsync, namespace, podName); + } + + /** + * DELETE + */ + async deleteAsync(pod) { + try { + const params = new KubernetesCommonParams(); + params.id = pod.Name; + const namespace = pod.Namespace; + await this.KubernetesPods(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove pod', err); + } + } + + delete(pod) { + return this.$async(this.deleteAsync, pod); + } +} + +export default KubernetesPodService; +angular.module('portainer.kubernetes').service('KubernetesPodService', KubernetesPodService); diff --git a/app/kubernetes/services/replicaSetService.js b/app/kubernetes/services/replicaSetService.js new file mode 100644 index 000000000..a6f65bc06 --- /dev/null +++ b/app/kubernetes/services/replicaSetService.js @@ -0,0 +1,31 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; + +class KubernetesReplicaSetService { + /* @ngInject */ + constructor($async, KubernetesReplicaSets) { + this.$async = $async; + this.KubernetesReplicaSets = KubernetesReplicaSets; + + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAllAsync(namespace) { + try { + const data = await this.KubernetesReplicaSets(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve ReplicaSets', err); + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesReplicaSetService; +angular.module('portainer.kubernetes').service('KubernetesReplicaSetService', KubernetesReplicaSetService); diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js new file mode 100644 index 000000000..7dbfb933b --- /dev/null +++ b/app/kubernetes/services/resourcePoolService.js @@ -0,0 +1,113 @@ +import _ from 'lodash-es'; +import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; + +import angular from 'angular'; +import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool'; +import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; +import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; + +class KubernetesResourcePoolService { + /* @ngInject */ + constructor($async, KubernetesNamespaceService, KubernetesResourceQuotaService) { + this.$async = $async; + this.KubernetesNamespaceService = KubernetesNamespaceService; + this.KubernetesResourceQuotaService = KubernetesResourceQuotaService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(name) { + try { + const namespace = await this.KubernetesNamespaceService.get(name); + const [quotaAttempt] = await Promise.allSettled([this.KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); + if (quotaAttempt.status === 'fulfilled') { + pool.Quota = quotaAttempt.value; + pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + } + return pool; + } catch (err) { + throw err; + } + } + + async getAllAsync() { + try { + const namespaces = await this.KubernetesNamespaceService.get(); + const pools = await Promise.all( + _.map(namespaces, async (namespace) => { + const name = namespace.Name; + const [quotaAttempt] = await Promise.allSettled([this.KubernetesResourceQuotaService.get(name, KubernetesResourceQuotaHelper.generateResourceQuotaName(name))]); + const pool = KubernetesResourcePoolConverter.apiToResourcePool(namespace); + if (quotaAttempt.status === 'fulfilled') { + pool.Quota = quotaAttempt.value; + pool.Yaml += '---\n' + quotaAttempt.value.Yaml; + } + return pool; + }) + ); + return pools; + } catch (err) { + throw err; + } + } + + get(name) { + if (name) { + return this.$async(this.getAsync, name); + } + return this.$async(this.getAllAsync); + } + + /** + * CREATE + */ + // TODO: review LimitRange future + async createAsync(name, owner, hasQuota, cpuLimit, memoryLimit) { + try { + const namespace = new KubernetesNamespace(); + namespace.Name = name; + namespace.ResourcePoolName = name; + namespace.ResourcePoolOwner = owner; + await this.KubernetesNamespaceService.create(namespace); + if (hasQuota) { + const quota = new KubernetesResourceQuota(name); + quota.CpuLimit = cpuLimit; + quota.MemoryLimit = memoryLimit; + quota.ResourcePoolName = name; + quota.ResourcePoolOwner = owner; + await this.KubernetesResourceQuotaService.create(quota); + } + } catch (err) { + throw err; + } + } + + create(name, owner, hasQuota, cpuLimit, memoryLimit) { + return this.$async(this.createAsync, name, owner, hasQuota, cpuLimit, memoryLimit); + } + + /** + * DELETE + */ + async deleteAsync(pool) { + try { + await this.KubernetesNamespaceService.delete(pool.Namespace); + } catch (err) { + throw err; + } + } + + delete(pool) { + return this.$async(this.deleteAsync, pool); + } +} + +export default KubernetesResourcePoolService; +angular.module('portainer.kubernetes').service('KubernetesResourcePoolService', KubernetesResourcePoolService); diff --git a/app/kubernetes/services/resourceQuotaService.js b/app/kubernetes/services/resourceQuotaService.js new file mode 100644 index 000000000..0a41da03f --- /dev/null +++ b/app/kubernetes/services/resourceQuotaService.js @@ -0,0 +1,109 @@ +import _ from 'lodash-es'; + +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota'; + +class KubernetesResourceQuotaService { + /* @ngInject */ + constructor($async, KubernetesResourceQuotas) { + this.$async = $async; + this.KubernetesResourceQuotas = KubernetesResourceQuotas; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesResourceQuotas(namespace).get(params).$promise, this.KubernetesResourceQuotas(namespace).getYaml(params).$promise]); + return KubernetesResourceQuotaConverter.apiToResourceQuota(raw, yaml); + } catch (err) { + throw new PortainerError('Unable to retrieve resource quota', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesResourceQuotas(namespace).get().$promise; + return _.map(data.items, (item) => KubernetesResourceQuotaConverter.apiToResourceQuota(item)); + } catch (err) { + throw new PortainerError('Unable to retrieve resource quotas', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(quota) { + try { + const payload = KubernetesResourceQuotaConverter.createPayload(quota); + const namespace = payload.metadata.namespace; + const params = {}; + const data = await this.KubernetesResourceQuotas(namespace).create(params, payload).$promise; + return KubernetesResourceQuotaConverter.apiToResourceQuota(data); + } catch (err) { + throw new PortainerError('Unable to create quota', err); + } + } + + create(quota) { + return this.$async(this.createAsync, quota); + } + + /** + * UPDATE + */ + async updateAsync(quota) { + try { + const payload = KubernetesResourceQuotaConverter.updatePayload(quota); + const params = new KubernetesCommonParams(); + params.id = payload.metadata.name; + const namespace = payload.metadata.namespace; + const data = await this.KubernetesResourceQuotas(namespace).update(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to update resource quota', err); + } + } + + update(quota) { + return this.$async(this.updateAsync, quota); + } + + /** + * DELETE + */ + async deleteAsync(quota) { + try { + const params = new KubernetesCommonParams(); + params.id = quota.Name; + await this.KubernetesResourceQuotas(quota.Namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete quota', err); + } + } + + delete(quota) { + return this.$async(this.deleteAsync, quota); + } +} + +export default KubernetesResourceQuotaService; +angular.module('portainer.kubernetes').service('KubernetesResourceQuotaService', KubernetesResourceQuotaService); diff --git a/app/kubernetes/services/secretService.js b/app/kubernetes/services/secretService.js new file mode 100644 index 000000000..b54ca5aad --- /dev/null +++ b/app/kubernetes/services/secretService.js @@ -0,0 +1,110 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import KubernetesSecretConverter from 'Kubernetes/converters/secret'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; + +class KubernetesSecretService { + /* @ngInject */ + constructor($async, KubernetesSecrets) { + this.$async = $async; + this.KubernetesSecrets = KubernetesSecrets; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.updateAsync = this.updateAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesSecrets(namespace).get(params).$promise, this.KubernetesSecrets(namespace).getYaml(params).$promise]); + const secret = KubernetesSecretConverter.apiToSecret(raw, yaml); + return secret; + } catch (err) { + throw new PortainerError('Unable to retrieve secret', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesSecrets(namespace).get().$promise; + return _.map(data.items, (item) => KubernetesSecretConverter.apiToSecret(item)); + } catch (err) { + throw new PortainerError('Unable to retrieve secrets', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(secret) { + try { + const payload = KubernetesSecretConverter.createPayload(secret); + const namespace = payload.metadata.namespace; + const params = {}; + const data = await this.KubernetesSecrets(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create secret', err); + } + } + + create(secret) { + return this.$async(this.createAsync, secret); + } + + /** + * UPDATE + */ + async updateAsync(secret) { + try { + const payload = KubernetesSecretConverter.updatePayload(secret); + const params = new KubernetesCommonParams(); + params.id = payload.metadata.name; + const namespace = payload.metadata.namespace; + const data = await this.KubernetesSecrets(namespace).update(params, payload).$promise; + return KubernetesSecretConverter.apiToSecret(data); + } catch (err) { + throw new PortainerError('Unable to update secret', err); + } + } + + update(secret) { + return this.$async(this.updateAsync, secret); + } + + /** + * DELETE + */ + async deleteAsync(secret) { + try { + const params = new KubernetesCommonParams(); + params.id = secret.Name; + const namespace = secret.Namespace; + await this.KubernetesSecrets(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete secret', err); + } + } + + delete(secret) { + return this.$async(this.deleteAsync, secret); + } +} + +export default KubernetesSecretService; +angular.module('portainer.kubernetes').service('KubernetesSecretService', KubernetesSecretService); diff --git a/app/kubernetes/services/serviceService.js b/app/kubernetes/services/serviceService.js new file mode 100644 index 000000000..a85292d76 --- /dev/null +++ b/app/kubernetes/services/serviceService.js @@ -0,0 +1,115 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesServiceConverter from 'Kubernetes/converters/service'; + +class KubernetesServiceService { + /* @ngInject */ + constructor($async, KubernetesServices) { + this.$async = $async; + this.KubernetesServices = KubernetesServices; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesServices(namespace).get(params).$promise, this.KubernetesServices(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve service', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesServices(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve services', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(service) { + try { + const params = {}; + const payload = KubernetesServiceConverter.createPayload(service); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesServices(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create service', err); + } + } + + create(service) { + return this.$async(this.createAsync, service); + } + + /** + * PATCH + */ + async patchAsync(oldService, newService) { + try { + const params = new KubernetesCommonParams(); + params.id = newService.Name; + const namespace = newService.Namespace; + const payload = KubernetesServiceConverter.patchPayload(oldService, newService); + if (!payload.length) { + return; + } + const data = await this.KubernetesServices(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch service', err); + } + } + + patch(oldService, newService) { + return this.$async(this.patchAsync, oldService, newService); + } + + /** + * DELETE + */ + async deleteAsync(service) { + try { + const params = new KubernetesCommonParams(); + params.id = service.Name; + const namespace = service.Namespace; + await this.KubernetesServices(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove service', err); + } + } + + delete(service) { + return this.$async(this.deleteAsync, service); + } +} + +export default KubernetesServiceService; +angular.module('portainer.kubernetes').service('KubernetesServiceService', KubernetesServiceService); diff --git a/app/kubernetes/services/stackService.js b/app/kubernetes/services/stackService.js new file mode 100644 index 000000000..23e09c01f --- /dev/null +++ b/app/kubernetes/services/stackService.js @@ -0,0 +1,32 @@ +import _ from 'lodash-es'; +import angular from 'angular'; + +class KubernetesStackService { + /* @ngInject */ + constructor($async, KubernetesApplicationService) { + this.$async = $async; + this.KubernetesApplicationService = KubernetesApplicationService; + + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAllAsync(namespace) { + try { + const applications = await this.KubernetesApplicationService.get(namespace); + const stacks = _.map(applications, (item) => item.StackName); + return _.uniq(_.without(stacks, '-')); + } catch (err) { + throw err; + } + } + + get(namespace) { + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesStackService; +angular.module('portainer.kubernetes').service('KubernetesStackService', KubernetesStackService); diff --git a/app/kubernetes/services/statefulSetService.js b/app/kubernetes/services/statefulSetService.js new file mode 100644 index 000000000..60cdea44c --- /dev/null +++ b/app/kubernetes/services/statefulSetService.js @@ -0,0 +1,144 @@ +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet'; + +class KubernetesStatefulSetService { + /* @ngInject */ + constructor($async, KubernetesStatefulSets, KubernetesServiceService) { + this.$async = $async; + this.KubernetesStatefulSets = KubernetesStatefulSets; + this.KubernetesServiceService = KubernetesServiceService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.rollbackAsync = this.rollbackAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesStatefulSets(namespace).get(params).$promise, this.KubernetesStatefulSets(namespace).getYaml(params).$promise]); + const res = { + Raw: raw, + Yaml: yaml.data, + }; + const headlessServiceName = raw.spec.serviceName; + if (headlessServiceName) { + try { + const headlessService = await this.KubernetesServiceService.get(namespace, headlessServiceName); + res.Yaml += '---\n' + headlessService.Yaml; + } catch (error) { + // if has error means headless service does not exist + // skip error as we don't care in this case + } + } + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve StatefulSet', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesStatefulSets(namespace).get().$promise; + return data.items; + } catch (err) { + throw new PortainerError('Unable to retrieve StatefulSets', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * CREATE + */ + async createAsync(statefulSet) { + try { + const params = {}; + const payload = KubernetesStatefulSetConverter.createPayload(statefulSet); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesStatefulSets(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create statefulSet', err); + } + } + + create(statefulSet) { + return this.$async(this.createAsync, statefulSet); + } + + /** + * PATCH + */ + async patchAsync(oldStatefulSet, newStatefulSet) { + try { + const params = new KubernetesCommonParams(); + params.id = newStatefulSet.Name; + const namespace = newStatefulSet.Namespace; + const payload = KubernetesStatefulSetConverter.patchPayload(oldStatefulSet, newStatefulSet); + if (!payload.length) { + return; + } + const data = await this.KubernetesStatefulSets(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch statefulSet', err); + } + } + + patch(oldStatefulSet, newStatefulSet) { + return this.$async(this.patchAsync, oldStatefulSet, newStatefulSet); + } + + /** + * DELETE + */ + async deleteAsync(statefulSet) { + try { + const params = new KubernetesCommonParams(); + params.id = statefulSet.Name; + const namespace = statefulSet.Namespace; + await this.KubernetesStatefulSets(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove statefulSet', err); + } + } + + delete(statefulSet) { + return this.$async(this.deleteAsync, statefulSet); + } + + /** + * ROLLBACK + */ + async rollbackAsync(namespace, name, payload) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + await this.KubernetesStatefulSets(namespace).rollback(params, payload).$promise; + } catch (err) { + throw new PortainerError('Unable to rollback statefulSet', err); + } + } + + rollback(namespace, name, payload) { + return this.$async(this.rollbackAsync, namespace, name, payload); + } +} + +export default KubernetesStatefulSetService; +angular.module('portainer.kubernetes').service('KubernetesStatefulSetService', KubernetesStatefulSetService); diff --git a/app/kubernetes/services/storageService.js b/app/kubernetes/services/storageService.js new file mode 100644 index 000000000..ea2a5f053 --- /dev/null +++ b/app/kubernetes/services/storageService.js @@ -0,0 +1,37 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import PortainerError from 'Portainer/error'; +import KubernetesStorageClassConverter from 'Kubernetes/converters/storageClass'; + +class KubernetesStorageService { + /* @ngInject */ + constructor($async, KubernetesStorage) { + this.$async = $async; + this.KubernetesStorage = KubernetesStorage; + + this.getAsync = this.getAsync.bind(this); + } + + /** + * GET + */ + async getAsync(endpointId) { + try { + const params = { + endpointId: endpointId, + }; + const classes = await this.KubernetesStorage().get(params).$promise; + const res = _.map(classes.items, (item) => KubernetesStorageClassConverter.apiToStorageClass(item)); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve storage classes', err); + } + } + + get(endpointId) { + return this.$async(this.getAsync, endpointId); + } +} + +export default KubernetesStorageService; +angular.module('portainer.kubernetes').service('KubernetesStorageService', KubernetesStorageService); diff --git a/app/kubernetes/services/volumeService.js b/app/kubernetes/services/volumeService.js new file mode 100644 index 000000000..b65b4cb93 --- /dev/null +++ b/app/kubernetes/services/volumeService.js @@ -0,0 +1,70 @@ +import angular from 'angular'; +import _ from 'lodash-es'; + +import KubernetesVolumeConverter from 'Kubernetes/converters/volume'; + +class KubernetesVolumeService { + /* @ngInject */ + constructor($async, KubernetesResourcePoolService, KubernetesApplicationService, KubernetesPersistentVolumeClaimService) { + this.$async = $async; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const [pvc, pool] = await Promise.all([await this.KubernetesPersistentVolumeClaimService.get(namespace, name), await this.KubernetesResourcePoolService.get(namespace)]); + return KubernetesVolumeConverter.pvcToVolume(pvc, pool); + } catch (err) { + throw err; + } + } + + async getAllAsync(namespace) { + try { + const pools = await this.KubernetesResourcePoolService.get(namespace); + const res = await Promise.all( + _.map(pools, async (pool) => { + const pvcs = await this.KubernetesPersistentVolumeClaimService.get(pool.Namespace.Name); + return _.map(pvcs, (pvc) => KubernetesVolumeConverter.pvcToVolume(pvc, pool)); + }) + ); + return _.flatten(res); + } catch (err) { + throw err; + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } + + /** + * DELETE + */ + async deleteAsync(volume) { + try { + await this.KubernetesPersistentVolumeClaimService.delete(volume.PersistentVolumeClaim); + } catch (err) { + throw err; + } + } + + delete(volume) { + return this.$async(this.deleteAsync, volume); + } +} + +export default KubernetesVolumeService; +angular.module('portainer.kubernetes').service('KubernetesVolumeService', KubernetesVolumeService); diff --git a/app/kubernetes/views/applications/applications.html b/app/kubernetes/views/applications/applications.html new file mode 100644 index 000000000..d60033ade --- /dev/null +++ b/app/kubernetes/views/applications/applications.html @@ -0,0 +1,60 @@ + + Applications + + + + +
+ + +

+ + As an administrator user, you have access to the advanced deployment feature allowing you to deploy any Kubernetes manifest inside your cluster. +

+

+ +

+
+
+ +
+
+ + + + + Applications + + + + + Port mappings + + + + + Stacks + + + + + + +
+
+
diff --git a/app/kubernetes/views/applications/applications.js b/app/kubernetes/views/applications/applications.js new file mode 100644 index 000000000..e4b0de22c --- /dev/null +++ b/app/kubernetes/views/applications/applications.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationsView', { + templateUrl: './applications.html', + controller: 'KubernetesApplicationsController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js new file mode 100644 index 000000000..859698a00 --- /dev/null +++ b/app/kubernetes/views/applications/applicationsController.js @@ -0,0 +1,139 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +class KubernetesApplicationsController { + /* @ngInject */ + constructor($async, $state, Notifications, KubernetesApplicationService, Authentication, ModalService, LocalStorage) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.KubernetesApplicationService = KubernetesApplicationService; + this.Authentication = Authentication; + this.ModalService = ModalService; + this.LocalStorage = LocalStorage; + + this.onInit = this.onInit.bind(this); + this.getApplications = this.getApplications.bind(this); + this.getApplicationsAsync = this.getApplicationsAsync.bind(this); + this.removeAction = this.removeAction.bind(this); + this.removeActionAsync = this.removeActionAsync.bind(this); + this.removeStacksAction = this.removeStacksAction.bind(this); + this.removeStacksActionAsync = this.removeStacksActionAsync.bind(this); + this.onPublishingModeClick = this.onPublishingModeClick.bind(this); + } + + selectTab(index) { + this.LocalStorage.storeActiveTab('applications', index); + } + + async removeStacksActionAsync(selectedItems) { + let actionCount = selectedItems.length; + for (const stack of selectedItems) { + try { + const promises = _.map(stack.Applications, (app) => this.KubernetesApplicationService.delete(app)); + await Promise.all(promises); + this.Notifications.success('Stack successfully removed', stack.Name); + _.remove(this.stacks, { Name: stack.Name }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to remove stack'); + } finally { + --actionCount; + if (actionCount === 0) { + this.$state.reload(); + } + } + } + } + + removeStacksAction(selectedItems) { + this.ModalService.confirmDeletion( + 'Are you sure that you want to remove the selected stack(s) ? This will remove all the applications associated to the stack(s).', + (confirmed) => { + if (confirmed) { + return this.$async(this.removeStacksActionAsync, selectedItems); + } + } + ); + } + + async removeActionAsync(selectedItems) { + let actionCount = selectedItems.length; + for (const application of selectedItems) { + try { + await this.KubernetesApplicationService.delete(application); + this.Notifications.success('Application successfully removed', application.Name); + const index = this.applications.indexOf(application); + this.applications.splice(index, 1); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to remove application'); + } finally { + --actionCount; + if (actionCount === 0) { + this.$state.reload(); + } + } + } + } + + removeAction(selectedItems) { + return this.$async(this.removeActionAsync, selectedItems); + } + + onPublishingModeClick(application) { + this.state.activeTab = 1; + _.forEach(this.ports, (item) => { + item.Expanded = false; + item.Highlighted = false; + if (item.Name === application.Name) { + if (item.Ports.length > 1) { + item.Expanded = true; + } + item.Highlighted = true; + } + }); + } + + async getApplicationsAsync() { + try { + this.applications = await this.KubernetesApplicationService.get(); + this.stacks = KubernetesStackHelper.stacksFromApplications(this.applications); + this.ports = KubernetesApplicationHelper.portMappingsFromApplications(this.applications); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve applications'); + } + } + + getApplications() { + return this.$async(this.getApplicationsAsync); + } + + async onInit() { + this.state = { + activeTab: 0, + currentName: this.$state.$current.name, + isAdmin: this.Authentication.isAdmin(), + viewReady: false, + }; + + this.state.activeTab = this.LocalStorage.getActiveTab('applications'); + + await this.getApplications(); + + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } + + $onDestroy() { + if (this.state.currentName !== this.$state.$current.name) { + this.LocalStorage.storeActiveTab('applications', 0); + } + } +} + +export default KubernetesApplicationsController; +angular.module('portainer.kubernetes').controller('KubernetesApplicationsController', KubernetesApplicationsController); diff --git a/app/kubernetes/views/applications/console/console.html b/app/kubernetes/views/applications/console/console.html new file mode 100644 index 000000000..51bb6ea3e --- /dev/null +++ b/app/kubernetes/views/applications/console/console.html @@ -0,0 +1,71 @@ + + Resource pools > + {{ ctrl.application.ResourcePool }} > + Applications > + {{ ctrl.application.Name }} > Pods > + {{ ctrl.podName }} > Console + + + + +
+
+
+ + +
+
+ Console +
+ +
+ +
+ + + + +
+
+ +
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+
+
diff --git a/app/kubernetes/views/applications/console/console.js b/app/kubernetes/views/applications/console/console.js new file mode 100644 index 000000000..2a44c97b4 --- /dev/null +++ b/app/kubernetes/views/applications/console/console.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationConsoleView', { + templateUrl: './console.html', + controller: 'KubernetesApplicationConsoleController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/applications/console/consoleController.js b/app/kubernetes/views/applications/console/consoleController.js new file mode 100644 index 000000000..ccf8e37ed --- /dev/null +++ b/app/kubernetes/views/applications/console/consoleController.js @@ -0,0 +1,113 @@ +import angular from 'angular'; +import { Terminal } from 'xterm'; + +class KubernetesApplicationConsoleController { + /* @ngInject */ + constructor($async, $state, Notifications, KubernetesApplicationService, EndpointProvider, LocalStorage) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.KubernetesApplicationService = KubernetesApplicationService; + this.EndpointProvider = EndpointProvider; + this.LocalStorage = LocalStorage; + + this.onInit = this.onInit.bind(this); + } + + disconnect() { + this.state.socket.close(); + this.state.term.dispose(); + this.state.connected = false; + } + + configureSocketAndTerminal(socket, term) { + socket.onopen = function () { + const terminal_container = document.getElementById('terminal-container'); + term.open(terminal_container); + term.setOption('cursorBlink', true); + term.focus(); + }; + + term.on('data', function (data) { + socket.send(data); + }); + + socket.onmessage = function (msg) { + term.write(msg.data); + }; + + socket.onerror = function (err) { + this.disconnect(); + this.Notifications.error('Failure', err, 'Websocket connection error'); + }.bind(this); + + this.state.socket.onclose = function () { + this.disconnect(); + }.bind(this); + + this.state.connected = true; + } + + connectConsole() { + const params = { + token: this.LocalStorage.getJWT(), + endpointId: this.EndpointProvider.endpointID(), + namespace: this.application.ResourcePool, + podName: this.podName, + containerName: this.application.Pods[0].Containers[0].name, + command: this.state.command, + }; + + let url = + window.location.href.split('#')[0] + + 'api/websocket/pod?' + + Object.keys(params) + .map((k) => k + '=' + params[k]) + .join('&'); + if (url.indexOf('https') > -1) { + url = url.replace('https://', 'wss://'); + } else { + url = url.replace('http://', 'ws://'); + } + + this.state.socket = new WebSocket(url); + this.state.term = new Terminal(); + + this.configureSocketAndTerminal(this.state.socket, this.state.term); + } + + async onInit() { + const availableCommands = ['/bin/bash', '/bin/sh']; + + this.state = { + actionInProgress: false, + availableCommands: availableCommands, + command: availableCommands[1], + connected: false, + socket: null, + term: null, + viewReady: false, + }; + + const podName = this.$transition$.params().pod; + const applicationName = this.$transition$.params().name; + const namespace = this.$transition$.params().namespace; + + this.podName = podName; + + try { + this.application = await this.KubernetesApplicationService.get(namespace, applicationName); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve application logs'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesApplicationConsoleController; +angular.module('portainer.kubernetes').controller('KubernetesApplicationConsoleController', KubernetesApplicationConsoleController); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html new file mode 100644 index 000000000..d9421b904 --- /dev/null +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -0,0 +1,904 @@ + + Applications > Create an application + + + Resource pools > + {{ ctrl.application.ResourcePool }} > + Applications > + {{ ctrl.application.Name }} > Edit + + + + +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+

This field must consist of lower case alphanumeric characters or '-', start with an alphabetic + character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').

+
+

An application with the same name already exists inside the selected resource pool.

+
+
+ + + +
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+ + +
+ Resource pool +
+ + +
+ +
+ +
+
+
+
+ + This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the + resource pool. +
+
+ + +
+ Stack +
+ +
+
+ + Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to use the + application name. +
+
+ + +
+ +
+ +
+
+ + +
+ Environment +
+ + +
+
+ + + add environment variable + +
+ +
+
+
+
+ name + +
+
+ +

Environment variable name is required.

+
+

This environment variable is already defined.

+
+
+ +
+ value + +
+ +
+ + +
+
+
+
+ + +
+ Configurations +
+ + +
+
+ + + add configuration + +
+
+ + Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key via + the override button. +
+
+ + +
+ +
+ +
+
+ + + +
+ +
+
+
+ The following keys will be loaded from the {{ config.SelectedConfiguration.Name }} configuration as environment variables: + + {{ key }}{{ $last ? '' : ', ' }} + +
+
+ + + +
+
+
+
+ configuration key + +
+ +
+
+ path on disk + +
+
+ +

Path is required.

+
+

This path is already used.

+
+
+ +
+ + +
+
+
+ +
+ + + +
+ Persisting data +
+ +
+
+ + No storage option is available to persist data, contact your administrator to enable a storage option. +
+
+ + +
+
+ + + add persisted folder + +
+ +
+
+
+ path in container + +
+ +
+ requested size + + + + +
+ +
+ storage + + +
+ +
+ + +
+
+ +
+
+
+ +

Path is required.

+
+

This path is already defined.

+
+
+ +
+
+ +

Size is required.

+

This value must be greater than zero.

+
+
+
+ +
+ +
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ Specify how the data will be used across instances. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ Resource reservations +
+ +
+
+ + Resource reservations are applied per instance of the application. +
+
+ +
+
+ + A resource quota is set on this resource pool, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums + are inherited from the resource pool quota. +
+
+ +
+
+ + This resource pool has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the + resource pool. +
+
+ + +
+ +
+ +
+
+ +
+
+

+ Maximum memory usage (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.state.sliders.memory.min }} and {{ ctrl.state.sliders.memory.max }} +

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ + +
+ Deployment +
+ +
+
+ Select how you want to deploy your application inside the cluster. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + +
+
+ + +
+
+ + This application will reserve the following resources: {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and + {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. +
+
+ +
+
+ + This application would exceed available resources. Please review resource reservations or the instance count. +
+
+ +
+
+ + The following storage option(s) do not support concurrent access from multiples instances: {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. +
+
+ +
+ Publishing the application +
+ +
+
+ Select how you want to publish your application. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + + publish a new port + +
+ +
+ + When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a port + number inside the default range 30000-32767. +
+ +
+
+
+ container port + +
+ +
+ node port + +
+ +
+ load balancer port + +
+ +
+
+ + +
+ +
+
+ +
+
+
+
+

Container port number is required.

+

Container port number must be inside the range 1-65535.

+

Container port number must be inside the range 1-65535.

+
+
+
+ +
+
+
+

Node port number must be inside the range 30000-32767.

+

Node port number must be inside the range 30000-32767.

+
+
+
+ +
+
+
+

Load balancer port number is required.

+

Load balancer port number must be inside the range 1-65535.

+

Load balancer port number must be inside the range 1-65535.

+
+
+
+ +
+
+
+
+ + +
+ Actions +
+ +
+
+ + +
+
+
+
+
+
+
+
diff --git a/app/kubernetes/views/applications/create/createApplication.js b/app/kubernetes/views/applications/create/createApplication.js new file mode 100644 index 000000000..382b0227a --- /dev/null +++ b/app/kubernetes/views/applications/create/createApplication.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesCreateApplicationView', { + templateUrl: './createApplication.html', + controller: 'KubernetesCreateApplicationController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js new file mode 100644 index 000000000..15ad90b2b --- /dev/null +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -0,0 +1,627 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; +import * as JsonPatch from 'fast-json-patch'; + +import { + KubernetesApplicationDataAccessPolicies, + KubernetesApplicationDeploymentTypes, + KubernetesApplicationPublishingTypes, + KubernetesApplicationQuotaDefaults, +} from 'Kubernetes/models/application/models'; +import { + KubernetesApplicationConfigurationFormValue, + KubernetesApplicationConfigurationFormValueOverridenKey, + KubernetesApplicationConfigurationFormValueOverridenKeyTypes, + KubernetesApplicationEnvironmentVariableFormValue, + KubernetesApplicationFormValues, + KubernetesApplicationPersistedFolderFormValue, + KubernetesApplicationPublishedPortFormValue, +} from 'Kubernetes/models/application/formValues'; +import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper'; +import KubernetesApplicationConverter from 'Kubernetes/converters/application'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; + +class KubernetesCreateApplicationController { + /* @ngInject */ + constructor( + $async, + $state, + Notifications, + EndpointProvider, + Authentication, + ModalService, + KubernetesResourcePoolService, + KubernetesApplicationService, + KubernetesStackService, + KubernetesConfigurationService, + KubernetesNodeService, + KubernetesPersistentVolumeClaimService + ) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.EndpointProvider = EndpointProvider; + this.Authentication = Authentication; + this.ModalService = ModalService; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesStackService = KubernetesStackService; + this.KubernetesConfigurationService = KubernetesConfigurationService; + this.KubernetesNodeService = KubernetesNodeService; + this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; + + this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; + this.ApplicationPublishingTypes = KubernetesApplicationPublishingTypes; + this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes; + this.ServiceTypes = KubernetesServiceTypes; + + this.onInit = this.onInit.bind(this); + this.updateApplicationAsync = this.updateApplicationAsync.bind(this); + this.deployApplicationAsync = this.deployApplicationAsync.bind(this); + this.updateSlidersAsync = this.updateSlidersAsync.bind(this); + this.refreshStacksAsync = this.refreshStacksAsync.bind(this); + this.refreshConfigurationsAsync = this.refreshConfigurationsAsync.bind(this); + this.refreshApplicationsAsync = this.refreshApplicationsAsync.bind(this); + this.refreshStacksConfigsAppsAsync = this.refreshStacksConfigsAppsAsync.bind(this); + this.getApplicationAsync = this.getApplicationAsync.bind(this); + } + + isValid() { + return !this.state.alreadyExists && !this.state.hasDuplicateEnvironmentVariables && !this.state.hasDuplicatePersistedFolderPaths && !this.state.hasDuplicateConfigurationPaths; + } + + onChangeName() { + const existingApplication = _.find(this.applications, { Name: this.formValues.Name }); + this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication); + } + + /** + * CONFIGURATION UI MANAGEMENT + */ + addConfiguration() { + let config = new KubernetesApplicationConfigurationFormValue(); + config.SelectedConfiguration = this.configurations[0]; + this.formValues.Configurations.push(config); + } + + removeConfiguration(index) { + this.formValues.Configurations.splice(index, 1); + this.onChangeConfigurationPath(); + } + + overrideConfiguration(index) { + const config = this.formValues.Configurations[index]; + config.Overriden = true; + config.OverridenKeys = _.map(_.keys(config.SelectedConfiguration.Data), (key) => { + const res = new KubernetesApplicationConfigurationFormValueOverridenKey(); + res.Key = key; + return res; + }); + } + + resetConfiguration(index) { + const config = this.formValues.Configurations[index]; + config.Overriden = false; + config.OverridenKeys = []; + this.onChangeConfigurationPath(); + } + + onChangeConfigurationPath() { + this.state.duplicateConfigurationPaths = []; + + const paths = _.reduce( + this.formValues.Configurations, + (result, config) => { + const uniqOverridenKeysPath = _.uniq(_.map(config.OverridenKeys, 'Path')); + return _.concat(result, uniqOverridenKeysPath); + }, + [] + ); + + const duplicatePaths = KubernetesFormValidationHelper.getDuplicates(paths); + + _.forEach(this.formValues.Configurations, (config, index) => { + _.forEach(config.OverridenKeys, (overridenKey, keyIndex) => { + const findPath = _.find(duplicatePaths, (path) => path === overridenKey.Path); + if (findPath) { + this.state.duplicateConfigurationPaths[index + '_' + keyIndex] = findPath; + } + }); + }); + + this.state.hasDuplicateConfigurationPaths = Object.keys(this.state.duplicateConfigurationPaths).length > 0; + } + /** + * !CONFIGURATION UI MANAGEMENT + */ + + /** + * ENVIRONMENT UI MANAGEMENT + */ + addEnvironmentVariable() { + this.formValues.EnvironmentVariables.push(new KubernetesApplicationEnvironmentVariableFormValue()); + } + + hasEnvironmentVariables() { + return this.formValues.EnvironmentVariables.length > 0; + } + + onChangeEnvironmentName() { + this.state.duplicateEnvironmentVariables = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.EnvironmentVariables, 'Name')); + this.state.hasDuplicateEnvironmentVariables = Object.keys(this.state.duplicateEnvironmentVariables).length > 0; + } + + restoreEnvironmentVariable(index) { + this.formValues.EnvironmentVariables[index].NeedsDeletion = false; + } + + removeEnvironmentVariable(index) { + if (this.state.isEdit && !this.formValues.EnvironmentVariables[index].IsNew) { + this.formValues.EnvironmentVariables[index].NeedsDeletion = true; + } else { + this.formValues.EnvironmentVariables.splice(index, 1); + } + this.onChangeEnvironmentName(); + } + /** + * !ENVIRONMENT UI MANAGEMENT + */ + + /** + * PERSISTENT FOLDERS UI MANAGEMENT + */ + addPersistedFolder() { + let storageClass = {}; + if (this.storageClasses.length > 0) { + storageClass = this.storageClasses[0]; + } + + this.formValues.PersistedFolders.push(new KubernetesApplicationPersistedFolderFormValue(storageClass)); + this.resetDeploymentType(); + } + + onChangePersistedFolderPath() { + this.state.duplicatePersistedFolderPaths = KubernetesFormValidationHelper.getDuplicates(_.map(this.formValues.PersistedFolders, 'ContainerPath')); + this.state.hasDuplicatePersistedFolderPaths = Object.keys(this.state.duplicatePersistedFolderPaths).length > 0; + } + + restorePersistedFolder(index) { + this.formValues.PersistedFolders[index].NeedsDeletion = false; + } + + removePersistedFolder(index) { + if (this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName) { + this.formValues.PersistedFolders[index].NeedsDeletion = true; + } else { + this.formValues.PersistedFolders.splice(index, 1); + } + this.onChangePersistedFolderPath(); + } + /** + * !PERSISTENT FOLDERS UI MANAGEMENT + */ + + /** + * PUBLISHED PORTS UI MANAGEMENT + */ + addPublishedPort() { + this.formValues.PublishedPorts.push(new KubernetesApplicationPublishedPortFormValue()); + } + + removePublishedPort(index) { + this.formValues.PublishedPorts.splice(index, 1); + } + /** + * !PUBLISHED PORTS UI MANAGEMENT + */ + + /** + * STATE VALIDATION FUNCTIONS + */ + storageClassAvailable() { + return this.storageClasses && this.storageClasses.length > 0; + } + + hasMultipleStorageClassesAvailable() { + return this.storageClasses && this.storageClasses.length > 1; + } + + resetDeploymentType() { + this.formValues.DeploymentType = this.ApplicationDeploymentTypes.REPLICATED; + } + + // The data access policy panel is not shown when: + // * There is not persisted folder specified + showDataAccessPolicySection() { + return this.formValues.PersistedFolders.length !== 0; + } + + // A global deployment is not available when either: + // * For each persisted folder specified, if one of the storage object only supports the RWO access mode + // * The data access policy is set to ISOLATED + supportGlobalDeployment() { + const hasFolders = this.formValues.PersistedFolders.length !== 0; + const hasRWOOnly = _.find(this.formValues.PersistedFolders, (item) => _.isEqual(item.StorageClass.AccessModes, ['RWO'])); + const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED; + + if ((hasFolders && hasRWOOnly) || isIsolated) { + return false; + } + return true; + } + + // A StatefulSet is defined by DataAccessPolicy === ISOLATED + isEditAndStatefulSet() { + return this.state.isEdit && this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED; + } + + // A scalable deployment is available when either: + // * No persisted folders are specified + // * The access policy is set to shared and for each persisted folders specified, all the associated + // storage objects support at least RWX access mode (no RWO only) + // * The access policy is set to isolated + supportScalableReplicaDeployment() { + const hasFolders = this.formValues.PersistedFolders.length !== 0; + const hasRWOOnly = _.find(this.formValues.PersistedFolders, (item) => _.isEqual(item.StorageClass.AccessModes, ['RWO'])); + const isIsolated = this.formValues.DataAccessPolicy === this.ApplicationDataAccessPolicies.ISOLATED; + + if (!hasFolders || isIsolated || (hasFolders && !hasRWOOnly)) { + return true; + } + return false; + } + + // For each persisted folders, returns the non scalable deployments options (storage class that only supports RWO) + getNonScalableStorage() { + let storageOptions = []; + + for (let i = 0; i < this.formValues.PersistedFolders.length; i++) { + const folder = this.formValues.PersistedFolders[i]; + + if (_.isEqual(folder.StorageClass.AccessModes, ['RWO'])) { + storageOptions.push(folder.StorageClass.Name); + } + } + + return _.uniq(storageOptions).join(', '); + } + + enforceReplicaCountMinimum() { + if (this.formValues.ReplicaCount === null) { + this.formValues.ReplicaCount = 1; + } + } + + resourceQuotaCapacityExceeded() { + return !this.state.sliders.memory.max || !this.state.sliders.cpu.max; + } + + resourceReservationsOverflow() { + const instances = this.formValues.ReplicaCount; + const cpu = this.formValues.CpuLimit; + const maxCpu = this.state.sliders.cpu.max; + const memory = this.formValues.MemoryLimit; + const maxMemory = this.state.sliders.memory.max; + + if (cpu * instances > maxCpu) { + return true; + } + + if (memory * instances > maxMemory) { + return true; + } + + return false; + } + + publishViaLoadBalancerEnabled() { + return this.state.useLoadBalancer; + } + + isEditAndNoChangesMade() { + if (!this.state.isEdit) return false; + const changes = JsonPatch.compare(this.savedFormValues, this.formValues); + this.editChanges = _.filter(changes, (change) => !_.includes(change.path, '$$hashKey') && change.path !== '/ApplicationType'); + return !this.editChanges.length; + } + + isEditAndExistingPersistedFolder(index) { + return this.state.isEdit && this.formValues.PersistedFolders[index].PersistentVolumeClaimName; + } + + isNonScalable() { + const scalable = this.supportScalableReplicaDeployment(); + const global = this.supportGlobalDeployment(); + const replica = this.formValues.ReplicaCount > 1; + const replicated = this.formValues.DeploymentType === this.ApplicationDeploymentTypes.REPLICATED; + const res = (replicated && !scalable && replica) || (!replicated && !global); + return res; + } + + isDeployUpdateButtonDisabled() { + const overflow = this.resourceReservationsOverflow(); + const inProgress = this.state.actionInProgress; + const invalid = !this.isValid(); + const hasNoChanges = this.isEditAndNoChangesMade(); + const nonScalable = this.isNonScalable(); + const res = overflow || inProgress || invalid || hasNoChanges || nonScalable; + return res; + } + + disableLoadBalancerEdit() { + return ( + this.state.isEdit && + this.application.ServiceType === this.ServiceTypes.LOAD_BALANCER && + !this.application.LoadBalancerIPAddress && + this.formValues.PublishingType === this.ApplicationPublishingTypes.LOAD_BALANCER + ); + } + /** + * !STATE VALIDATION FUNCTIONS + */ + + /** + * DATA AUTO REFRESH + */ + async updateSlidersAsync() { + try { + const quota = this.formValues.ResourcePool.Quota; + let minCpu, + maxCpu, + minMemory, + maxMemory = 0; + if (quota) { + this.state.resourcePoolHasQuota = true; + if (quota.CpuLimit) { + minCpu = KubernetesApplicationQuotaDefaults.CpuLimit; + maxCpu = quota.CpuLimit - quota.CpuLimitUsed; + if (this.state.isEdit && this.savedFormValues.CpuLimit) { + maxCpu += this.savedFormValues.CpuLimit * this.savedFormValues.ReplicaCount; + } + } else { + minCpu = 0; + maxCpu = this.state.nodes.cpu; + } + if (quota.MemoryLimit) { + minMemory = KubernetesApplicationQuotaDefaults.MemoryLimit; + maxMemory = quota.MemoryLimit - quota.MemoryLimitUsed; + if (this.state.isEdit && this.savedFormValues.MemoryLimit) { + maxMemory += KubernetesResourceReservationHelper.bytesValue(this.savedFormValues.MemoryLimit) * this.savedFormValues.ReplicaCount; + } + } else { + minMemory = 0; + maxMemory = this.state.nodes.memory; + } + } else { + this.state.resourcePoolHasQuota = false; + minCpu = 0; + maxCpu = this.state.nodes.cpu; + minMemory = 0; + maxMemory = this.state.nodes.memory; + } + this.state.sliders.memory.min = minMemory; + this.state.sliders.memory.max = KubernetesResourceReservationHelper.megaBytesValue(maxMemory); + this.state.sliders.cpu.min = minCpu; + this.state.sliders.cpu.max = _.round(maxCpu, 2); + if (!this.state.isEdit) { + this.formValues.CpuLimit = minCpu; + this.formValues.MemoryLimit = minMemory; + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to update resources selector'); + } + } + + updateSliders() { + return this.$async(this.updateSlidersAsync); + } + + async refreshStacksAsync(namespace) { + try { + this.stacks = await this.KubernetesStackService.get(namespace); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve stacks'); + } + } + + refreshStacks(namespace) { + return this.$async(this.refreshStacksAsync, namespace); + } + + async refreshConfigurationsAsync(namespace) { + try { + this.configurations = await this.KubernetesConfigurationService.get(namespace); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve configurations'); + } + } + + refreshConfigurations(namespace) { + return this.$async(this.refreshConfigurationsAsync, namespace); + } + + async refreshApplicationsAsync(namespace) { + try { + this.applications = await this.KubernetesApplicationService.get(namespace); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve applications'); + } + } + + refreshApplications(namespace) { + return this.$async(this.refreshApplicationsAsync, namespace); + } + + async refreshStacksConfigsAppsAsync(namespace) { + await Promise.all([this.refreshStacks(namespace), this.refreshConfigurations(namespace), this.refreshApplications(namespace)]); + this.onChangeName(); + } + + refreshStacksConfigsApps(namespace) { + return this.$async(this.refreshStacksConfigsAppsAsync, namespace); + } + + onResourcePoolSelectionChange() { + const namespace = this.formValues.ResourcePool.Namespace.Name; + this.updateSliders(); + this.refreshStacksConfigsApps(namespace); + this.formValues.Configurations = []; + } + /** + * !DATA AUTO REFRESH + */ + + /** + * ACTIONS + */ + async deployApplicationAsync() { + this.state.actionInProgress = true; + try { + this.formValues.ApplicationOwner = this.Authentication.getUserDetails().username; + _.remove(this.formValues.Configurations, (item) => item.SelectedConfiguration === undefined); + await this.KubernetesApplicationService.create(this.formValues); + this.Notifications.success('Application successfully deployed', this.formValues.Name); + this.$state.go('kubernetes.applications'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to create application'); + } finally { + this.state.actionInProgress = false; + } + } + + async updateApplicationAsync() { + try { + this.state.actionInProgress = true; + await this.KubernetesApplicationService.patch(this.savedFormValues, this.formValues); + this.Notifications.success('Application successfully updated'); + this.$state.go('kubernetes.applications.application', { name: this.application.Name, namespace: this.application.ResourcePool }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve application related events'); + } finally { + this.state.actionInProgress = false; + } + } + + deployApplication() { + if (this.state.isEdit) { + this.ModalService.confirmUpdate('Updating the application may cause a service interruption. Do you wish to continue?', (confirmed) => { + if (confirmed) { + return this.$async(this.updateApplicationAsync); + } + }); + } else { + return this.$async(this.deployApplicationAsync); + } + } + /** + * !ACTIONS + */ + + /** + * APPLICATION - used on edit context only + */ + async getApplicationAsync() { + try { + const namespace = this.state.params.namespace; + [this.application, this.persistentVolumeClaims] = await Promise.all([ + this.KubernetesApplicationService.get(namespace, this.state.params.name), + this.KubernetesPersistentVolumeClaimService.get(namespace), + ]); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve application details'); + } + } + + getApplication() { + return this.$async(this.getApplicationAsync); + } + /** + * !APPLICATION + */ + + async onInit() { + try { + this.state = { + actionInProgress: false, + useLoadBalancer: false, + sliders: { + cpu: { + min: 0, + max: 0, + }, + memory: { + min: 0, + max: 0, + }, + }, + nodes: { + memory: 0, + cpu: 0, + }, + resourcePoolHasQuota: false, + viewReady: false, + availableSizeUnits: ['MB', 'GB', 'TB'], + alreadyExists: false, + duplicateEnvironmentVariables: {}, + hasDuplicateEnvironmentVariables: false, + duplicatePersistedFolderPaths: {}, + hasDuplicatePersistedFolderPaths: false, + duplicateConfigurationPaths: {}, + hasDuplicateConfigurationPaths: false, + isEdit: false, + params: { + namespace: this.$transition$.params().namespace, + name: this.$transition$.params().name, + }, + }; + + this.editChanges = []; + + if (this.$transition$.params().namespace && this.$transition$.params().name) { + this.state.isEdit = true; + } + + const endpoint = this.EndpointProvider.currentEndpoint(); + this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses; + this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer; + + this.formValues = new KubernetesApplicationFormValues(); + + const [resourcePools, nodes] = await Promise.all([this.KubernetesResourcePoolService.get(), this.KubernetesNodeService.get()]); + + this.resourcePools = resourcePools; + this.formValues.ResourcePool = this.resourcePools[0]; + + _.forEach(nodes, (item) => { + this.state.nodes.memory += filesizeParser(item.Memory); + this.state.nodes.cpu += item.CPU; + }); + + const namespace = this.state.isEdit ? this.state.params.namespace : this.formValues.ResourcePool.Namespace.Name; + await this.refreshStacksConfigsApps(namespace); + + if (this.state.isEdit) { + await this.getApplication(); + this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims); + this.savedFormValues = angular.copy(this.formValues); + delete this.formValues.ApplicationType; + } + + await this.updateSliders(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesCreateApplicationController; +angular.module('portainer.kubernetes').controller('KubernetesCreateApplicationController', KubernetesCreateApplicationController); diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html new file mode 100644 index 000000000..0ea9118ba --- /dev/null +++ b/app/kubernetes/views/applications/edit/application.html @@ -0,0 +1,477 @@ + + Resource pools > + {{ ctrl.application.ResourcePool }} > + Applications > {{ ctrl.application.Name }} + + + + +
+
+
+ + + + + Application +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ ctrl.application.Name }} + external +
Stack{{ ctrl.application.StackName }}
Resource pool + {{ ctrl.application.ResourcePool }} + system +
Deployment + Replicated + Global + {{ ctrl.application.RunningPodsCount }} / {{ ctrl.application.TotalPodsCount }} +
+
Resource reservations
+
per instance
+
+
CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}
+
Memory {{ ctrl.application.Requests.Memory | humansize }}
+
Creation + {{ ctrl.application.ApplicationOwner }} + {{ ctrl.application.CreationDate | getisodate }} +
+
+
+
+ Note + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ + + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ +
+ + + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+ + + +
+ + +
+ Accessing the application +
+ +
+ + This application is not exposing any port. +
+ +
+
+
+ + This application is exposed through an external load balancer. Use the links below to access the different ports exposed. +
+
+ +

Load balancer status: pending

+

+ what does the "pending" status means? + + +

+
+ +

Load balancer status: available

+

+ Load balancer IP address: {{ ctrl.application.LoadBalancerIPAddress }} + + Copy + + +

+
+
+
+ + + + + + + + + + + +
Container portLoad balancer port
{{ port.targetPort }} + {{ port.port }} + + access + +
+
+
+ +
+
+ + This application is exposed globally on all nodes of your cluster. It can be reached using the IP address of any node in your cluster using the port configuration + below. +
+
+ + + + + + + + + + + +
Container portCluster node port
{{ port.targetPort }}{{ port.nodePort }}
+
+
+ +
+
+ + This application is only available for internal usage inside the cluster via the application name {{ ctrl.application.ServiceName }} + Copy + +
+
+

Refer to the below port configuration to access the application.

+
+
+ + + + + + + + + + + + + +
Container portApplication portProtocol
{{ port.targetPort }}{{ port.port }}{{ port.protocol }}
+
+
+
+ + +
Auto-scaling
+ +
+ + This application does not have an autoscaling policy defined. +
+ +
+
+ + + + + + + + + + + + + +
Minimum instancesMaximum instances + Target CPU usage + +
{{ ctrl.application.AutoScaler.MinReplicas }}{{ ctrl.application.AutoScaler.MaxReplicas }}{{ ctrl.application.AutoScaler.TargetCPUUtilizationPercentage }}%
+
+
+ + + +
Configuration
+ +
+ + This application is not using any environment variable or configuration. +
+ +
+
+ + + + + + + + + + + + + +
Environment variableValueConfiguration
{{ envvar.name }} + {{ envvar.value }} + {{ envvar.valueFrom.configMapKeyRef.key }} + {{ envvar.valueFrom.secretKeyRef.key }} + {{ envvar.valueFrom.fieldRef.fieldPath }} (downward API) + - + + - + {{ envvar.valueFrom.configMapKeyRef.name }} + {{ envvar.valueFrom.secretKeyRef.name }} +
+
+
+ +
+ + + + + + + + + + + + + + +
Configuration pathValueConfiguration
+ {{ volume.fileMountPath }} + {{ volume.configurationKey ? volume.configurationKey : '-' }} + {{ volume.configurationName }} +
+
+ + + +
+ Data persistence +
+ +
+ + This application has no persisted folders. +
+ +
+
+ Data access policy: + {{ ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyText }} + +
+ + + + + + + + + + + + + +
Persisted folderPersistence
+ {{ volume.MountPath }} + + {{ volume.PersistentVolumeClaimName }} + {{ volume.HostPath }} on host filesystem
+ + + + + + + + + + + + + + + + + +
PodPersisted folderPersistence
+ {{ pod.Name }} + + {{ volume.MountPath }} + + + {{ volume.PersistentVolumeClaimName + '-' + pod.Name }} + {{ volume.HostPath }} on host filesystem
+ +
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/applications/edit/application.js b/app/kubernetes/views/applications/edit/application.js new file mode 100644 index 000000000..ae935f0f4 --- /dev/null +++ b/app/kubernetes/views/applications/edit/application.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationView', { + templateUrl: './application.html', + controller: 'KubernetesApplicationController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js new file mode 100644 index 000000000..0aa896c25 --- /dev/null +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -0,0 +1,245 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; +import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; + +class KubernetesApplicationController { + /* @ngInject */ + constructor( + $async, + $state, + clipboard, + Notifications, + LocalStorage, + ModalService, + KubernetesApplicationService, + KubernetesEventService, + KubernetesStackService, + KubernetesPodService, + KubernetesNamespaceHelper + ) { + this.$async = $async; + this.$state = $state; + this.clipboard = clipboard; + this.Notifications = Notifications; + this.LocalStorage = LocalStorage; + this.ModalService = ModalService; + + this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesEventService = KubernetesEventService; + this.KubernetesStackService = KubernetesStackService; + this.KubernetesPodService = KubernetesPodService; + + this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + + this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; + + this.onInit = this.onInit.bind(this); + this.getApplication = this.getApplication.bind(this); + this.getApplicationAsync = this.getApplicationAsync.bind(this); + this.getEvents = this.getEvents.bind(this); + this.getEventsAsync = this.getEventsAsync.bind(this); + this.updateApplicationAsync = this.updateApplicationAsync.bind(this); + this.redeployApplicationAsync = this.redeployApplicationAsync.bind(this); + this.rollbackApplicationAsync = this.rollbackApplicationAsync.bind(this); + this.copyLoadBalancerIP = this.copyLoadBalancerIP.bind(this); + } + + selectTab(index) { + this.LocalStorage.storeActiveTab('application', index); + } + + showEditor() { + this.state.showEditorTab = true; + this.selectTab(2); + } + + isSystemNamespace() { + return this.KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool); + } + + isExternalApplication() { + return KubernetesApplicationHelper.isExternalApplication(this.application); + } + + copyLoadBalancerIP() { + this.clipboard.copyText(this.application.LoadBalancerIPAddress); + $('#copyNotificationLB').show().fadeOut(2500); + } + + copyApplicationName() { + this.clipboard.copyText(this.application.Name); + $('#copyNotificationApplicationName').show().fadeOut(2500); + } + + hasPersistedFolders() { + return this.application && this.application.PersistedFolders.length; + } + + hasVolumeConfiguration() { + return this.application && this.application.ConfigurationVolumes.length; + } + + hasEventWarnings() { + return this.state.eventWarningCount; + } + + /** + * ROLLBACK + */ + + async rollbackApplicationAsync() { + try { + // await this.KubernetesApplicationService.rollback(this.application, this.formValues.SelectedRevision); + const revision = _.nth(this.application.Revisions, -2); + await this.KubernetesApplicationService.rollback(this.application, revision); + this.Notifications.success('Application successfully rolled back'); + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to rollback the application'); + } + } + + rollbackApplication() { + this.ModalService.confirmUpdate('Rolling back the application to a previous configuration may cause a service interruption. Do you wish to continue?', (confirmed) => { + if (confirmed) { + return this.$async(this.rollbackApplicationAsync); + } + }); + } + /** + * REDEPLOY + */ + async redeployApplicationAsync() { + try { + const promises = _.map(this.application.Pods, (item) => this.KubernetesPodService.delete(item)); + await Promise.all(promises); + this.Notifications.success('Application successfully redeployed'); + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to redeploy the application'); + } + } + + redeployApplication() { + this.ModalService.confirmUpdate('Redeploying the application may cause a service interruption. Do you wish to continue?', (confirmed) => { + if (confirmed) { + return this.$async(this.redeployApplicationAsync); + } + }); + } + + /** + * UPDATE + */ + async updateApplicationAsync() { + try { + const application = angular.copy(this.application); + application.Note = this.formValues.Note; + await this.KubernetesApplicationService.patch(this.application, application, true); + this.Notifications.success('Application successfully updated'); + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to update application'); + } + } + + updateApplication() { + return this.$async(this.updateApplicationAsync); + } + + /** + * EVENTS + */ + async getEventsAsync() { + try { + this.state.eventsLoading = true; + const events = await this.KubernetesEventService.get(this.state.params.namespace); + this.events = _.filter( + events, + (event) => + event.Involved.uid === this.application.Id || + event.Involved.uid === this.application.ServiceId || + _.find(this.application.Pods, (pod) => pod.Id === event.Involved.uid) !== undefined + ); + this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve application related events'); + } finally { + this.state.eventsLoading = false; + } + } + + getEvents() { + return this.$async(this.getEventsAsync); + } + + /** + * APPLICATION + */ + async getApplicationAsync() { + try { + this.state.dataLoading = true; + this.application = await this.KubernetesApplicationService.get(this.state.params.namespace, this.state.params.name); + this.formValues.Note = this.application.Note; + if (this.application.Note) { + this.state.expandedNote = true; + } + if (this.application.CurrentRevision) { + this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision }); + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve application details'); + } finally { + this.state.dataLoading = false; + } + } + + getApplication() { + return this.$async(this.getApplicationAsync); + } + + async onInit() { + this.state = { + activeTab: 0, + currentName: this.$state.$current.name, + showEditorTab: false, + DisplayedPanel: 'pods', + eventsLoading: true, + dataLoading: true, + viewReady: false, + params: { + namespace: this.$transition$.params().namespace, + name: this.$transition$.params().name, + }, + eventWarningCount: 0, + expandedNote: false, + }; + + this.state.activeTab = this.LocalStorage.getActiveTab('application'); + + this.formValues = { + Note: '', + SelectedRevision: undefined, + }; + + this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; + await this.getApplication(); + await this.getEvents(); + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } + + $onDestroy() { + if (this.state.currentName !== this.$state.$current.name) { + this.LocalStorage.storeActiveTab('application', 0); + } + } +} + +export default KubernetesApplicationController; +angular.module('portainer.kubernetes').controller('KubernetesApplicationController', KubernetesApplicationController); diff --git a/app/kubernetes/views/applications/logs/logs.html b/app/kubernetes/views/applications/logs/logs.html new file mode 100644 index 000000000..eaefc6417 --- /dev/null +++ b/app/kubernetes/views/applications/logs/logs.html @@ -0,0 +1,62 @@ + + Resource pools > + {{ ctrl.application.ResourcePool }} > + Applications > + {{ ctrl.application.Name }} > Pods > + {{ ctrl.podName }} > Logs + + + + +
+
+
+ + +
+
+ Actions +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+

{{ line }}

No log line matching the '{{ ctrl.state.search }}' filter

No logs available

+
+
+
diff --git a/app/kubernetes/views/applications/logs/logs.js b/app/kubernetes/views/applications/logs/logs.js new file mode 100644 index 000000000..513bf9285 --- /dev/null +++ b/app/kubernetes/views/applications/logs/logs.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationLogsView', { + templateUrl: './logs.html', + controller: 'KubernetesApplicationLogsController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/applications/logs/logsController.js b/app/kubernetes/views/applications/logs/logsController.js new file mode 100644 index 000000000..a7d761dd5 --- /dev/null +++ b/app/kubernetes/views/applications/logs/logsController.js @@ -0,0 +1,87 @@ +import angular from 'angular'; + +class KubernetesApplicationLogsController { + /* @ngInject */ + constructor($async, $state, $interval, Notifications, KubernetesApplicationService, KubernetesPodService) { + this.$async = $async; + this.$state = $state; + this.$interval = $interval; + this.Notifications = Notifications; + this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesPodService = KubernetesPodService; + + this.onInit = this.onInit.bind(this); + this.stopRepeater = this.stopRepeater.bind(this); + this.getApplicationLogsAsync = this.getApplicationLogsAsync.bind(this); + } + + updateAutoRefresh() { + if (this.state.autoRefresh) { + this.setUpdateRepeater(); + return; + } + + this.stopRepeater(); + } + + stopRepeater() { + if (angular.isDefined(this.repeater)) { + this.$interval.cancel(this.repeater); + this.repeater = null; + } + } + + setUpdateRepeater() { + this.repeater = this.$interval(this.getApplicationLogsAsync, this.state.refreshRate); + } + + async getApplicationLogsAsync() { + try { + this.applicationLogs = await this.KubernetesPodService.logs(this.application.ResourcePool, this.podName); + } catch (err) { + this.stopRepeater(); + this.Notifications.error('Failure', err, 'Unable to retrieve application logs'); + } + } + + async onInit() { + this.state = { + autoRefresh: false, + refreshRate: 5000, // 5 seconds + search: '', + viewReady: false, + }; + + const podName = this.$transition$.params().pod; + const applicationName = this.$transition$.params().name; + const namespace = this.$transition$.params().namespace; + + this.applicationLogs = []; + this.podName = podName; + + try { + const [application, applicationLogs] = await Promise.all([ + this.KubernetesApplicationService.get(namespace, applicationName), + this.KubernetesPodService.logs(namespace, podName), + ]); + + this.application = application; + this.applicationLogs = applicationLogs; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve application logs'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } + + $onDestroy() { + this.stopRepeater(); + } +} + +export default KubernetesApplicationLogsController; +angular.module('portainer.kubernetes').controller('KubernetesApplicationLogsController', KubernetesApplicationLogsController); diff --git a/app/kubernetes/views/cluster/cluster.html b/app/kubernetes/views/cluster/cluster.html new file mode 100644 index 000000000..eeb43f9da --- /dev/null +++ b/app/kubernetes/views/cluster/cluster.html @@ -0,0 +1,40 @@ + + Cluster information + + + + +
+
+
+ + +
+ + +
+
+
+
+
+ +
+
+ +
+
+
diff --git a/app/kubernetes/views/cluster/cluster.js b/app/kubernetes/views/cluster/cluster.js new file mode 100644 index 000000000..688cf877e --- /dev/null +++ b/app/kubernetes/views/cluster/cluster.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesClusterView', { + templateUrl: './cluster.html', + controller: 'KubernetesClusterController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js new file mode 100644 index 000000000..2a8313a87 --- /dev/null +++ b/app/kubernetes/views/cluster/clusterController.js @@ -0,0 +1,88 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models'; + +class KubernetesClusterController { + /* @ngInject */ + constructor($async, Authentication, Notifications, KubernetesNodeService, KubernetesApplicationService) { + this.$async = $async; + this.Authentication = Authentication; + this.Notifications = Notifications; + this.KubernetesNodeService = KubernetesNodeService; + this.KubernetesApplicationService = KubernetesApplicationService; + + this.onInit = this.onInit.bind(this); + this.getNodes = this.getNodes.bind(this); + this.getNodesAsync = this.getNodesAsync.bind(this); + this.getApplicationsAsync = this.getApplicationsAsync.bind(this); + } + + async getNodesAsync() { + try { + const nodes = await this.KubernetesNodeService.get(); + _.forEach(nodes, (node) => (node.Memory = filesizeParser(node.Memory))); + this.nodes = nodes; + this.CPULimit = _.reduce(this.nodes, (acc, node) => node.CPU + acc, 0); + this.MemoryLimit = _.reduce(this.nodes, (acc, node) => KubernetesResourceReservationHelper.megaBytesValue(node.Memory) + acc, 0); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve nodes'); + } + } + + getNodes() { + return this.$async(this.getNodesAsync); + } + + async getApplicationsAsync() { + try { + this.state.applicationsLoading = true; + this.applications = await this.KubernetesApplicationService.get(); + const nodeNames = _.map(this.nodes, (node) => node.Name); + this.resourceReservation = _.reduce( + this.applications, + (acc, app) => { + app.Pods = _.filter(app.Pods, (pod) => nodeNames.includes(pod.Node)); + const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods); + acc.CPU += resourceReservation.CPU; + acc.Memory += resourceReservation.Memory; + return acc; + }, + new KubernetesResourceReservation() + ); + this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory); + } catch (err) { + this.Notifications.error('Failure', 'Unable to retrieve applications', err); + } finally { + this.state.applicationsLoading = false; + } + } + + getApplications() { + return this.$async(this.getApplicationsAsync); + } + + async onInit() { + this.state = { + applicationsLoading: true, + viewReady: false, + }; + + this.isAdmin = this.Authentication.isAdmin(); + + await this.getNodes(); + if (this.isAdmin) { + await this.getApplications(); + } + + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesClusterController; +angular.module('portainer.kubernetes').controller('KubernetesClusterController', KubernetesClusterController); diff --git a/app/kubernetes/views/cluster/node/node.html b/app/kubernetes/views/cluster/node/node.html new file mode 100644 index 000000000..6fabaaa77 --- /dev/null +++ b/app/kubernetes/views/cluster/node/node.html @@ -0,0 +1,107 @@ + + Cluster > {{ ctrl.node.Name }} + + + + +
+
+
+ + + + + Node + +
+ + + + + + + + + + + + + + + + + + + + + + + +
Hostname{{ ctrl.node.Name }}
Role{{ ctrl.node.Role }}
Kubelet version{{ ctrl.node.Version }}
Creation date{{ ctrl.node.CreationDate | getisodate }}
Status + + {{ ctrl.node.Status }} + + + {{ ctrl.node.Conditions | kubernetesNodeConditionsMessage }} + +
+
+ + +
+
+
+ + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ + +
+ + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/cluster/node/node.js b/app/kubernetes/views/cluster/node/node.js new file mode 100644 index 000000000..38fb14ede --- /dev/null +++ b/app/kubernetes/views/cluster/node/node.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesNodeView', { + templateUrl: './node.html', + controller: 'KubernetesNodeController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/cluster/node/nodeController.js b/app/kubernetes/views/cluster/node/nodeController.js new file mode 100644 index 000000000..eb4bb7327 --- /dev/null +++ b/app/kubernetes/views/cluster/node/nodeController.js @@ -0,0 +1,137 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import { KubernetesResourceReservation } from 'Kubernetes/models/resource-reservation/models'; +import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; + +class KubernetesNodeController { + /* @ngInject */ + constructor($async, $state, Notifications, LocalStorage, KubernetesNodeService, KubernetesEventService, KubernetesPodService, KubernetesApplicationService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.LocalStorage = LocalStorage; + this.KubernetesNodeService = KubernetesNodeService; + this.KubernetesEventService = KubernetesEventService; + this.KubernetesPodService = KubernetesPodService; + this.KubernetesApplicationService = KubernetesApplicationService; + + this.onInit = this.onInit.bind(this); + this.getNodeAsync = this.getNodeAsync.bind(this); + this.getEvents = this.getEvents.bind(this); + this.getEventsAsync = this.getEventsAsync.bind(this); + this.getApplicationsAsync = this.getApplicationsAsync.bind(this); + } + + selectTab(index) { + this.LocalStorage.storeActiveTab('node', index); + } + + async getNodeAsync() { + try { + this.state.dataLoading = true; + const nodeName = this.$transition$.params().name; + this.node = await this.KubernetesNodeService.get(nodeName); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve node'); + } finally { + this.state.dataLoading = false; + } + } + + getNode() { + return this.$async(this.getNodeAsync); + } + + hasEventWarnings() { + return this.state.eventWarningCount; + } + + async getEventsAsync() { + try { + this.state.eventsLoading = true; + this.events = await this.KubernetesEventService.get(); + this.events = _.filter(this.events.items, (item) => item.involvedObject.kind === 'Node'); + this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve node events'); + } finally { + this.state.eventsLoading = false; + } + } + + getEvents() { + return this.$async(this.getEventsAsync); + } + + showEditor() { + this.state.showEditorTab = true; + this.selectTab(2); + } + + async getApplicationsAsync() { + try { + this.state.applicationsLoading = true; + this.applications = await this.KubernetesApplicationService.get(); + + this.resourceReservation = new KubernetesResourceReservation(); + this.applications = _.map(this.applications, (app) => { + app.Pods = _.filter(app.Pods, (pod) => pod.Node === this.node.Name); + return app; + }); + this.applications = _.filter(this.applications, (app) => app.Pods.length !== 0); + this.applications = _.map(this.applications, (app) => { + const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods); + app.CPU = resourceReservation.CPU; + app.Memory = resourceReservation.Memory; + this.resourceReservation.CPU += resourceReservation.CPU; + this.resourceReservation.Memory += resourceReservation.Memory; + return app; + }); + this.resourceReservation.Memory = KubernetesResourceReservationHelper.megaBytesValue(this.resourceReservation.Memory); + this.memoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.node.Memory); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve applications'); + } finally { + this.state.applicationsLoading = false; + } + } + + getApplications() { + return this.$async(this.getApplicationsAsync); + } + + async onInit() { + this.state = { + activeTab: 0, + currentName: this.$state.$current.name, + dataLoading: true, + eventsLoading: true, + applicationsLoading: true, + showEditorTab: false, + viewReady: false, + eventWarningCount: 0, + }; + + this.state.activeTab = this.LocalStorage.getActiveTab('node'); + + await this.getNode(); + await this.getEvents(); + await this.getApplications(); + + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } + + $onDestroy() { + if (this.state.currentName !== this.$state.$current.name) { + this.LocalStorage.storeActiveTab('node', 0); + } + } +} + +export default KubernetesNodeController; +angular.module('portainer.kubernetes').controller('KubernetesNodeController', KubernetesNodeController); diff --git a/app/kubernetes/views/configurations/configurations.html b/app/kubernetes/views/configurations/configurations.html new file mode 100644 index 000000000..8e55b4fc8 --- /dev/null +++ b/app/kubernetes/views/configurations/configurations.html @@ -0,0 +1,19 @@ + + Configurations + + + + +
+
+
+ +
+
+
diff --git a/app/kubernetes/views/configurations/configurations.js b/app/kubernetes/views/configurations/configurations.js new file mode 100644 index 000000000..98538965b --- /dev/null +++ b/app/kubernetes/views/configurations/configurations.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesConfigurationsView', { + templateUrl: './configurations.html', + controller: 'KubernetesConfigurationsController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/configurations/configurationsController.js b/app/kubernetes/views/configurations/configurationsController.js new file mode 100644 index 000000000..eb94722fe --- /dev/null +++ b/app/kubernetes/views/configurations/configurationsController.js @@ -0,0 +1,105 @@ +import angular from 'angular'; +import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; + +class KubernetesConfigurationsController { + /* @ngInject */ + constructor($async, $state, Notifications, KubernetesConfigurationService, KubernetesApplicationService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.KubernetesConfigurationService = KubernetesConfigurationService; + this.KubernetesApplicationService = KubernetesApplicationService; + + this.onInit = this.onInit.bind(this); + this.getConfigurations = this.getConfigurations.bind(this); + this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this); + this.getApplicationsAsync = this.getApplicationsAsync.bind(this); + this.removeAction = this.removeAction.bind(this); + this.removeActionAsync = this.removeActionAsync.bind(this); + this.refreshCallback = this.refreshCallback.bind(this); + this.refreshCallbackAsync = this.refreshCallbackAsync.bind(this); + } + + async getConfigurationsAsync() { + try { + this.state.configurationsLoading = true; + this.configurations = await this.KubernetesConfigurationService.get(); + KubernetesConfigurationHelper.setConfigurationsUsed(this.configurations, this.applications); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve configurations'); + } finally { + this.state.configurationsLoading = false; + } + } + + getConfigurations() { + return this.$async(this.getConfigurationsAsync); + } + + async removeActionAsync(selectedItems) { + let actionCount = selectedItems.length; + for (const configuration of selectedItems) { + try { + await this.KubernetesConfigurationService.delete(configuration); + this.Notifications.success('Configurations successfully removed', configuration.Name); + const index = this.configurations.indexOf(configuration); + this.configurations.splice(index, 1); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to remove configuration'); + } finally { + --actionCount; + if (actionCount === 0) { + this.$state.reload(); + } + } + } + } + + removeAction(selectedItems) { + return this.$async(this.removeActionAsync, selectedItems); + } + + async getApplicationsAsync() { + try { + this.state.applicationsLoading = true; + this.applications = await this.KubernetesApplicationService.get(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve applications'); + } finally { + this.state.applicationsLoading = false; + } + } + + getApplications() { + return this.$async(this.getApplicationsAsync); + } + + async refreshCallbackAsync() { + await this.getConfigurations(); + await this.getApplications(); + } + + refreshCallback() { + return this.$async(this.refreshCallbackAsync); + } + + async onInit() { + this.state = { + configurationsLoading: true, + applicationsLoading: true, + viewReady: false, + }; + + await this.getApplications(); + await this.getConfigurations(); + + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesConfigurationsController; +angular.module('portainer.kubernetes').controller('KubernetesConfigurationsController', KubernetesConfigurationsController); diff --git a/app/kubernetes/views/configurations/create/createConfiguration.html b/app/kubernetes/views/configurations/create/createConfiguration.html new file mode 100644 index 000000000..446f7a334 --- /dev/null +++ b/app/kubernetes/views/configurations/create/createConfiguration.html @@ -0,0 +1,135 @@ + + Configurations > Create a configuration + + + + +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+

This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end + with an alphanumeric character.

+
+

A configuration with the same name already exists inside the selected resource pool.

+
+
+ + +
+ Resource pool +
+ + +
+ +
+ +
+
+
+
+ + This resource pool has exhausted its resource capacity and you will not be able to deploy the configuration. Contact your administrator to expand the capacity of + the resource pool. +
+
+ + +
+ Configuration type +
+ +
+
+ Select the type of data that you want to save in the configuration. +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + + + + +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
+
diff --git a/app/kubernetes/views/configurations/create/createConfiguration.js b/app/kubernetes/views/configurations/create/createConfiguration.js new file mode 100644 index 000000000..515c39a81 --- /dev/null +++ b/app/kubernetes/views/configurations/create/createConfiguration.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesCreateConfigurationView', { + templateUrl: './createConfiguration.html', + controller: 'KubernetesCreateConfigurationController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/configurations/create/createConfigurationController.js b/app/kubernetes/views/configurations/create/createConfigurationController.js new file mode 100644 index 000000000..696a2efc7 --- /dev/null +++ b/app/kubernetes/views/configurations/create/createConfigurationController.js @@ -0,0 +1,93 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues'; +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; + +class KubernetesCreateConfigurationController { + /* @ngInject */ + constructor($async, $state, Notifications, Authentication, KubernetesConfigurationService, KubernetesResourcePoolService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.Authentication = Authentication; + this.KubernetesConfigurationService = KubernetesConfigurationService; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesConfigurationTypes = KubernetesConfigurationTypes; + + this.onInit = this.onInit.bind(this); + this.createConfigurationAsync = this.createConfigurationAsync.bind(this); + this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this); + } + + onChangeName() { + const filteredConfigurations = _.filter(this.configurations, (config) => config.Namespace === this.formValues.ResourcePool.Namespace.Name); + this.state.alreadyExist = _.find(filteredConfigurations, (config) => config.Name === this.formValues.Name) !== undefined; + } + + isFormValid() { + const uniqueCheck = !this.state.alreadyExist && this.state.isDataValid; + if (this.formValues.IsSimple) { + return this.formValues.Data.length > 0 && uniqueCheck; + } + return uniqueCheck; + } + + async createConfigurationAsync() { + try { + this.state.actionInProgress = true; + this.formValues.ConfigurationOwner = this.Authentication.getUserDetails().username; + await this.KubernetesConfigurationService.create(this.formValues); + this.Notifications.success('Configuration succesfully created'); + this.$state.go('kubernetes.configurations'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to create configuration'); + } finally { + this.state.actionInProgress = false; + } + } + + createConfiguration() { + return this.$async(this.createConfigurationAsync); + } + + async getConfigurationsAsync() { + try { + this.configurations = await this.KubernetesConfigurationService.get(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve configurations'); + } + } + + getConfigurations() { + return this.$async(this.getConfigurationsAsync); + } + + async onInit() { + this.state = { + actionInProgress: false, + viewReady: false, + alreadyExist: false, + isDataValid: true, + }; + + this.formValues = new KubernetesConfigurationFormValues(); + this.formValues.Data.push(new KubernetesConfigurationFormValuesDataEntry()); + + try { + this.resourcePools = await this.KubernetesResourcePoolService.get(); + this.formValues.ResourcePool = this.resourcePools[0]; + await this.getConfigurations(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesCreateConfigurationController; +angular.module('portainer.kubernetes').controller('KubernetesCreateConfigurationController', KubernetesCreateConfigurationController); diff --git a/app/kubernetes/views/configurations/edit/configuration.html b/app/kubernetes/views/configurations/edit/configuration.html new file mode 100644 index 000000000..3d97546ea --- /dev/null +++ b/app/kubernetes/views/configurations/edit/configuration.html @@ -0,0 +1,120 @@ + + Resource pools > + {{ ctrl.configuration.Namespace }} > + Configurations > {{ ctrl.configuration.Name }} + + + + +
+
+
+ + + + + Configuration +
+ + + + + + + + + + + + + + + +
Name + {{ ctrl.configuration.Name }} +
Resource Pool + {{ ctrl.configuration.Namespace }} + system +
Configuration type + {{ ctrl.configuration.Type | kubernetesConfigurationTypeText }} +
+
+
+ + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ + +
+ + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+ + + +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/configurations/edit/configuration.js b/app/kubernetes/views/configurations/edit/configuration.js new file mode 100644 index 000000000..3fbee48b8 --- /dev/null +++ b/app/kubernetes/views/configurations/edit/configuration.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesConfigurationView', { + templateUrl: './configuration.html', + controller: 'KubernetesConfigurationController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/configurations/edit/configurationController.js b/app/kubernetes/views/configurations/edit/configurationController.js new file mode 100644 index 000000000..3906d9f59 --- /dev/null +++ b/app/kubernetes/views/configurations/edit/configurationController.js @@ -0,0 +1,236 @@ +import angular from 'angular'; +import { KubernetesConfigurationFormValues, KubernetesConfigurationFormValuesDataEntry } from 'Kubernetes/models/configuration/formvalues'; +import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; +import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; +import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; +import _ from 'lodash-es'; + +class KubernetesConfigurationController { + /* @ngInject */ + constructor( + $async, + $state, + Notifications, + LocalStorage, + KubernetesConfigurationService, + KubernetesResourcePoolService, + ModalService, + KubernetesApplicationService, + KubernetesEventService, + KubernetesNamespaceHelper + ) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.LocalStorage = LocalStorage; + this.ModalService = ModalService; + this.KubernetesConfigurationService = KubernetesConfigurationService; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesEventService = KubernetesEventService; + this.KubernetesConfigurationTypes = KubernetesConfigurationTypes; + this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + + this.onInit = this.onInit.bind(this); + this.getConfigurationAsync = this.getConfigurationAsync.bind(this); + this.getEvents = this.getEvents.bind(this); + this.getEventsAsync = this.getEventsAsync.bind(this); + this.getApplications = this.getApplications.bind(this); + this.getApplicationsAsync = this.getApplicationsAsync.bind(this); + this.getConfigurationsAsync = this.getConfigurationsAsync.bind(this); + this.updateConfiguration = this.updateConfiguration.bind(this); + this.updateConfigurationAsync = this.updateConfigurationAsync.bind(this); + } + + isSystemNamespace() { + return this.KubernetesNamespaceHelper.isSystemNamespace(this.configuration.Namespace); + } + + selectTab(index) { + this.LocalStorage.storeActiveTab('configuration', index); + } + + showEditor() { + this.state.showEditorTab = true; + this.selectTab(2); + } + + isFormValid() { + if (this.formValues.IsSimple) { + return this.formValues.Data.length > 0 && this.state.isDataValid; + } + return this.state.isDataValid; + } + + async updateConfigurationAsync() { + try { + this.state.actionInProgress = true; + if ( + this.formValues.Type !== this.configuration.Type || + this.formValues.ResourcePool.Namespace.Name !== this.configuration.Namespace || + this.formValues.Name !== this.configuration.Name + ) { + await this.KubernetesConfigurationService.create(this.formValues); + await this.KubernetesConfigurationService.delete(this.configuration); + this.Notifications.success('Configuration succesfully updated'); + this.$state.go( + 'kubernetes.configurations.configuration', + { + namespace: this.formValues.ResourcePool.Namespace.Name, + name: this.formValues.Name, + }, + { reload: true } + ); + } else { + await this.KubernetesConfigurationService.update(this.formValues); + this.Notifications.success('Configuration succesfully updated'); + this.$state.reload(); + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to update configuration'); + } finally { + this.state.actionInProgress = false; + } + } + + updateConfiguration() { + if (this.configuration.Used) { + const plural = this.configuration.Applications.length > 1 ? 's' : ''; + this.ModalService.confirmUpdate( + `The changes will be propagated to ${this.configuration.Applications.length} running application${plural}. Are you sure you want to update this configuration?`, + (confirmed) => { + if (confirmed) { + return this.$async(this.updateConfigurationAsync); + } + } + ); + } else { + return this.$async(this.updateConfigurationAsync); + } + } + + async getConfigurationAsync() { + try { + this.state.configurationLoading = true; + const name = this.$transition$.params().name; + const namespace = this.$transition$.params().namespace; + this.configuration = await this.KubernetesConfigurationService.get(namespace, name); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve configuration'); + } finally { + this.state.configurationLoading = false; + } + } + + getConfiguration() { + return this.$async(this.getConfigurationAsync); + } + + async getApplicationsAsync(namespace) { + try { + this.state.applicationsLoading = true; + const applications = await this.KubernetesApplicationService.get(namespace); + this.configuration.Applications = KubernetesConfigurationHelper.getUsingApplications(this.configuration, applications); + KubernetesConfigurationHelper.setConfigurationUsed(this.configuration); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve applications'); + } finally { + this.state.applicationsLoading = false; + } + } + + getApplications(namespace) { + return this.$async(this.getApplicationsAsync, namespace); + } + + hasEventWarnings() { + return this.state.eventWarningCount; + } + + async getEventsAsync(namespace) { + try { + this.state.eventsLoading = true; + this.events = await this.KubernetesEventService.get(namespace); + this.events = _.filter(this.events, (event) => event.Involved.uid === this.configuration.Id); + this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events); + } catch (err) { + this.Notifications('Failure', err, 'Unable to retrieve events'); + } finally { + this.state.eventsLoading = false; + } + } + + getEvents(namespace) { + return this.$async(this.getEventsAsync, namespace); + } + + async getConfigurationsAsync() { + try { + this.configurations = await this.KubernetesConfigurationService.get(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve configurations'); + } + } + + getConfigurations() { + return this.$async(this.getConfigurationsAsync); + } + + async onInit() { + try { + this.state = { + actionInProgress: false, + configurationLoading: true, + applicationsLoading: true, + eventsLoading: true, + showEditorTab: false, + viewReady: false, + eventWarningCount: 0, + activeTab: 0, + currentName: this.$state.$current.name, + isDataValid: true, + }; + + this.state.activeTab = this.LocalStorage.getActiveTab('configuration'); + + this.formValues = new KubernetesConfigurationFormValues(); + + this.resourcePools = await this.KubernetesResourcePoolService.get(); + await this.getConfiguration(); + await this.getApplications(this.configuration.Namespace); + await this.getEvents(this.configuration.Namespace); + this.formValues.ResourcePool = _.find(this.resourcePools, (resourcePool) => resourcePool.Namespace.Name === this.configuration.Namespace); + this.formValues.Id = this.configuration.Id; + this.formValues.Name = this.configuration.Name; + this.formValues.Type = this.configuration.Type; + this.formValues.Data = _.map(this.configuration.Data, (value, key) => { + if (this.configuration.Type === KubernetesConfigurationTypes.SECRET) { + value = atob(value); + } + this.formValues.DataYaml += key + ': ' + value + '\n'; + const entry = new KubernetesConfigurationFormValuesDataEntry(); + entry.Key = key; + entry.Value = value; + return entry; + }); + await this.getConfigurations(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } + + $onDestroy() { + if (this.state.currentName !== this.$state.$current.name) { + this.LocalStorage.storeActiveTab('configuration', 0); + } + } +} + +export default KubernetesConfigurationController; +angular.module('portainer.kubernetes').controller('KubernetesConfigurationController', KubernetesConfigurationController); diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html new file mode 100644 index 000000000..697c19e6c --- /dev/null +++ b/app/kubernetes/views/configure/configure.html @@ -0,0 +1,122 @@ + + Endpoints > {{ ctrl.endpoint.Name }} > Kubernetes configuration + + + + +
+
+
+ + +
+
+ Expose applications over external IP addresses +
+
+ + Enabling this feature will allow users to expose application they deploy over an external IP address assigned by cloud provider. +

+ + Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs. +

+
+
+
+
+ + +
+
+ +
+ Available storage options +
+ +
+
+ + Unable to detect any storage class available to persist data. Users won't be able to persist application data inside this cluster. +
+
+ +
+ +

+ Select which storage options will be available for use when deploying applications. Have a look at your storage driver documentation to figure out which access + policy to configure. +

+

+ You can find more information about access modes + in the official Kubernetes documentation. +

+
+
+ +
+
+ + + + + + + + + + + +
StorageShared access policy
+
+ + {{ class.Name }} +
+
+ + +
+
+
+ + + Shared access policy configuration required + +
+
+ +
+ Actions +
+ +
+
+ +
+
+
+
+
+
+
+
diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js new file mode 100644 index 000000000..53f876166 --- /dev/null +++ b/app/kubernetes/views/configure/configureController.js @@ -0,0 +1,115 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import { KubernetesStorageClassAccessPolicies, KubernetesStorageClass } from 'Kubernetes/models/storage-class/models'; + +class KubernetesConfigureController { + /* @ngInject */ + constructor($async, $state, $stateParams, Notifications, KubernetesStorageService, EndpointService, EndpointProvider) { + this.$async = $async; + this.$state = $state; + this.$stateParams = $stateParams; + this.Notifications = Notifications; + this.KubernetesStorageService = KubernetesStorageService; + this.EndpointService = EndpointService; + this.EndpointProvider = EndpointProvider; + + this.onInit = this.onInit.bind(this); + this.configureAsync = this.configureAsync.bind(this); + } + + storageClassAvailable() { + return this.StorageClasses && this.StorageClasses.length > 0; + } + + hasValidStorageConfiguration() { + let valid = true; + _.forEach(this.StorageClasses, (item) => { + if (item.selected && item.AccessModes.length === 0) { + valid = false; + } + }); + + return valid; + } + + async configureAsync() { + try { + this.state.actionInProgress = true; + const classes = _.without( + _.map(this.StorageClasses, (item) => { + if (item.selected) { + const res = new KubernetesStorageClass(); + res.Name = item.Name; + res.AccessModes = _.map(item.AccessModes, 'Name'); + return res; + } + }), + undefined + ); + + this.endpoint.Kubernetes.Configuration.StorageClasses = classes; + this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; + await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint); + const endpoints = this.EndpointProvider.endpoints(); + const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id); + if (modifiedEndpoint) { + modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes; + modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; + this.EndpointProvider.setEndpoints(endpoints); + } + this.Notifications.success('Configuration successfully applied'); + this.$state.go('portainer.home'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to apply configuration'); + } finally { + this.state.actionInProgress = false; + } + } + + configure() { + return this.$async(this.configureAsync); + } + + async onInit() { + this.state = { + actionInProgress: false, + displayConfigureClassPanel: {}, + viewReady: false, + }; + + this.formValues = { + UseLoadBalancer: false, + }; + + try { + const endpointId = this.$stateParams.id; + [this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(endpointId), this.EndpointService.endpoint(endpointId)]); + _.forEach(this.StorageClasses, (item) => { + item.availableAccessModes = new KubernetesStorageClassAccessPolicies(); + const storage = _.find(this.endpoint.Kubernetes.Configuration.StorageClasses, (sc) => sc.Name === item.Name); + if (storage) { + item.selected = true; + _.forEach(storage.AccessModes, (access) => { + const mode = _.find(item.availableAccessModes, { Name: access }); + if (mode) { + mode.selected = true; + } + }); + } + }); + + this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve storage classes'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesConfigureController; +angular.module('portainer.kubernetes').controller('KubernetesConfigureController', KubernetesConfigureController); diff --git a/app/kubernetes/views/dashboard/dashboard.html b/app/kubernetes/views/dashboard/dashboard.html new file mode 100644 index 000000000..776479e3d --- /dev/null +++ b/app/kubernetes/views/dashboard/dashboard.html @@ -0,0 +1,99 @@ + + Endpoint summary + + + + + diff --git a/app/kubernetes/views/dashboard/dashboard.js b/app/kubernetes/views/dashboard/dashboard.js new file mode 100644 index 000000000..76b30ec2e --- /dev/null +++ b/app/kubernetes/views/dashboard/dashboard.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesDashboardView', { + templateUrl: './dashboard.html', + controller: 'KubernetesDashboardController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/dashboard/dashboardController.js b/app/kubernetes/views/dashboard/dashboardController.js new file mode 100644 index 000000000..8cc6e2cb9 --- /dev/null +++ b/app/kubernetes/views/dashboard/dashboardController.js @@ -0,0 +1,88 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import KubernetesConfigurationHelper from 'Kubernetes/helpers/configurationHelper'; + +class KubernetesDashboardController { + /* @ngInject */ + constructor( + $async, + Notifications, + EndpointService, + EndpointProvider, + KubernetesResourcePoolService, + KubernetesApplicationService, + KubernetesConfigurationService, + KubernetesVolumeService, + KubernetesNamespaceHelper, + Authentication + ) { + this.$async = $async; + this.Notifications = Notifications; + this.EndpointService = EndpointService; + this.EndpointProvider = EndpointProvider; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesConfigurationService = KubernetesConfigurationService; + this.KubernetesVolumeService = KubernetesVolumeService; + this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + this.Authentication = Authentication; + + this.onInit = this.onInit.bind(this); + this.getAll = this.getAll.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + } + + async getAllAsync() { + const isAdmin = this.Authentication.isAdmin(); + + try { + const endpointId = this.EndpointProvider.endpointID(); + const [endpoint, pools, applications, configurations, volumes] = await Promise.all([ + this.EndpointService.endpoint(endpointId), + this.KubernetesResourcePoolService.get(), + this.KubernetesApplicationService.get(), + this.KubernetesConfigurationService.get(), + this.KubernetesVolumeService.get(), + ]); + this.endpoint = endpoint; + this.applications = applications; + this.volumes = volumes; + + if (!isAdmin) { + this.pools = _.filter(pools, (pool) => { + return !this.KubernetesNamespaceHelper.isSystemNamespace(pool.Namespace.Name); + }); + + this.configurations = _.filter(configurations, (config) => { + return !KubernetesConfigurationHelper.isSystemToken(config); + }); + } else { + this.pools = pools; + this.configurations = configurations; + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load dashboard data'); + } + } + + getAll() { + return this.$async(this.getAllAsync); + } + + async onInit() { + this.state = { + viewReady: false, + }; + + await this.getAll(); + + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesDashboardController; +angular.module('portainer.kubernetes').controller('KubernetesDashboardController', KubernetesDashboardController); diff --git a/app/kubernetes/views/deploy/deploy.html b/app/kubernetes/views/deploy/deploy.html new file mode 100644 index 000000000..2f657ab54 --- /dev/null +++ b/app/kubernetes/views/deploy/deploy.html @@ -0,0 +1,118 @@ + + Deploy Kubernetes resources + + + + +
+ + +
+
+ + + + + Deploy + +
+
+ +
+ +
+
+ +
+ Deployment type +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ Web editor +
+
+ +

+ + Portainer uses Kompose to convert your Compose manifest to a Kubernetes compliant manifest. Be wary that not + all the Compose format options are supported by Kompose at the moment. +

+

+ You can get more information about Compose file format in the + official documentation. +

+
+ + You can get more information about Kubernetes file format in the + official documentation. + +
+
+
+ +
+
+ + +
+ Actions +
+
+
+ +
+
+ +
+
+ + + Logs +
+
+
+ +
+
+
+
+
+
+
+
+
+
diff --git a/app/kubernetes/views/deploy/deploy.js b/app/kubernetes/views/deploy/deploy.js new file mode 100644 index 000000000..f02365586 --- /dev/null +++ b/app/kubernetes/views/deploy/deploy.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesDeployView', { + templateUrl: './deploy.html', + controller: 'KubernetesDeployController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/deploy/deployController.js b/app/kubernetes/views/deploy/deployController.js new file mode 100644 index 000000000..a37200df9 --- /dev/null +++ b/app/kubernetes/views/deploy/deployController.js @@ -0,0 +1,99 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import stripAnsi from 'strip-ansi'; +import { KubernetesDeployManifestTypes } from 'Kubernetes/models/deploy'; + +class KubernetesDeployController { + /* @ngInject */ + constructor($async, $state, Notifications, EndpointProvider, KubernetesResourcePoolService, StackService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.EndpointProvider = EndpointProvider; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.StackService = StackService; + + this.onInit = this.onInit.bind(this); + this.deployAsync = this.deployAsync.bind(this); + this.editorUpdate = this.editorUpdate.bind(this); + this.editorUpdateAsync = this.editorUpdateAsync.bind(this); + this.getNamespacesAsync = this.getNamespacesAsync.bind(this); + } + + disableDeploy() { + return _.isEmpty(this.formValues.EditorContent) || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress; + } + + async editorUpdateAsync(cm) { + this.formValues.EditorContent = cm.getValue(); + } + + editorUpdate(cm) { + return this.$async(this.editorUpdateAsync, cm); + } + + displayErrorLog(log) { + this.errorLog = stripAnsi(log); + this.state.tabLogsDisabled = false; + this.state.activeTab = 1; + } + + async deployAsync() { + this.errorLog = ''; + this.state.actionInProgress = true; + + try { + const compose = this.state.DeployType === this.ManifestDeployTypes.COMPOSE; + await this.StackService.kubernetesDeploy(this.endpointId, this.formValues.Namespace, this.formValues.EditorContent, compose); + this.Notifications.success('Manifest successfully deployed'); + this.$state.go('kubernetes.applications'); + } catch (err) { + this.Notifications.error('Unable to deploy manifest', err, 'Unable to deploy resources'); + this.displayErrorLog(err.err.data.details); + } finally { + this.state.actionInProgress = false; + } + } + + deploy() { + return this.$async(this.deployAsync); + } + + async getNamespacesAsync() { + try { + const pools = await this.KubernetesResourcePoolService.get(); + this.namespaces = _.map(pools, 'Namespace'); + this.formValues.Namespace = this.namespaces[0].Name; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load resource pools data'); + } + } + + getNamespaces() { + return this.$async(this.getNamespacesAsync); + } + + async onInit() { + this.state = { + DeployType: KubernetesDeployManifestTypes.KUBERNETES, + tabLogsDisabled: true, + activeTab: 0, + viewReady: false, + }; + + this.formValues = {}; + this.ManifestDeployTypes = KubernetesDeployManifestTypes; + this.endpointId = this.EndpointProvider.endpointID(); + + await this.getNamespaces(); + + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesDeployController; +angular.module('portainer.kubernetes').controller('KubernetesDeployController', KubernetesDeployController); diff --git a/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html new file mode 100644 index 000000000..3044762b0 --- /dev/null +++ b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.html @@ -0,0 +1,116 @@ + + Resource pools > + {{ ctrl.pool.Namespace.Name }} > Access management + + + + +
+
+
+ + + + + + + + + + +
Name + {{ ctrl.pool.Namespace.Name }} +
+
+
+
+
+ +
+
+ + + +
+
+ +
+ + No user nor team access has been set on the endpoint. Head over to the + endpoint access view to manage them. + + + +
+
+ + + + + +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ + + +
+
+
diff --git a/app/kubernetes/views/resource-pools/access/resourcePoolAccess.js b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.js new file mode 100644 index 000000000..cda4acfbd --- /dev/null +++ b/app/kubernetes/views/resource-pools/access/resourcePoolAccess.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolAccessView', { + templateUrl: './resourcePoolAccess.html', + controller: 'KubernetesResourcePoolAccessController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/resource-pools/access/resourcePoolAccessController.js b/app/kubernetes/views/resource-pools/access/resourcePoolAccessController.js new file mode 100644 index 000000000..e6f443913 --- /dev/null +++ b/app/kubernetes/views/resource-pools/access/resourcePoolAccessController.js @@ -0,0 +1,138 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import { KubernetesPortainerConfigMapConfigName, KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapAccessKey } from 'Kubernetes/models/config-map/models'; +import { UserAccessViewModel, TeamAccessViewModel } from 'Portainer/models/access'; +import KubernetesConfigMapHelper from 'Kubernetes/helpers/configMapHelper'; + +class KubernetesResourcePoolAccessController { + /* @ngInject */ + constructor($async, $state, Notifications, KubernetesResourcePoolService, KubernetesConfigMapService, EndpointProvider, EndpointService, GroupService, AccessService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesConfigMapService = KubernetesConfigMapService; + + this.EndpointProvider = EndpointProvider; + this.EndpointService = EndpointService; + this.GroupService = GroupService; + this.AccessService = AccessService; + + this.onInit = this.onInit.bind(this); + this.authorizeAccessAsync = this.authorizeAccessAsync.bind(this); + this.unauthorizeAccessAsync = this.unauthorizeAccessAsync.bind(this); + + this.unauthorizeAccess = this.unauthorizeAccess.bind(this); + } + + initAccessConfigMap(configMap) { + configMap.Name = KubernetesPortainerConfigMapConfigName; + configMap.Namespace = KubernetesPortainerConfigMapNamespace; + configMap.Data[KubernetesPortainerConfigMapAccessKey] = {}; + return configMap; + } + + /** + * Init + */ + // TODO: refactor: roles need to be fetched if RBAC is activated on Portainer + // see porAccessManagementController for more details + // Extract the fetching code and merge it in AccessService.accesses() function + async onInit() { + this.state = { + actionInProgress: false, + viewReady: false, + }; + + this.formValues = { + multiselectOutput: [], + }; + + this.endpointId = this.EndpointProvider.endpointID(); + + try { + const name = this.$transition$.params().id; + let [endpoint, pool, configMap] = await Promise.all([ + this.EndpointService.endpoint(this.endpointId), + this.KubernetesResourcePoolService.get(name), + this.KubernetesConfigMapService.get(KubernetesPortainerConfigMapNamespace, KubernetesPortainerConfigMapConfigName), + ]); + const group = await this.GroupService.group(endpoint.GroupId); + const roles = []; + const endpointAccesses = await this.AccessService.accesses(endpoint, group, roles); + this.pool = pool; + if (configMap.Id === 0) { + configMap = this.initAccessConfigMap(configMap); + } + configMap = KubernetesConfigMapHelper.parseJSONData(configMap); + + this.authorizedUsersAndTeams = []; + this.accessConfigMap = configMap; + const poolAccesses = configMap.Data[KubernetesPortainerConfigMapAccessKey][name]; + if (poolAccesses) { + this.authorizedUsersAndTeams = _.filter(endpointAccesses.authorizedUsersAndTeams, (item) => { + if (item instanceof UserAccessViewModel && poolAccesses.UserAccessPolicies) { + return poolAccesses.UserAccessPolicies[item.Id] !== undefined; + } else if (item instanceof TeamAccessViewModel && poolAccesses.TeamAccessPolicies) { + return poolAccesses.TeamAccessPolicies[item.Id] !== undefined; + } + return false; + }); + } + this.availableUsersAndTeams = _.without(endpointAccesses.authorizedUsersAndTeams, ...this.authorizedUsersAndTeams); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve resource pool information'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } + + /** + * Authorize access + */ + async authorizeAccessAsync() { + try { + this.state.actionInProgress = true; + const newAccesses = _.concat(this.authorizedUsersAndTeams, this.formValues.multiselectOutput); + const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses); + await this.KubernetesConfigMapService.update(accessConfigMap); + this.Notifications.success('Access successfully created'); + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to create accesses'); + } + } + + authorizeAccess() { + return this.$async(this.authorizeAccessAsync); + } + + /** + * + */ + async unauthorizeAccessAsync(selectedItems) { + try { + this.state.actionInProgress = true; + const newAccesses = _.without(this.authorizedUsersAndTeams, ...selectedItems); + const accessConfigMap = KubernetesConfigMapHelper.modifiyNamespaceAccesses(angular.copy(this.accessConfigMap), this.pool.Namespace.Name, newAccesses); + await this.KubernetesConfigMapService.update(accessConfigMap); + this.Notifications.success('Access successfully removed'); + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to remove accesses'); + } finally { + this.state.actionInProgress = false; + } + } + + unauthorizeAccess(selectedItems) { + return this.$async(this.unauthorizeAccessAsync, selectedItems); + } +} + +export default KubernetesResourcePoolAccessController; +angular.module('portainer.kubernetes').controller('KubernetesResourcePoolAccessController', KubernetesResourcePoolAccessController); diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html new file mode 100644 index 000000000..052b7c888 --- /dev/null +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html @@ -0,0 +1,163 @@ + + Resource pools > Create a resource pool + + + + +
+
+
+ + +
+ +
+ +
+ +
+
+
+
+
+

This field is required.

+

This field must consist of lower case alphanumeric characters, '-' or '.', and must start and end + with an alphanumeric character.

+
+

A resource pool with the same name already exists.

+
+
+ +
+ Quota +
+ +
+
+

+ + A resource pool segments the underyling physical Kubernetes cluster into smaller virtual clusters. You should assign a capped limit of resources to this pool or + disable for the safe operation of your platform. +

+
+
+ + +
+
+
+ +

At least a single limit must be set for the quota to be valid.

+
+
+ +
+
+ Resource limits +
+
+ +
+ +
+ +
+
+ +
+
+

+ Maximum memory usage (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.defaults.MemoryLimit }} and {{ ctrl.state.sliderMaxMemory }}

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ +
+
+ +
+ Actions +
+
+
+ +
+
+ +
+
+
+
+
+
diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.js b/app/kubernetes/views/resource-pools/create/createResourcePool.js new file mode 100644 index 000000000..daf67bd9c --- /dev/null +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesCreateResourcePoolView', { + templateUrl: './createResourcePool.html', + controller: 'KubernetesCreateResourcePoolController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js new file mode 100644 index 000000000..e614cc878 --- /dev/null +++ b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js @@ -0,0 +1,127 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; +import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; + +class KubernetesCreateResourcePoolController { + /* @ngInject */ + constructor($async, $state, Notifications, KubernetesNodeService, KubernetesResourcePoolService, Authentication) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.Authentication = Authentication; + + this.KubernetesNodeService = KubernetesNodeService; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + + this.onInit = this.onInit.bind(this); + this.createResourcePoolAsync = this.createResourcePoolAsync.bind(this); + this.getResourcePoolsAsync = this.getResourcePoolsAsync.bind(this); + } + + isValid() { + return !this.state.isAlreadyExist; + } + + onChangeName() { + this.state.isAlreadyExist = _.find(this.resourcePools, (resourcePool) => resourcePool.Namespace.Name === this.formValues.Name) !== undefined; + } + + isQuotaValid() { + if ( + this.state.sliderMaxCpu < this.formValues.CpuLimit || + this.state.sliderMaxMemory < this.formValues.MemoryLimit || + (this.formValues.CpuLimit === 0 && this.formValues.MemoryLimit === 0) + ) { + return false; + } + return true; + } + + checkDefaults() { + if (this.formValues.CpuLimit < this.defaults.CpuLimit) { + this.formValues.CpuLimit = this.defaults.CpuLimit; + } + if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit)) { + this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit); + } + } + + async createResourcePoolAsync() { + this.state.actionInProgress = true; + try { + this.checkDefaults(); + const owner = this.Authentication.getUserDetails().username; + await this.KubernetesResourcePoolService.create( + this.formValues.Name, + owner, + this.formValues.hasQuota, + this.formValues.CpuLimit, + KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit) + ); + this.Notifications.success('Resource pool successfully created', this.formValues.Name); + this.$state.go('kubernetes.resourcePools'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to create resource pool'); + } finally { + this.state.actionInProgress = false; + } + } + + createResourcePool() { + return this.$async(this.createResourcePoolAsync); + } + + async getResourcePoolsAsync() { + try { + this.resourcePools = await this.KubernetesResourcePoolService.get(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve resource pools'); + } + } + + getResourcePools() { + return this.$async(this.getResourcePoolsAsync); + } + + async onInit() { + try { + this.defaults = KubernetesResourceQuotaDefaults; + + this.formValues = { + MemoryLimit: this.defaults.MemoryLimit, + CpuLimit: this.defaults.CpuLimit, + hasQuota: true, + }; + + this.state = { + actionInProgress: false, + sliderMaxMemory: 0, + sliderMaxCpu: 0, + viewReady: false, + isAlreadyExist: false, + }; + + const nodes = await this.KubernetesNodeService.get(); + + _.forEach(nodes, (item) => { + this.state.sliderMaxMemory += filesizeParser(item.Memory); + this.state.sliderMaxCpu += item.CPU; + }); + this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory); + await this.getResourcePools(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesCreateResourcePoolController; +angular.module('portainer.kubernetes').controller('KubernetesCreateResourcePoolController', KubernetesCreateResourcePoolController); diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html new file mode 100644 index 000000000..c27b9d162 --- /dev/null +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -0,0 +1,189 @@ + + Resource pools > {{ ctrl.pool.Namespace.Name }} + + + + +
+
+
+ + + + + Resource pool +
+ +
+ +
+ +
+
+ +
Quota
+ +
+
+ + +
+
+
+ +

At least a single limit must be set for the quota to be valid.

+
+
+ +
+
+ Resource limits +
+
+ +
+ +
+ +
+
+ +
+
+

+ Memory limit (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.defaults.MemoryLimit }} and + {{ ctrl.state.sliderMaxMemory }}

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ +
+
+
+ + +
+ +
+ Actions +
+
+
+ +
+
+ +
+
+ + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ +
+ + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.js b/app/kubernetes/views/resource-pools/edit/resourcePool.js new file mode 100644 index 000000000..3b6012a2d --- /dev/null +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolView', { + templateUrl: './resourcePool.html', + controller: 'KubernetesResourcePoolController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js new file mode 100644 index 000000000..790200d5c --- /dev/null +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -0,0 +1,230 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; +import { KubernetesResourceQuotaDefaults, KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; +import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; + +class KubernetesResourcePoolController { + /* @ngInject */ + constructor( + $async, + $state, + Authentication, + Notifications, + LocalStorage, + KubernetesNodeService, + KubernetesResourceQuotaService, + KubernetesResourcePoolService, + KubernetesEventService, + KubernetesPodService, + KubernetesApplicationService + ) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.Authentication = Authentication; + this.LocalStorage = LocalStorage; + + this.KubernetesNodeService = KubernetesNodeService; + this.KubernetesResourceQuotaService = KubernetesResourceQuotaService; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + this.KubernetesEventService = KubernetesEventService; + this.KubernetesPodService = KubernetesPodService; + this.KubernetesApplicationService = KubernetesApplicationService; + + this.onInit = this.onInit.bind(this); + this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this); + this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this); + this.getEvents = this.getEvents.bind(this); + this.getEventsAsync = this.getEventsAsync.bind(this); + this.getApplicationsAsync = this.getApplicationsAsync.bind(this); + } + + selectTab(index) { + this.LocalStorage.storeActiveTab('resourcePool', index); + } + + isQuotaValid() { + if ( + this.state.sliderMaxCpu < this.formValues.CpuLimit || + this.state.sliderMaxMemory < this.formValues.MemoryLimit || + (this.formValues.CpuLimit === 0 && this.formValues.MemoryLimit === 0) + ) { + return false; + } + return true; + } + + checkDefaults() { + if (this.formValues.CpuLimit < this.defaults.CpuLimit) { + this.formValues.CpuLimit = this.defaults.CpuLimit; + } + if (this.formValues.MemoryLimit < KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit)) { + this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(this.defaults.MemoryLimit); + } + } + + showEditor() { + this.state.showEditorTab = true; + this.selectTab(2); + } + + async createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit) { + const quota = new KubernetesResourceQuota(namespace); + quota.CpuLimit = cpuLimit; + quota.MemoryLimit = memoryLimit; + quota.ResourcePoolName = namespace; + quota.ResourcePoolOwner = owner; + await this.KubernetesResourceQuotaService.create(quota); + } + + async updateResourcePoolAsync() { + this.state.actionInProgress = true; + try { + this.checkDefaults(); + const namespace = this.pool.Namespace.Name; + const cpuLimit = this.formValues.CpuLimit; + const memoryLimit = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit); + const owner = this.pool.Namespace.ResourcePoolOwner; + const quota = this.pool.Quota; + + if (this.formValues.hasQuota) { + if (quota) { + quota.CpuLimit = cpuLimit; + quota.MemoryLimit = memoryLimit; + await this.KubernetesResourceQuotaService.update(quota); + } else { + await this.createResourceQuotaAsync(namespace, owner, cpuLimit, memoryLimit); + } + } else if (quota) { + await this.KubernetesResourceQuotaService.delete(quota); + } + this.Notifications.success('Resource pool successfully updated', this.pool.Namespace.Name); + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to create resource pool'); + } finally { + this.state.actionInProgress = false; + } + } + + updateResourcePool() { + return this.$async(this.updateResourcePoolAsync); + } + + hasEventWarnings() { + return this.state.eventWarningCount; + } + + async getEventsAsync() { + try { + this.state.eventsLoading = true; + this.events = await this.KubernetesEventService.get(this.pool.Namespace.Name); + this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve resource pool related events'); + } finally { + this.state.eventsLoading = false; + } + } + + getEvents() { + return this.$async(this.getEventsAsync); + } + + async getApplicationsAsync() { + try { + this.state.applicationsLoading = true; + this.applications = await this.KubernetesApplicationService.get(this.pool.Namespace.Name); + this.applications = _.map(this.applications, (app) => { + const resourceReservation = KubernetesResourceReservationHelper.computeResourceReservation(app.Pods); + app.CPU = resourceReservation.CPU; + app.Memory = resourceReservation.Memory; + return app; + }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve applications.'); + } finally { + this.state.applicationsLoading = false; + } + } + + getApplications() { + return this.$async(this.getApplicationsAsync); + } + + async onInit() { + try { + this.isAdmin = this.Authentication.isAdmin(); + this.defaults = KubernetesResourceQuotaDefaults; + + this.formValues = { + MemoryLimit: this.defaults.MemoryLimit, + CpuLimit: this.defaults.CpuLimit, + hasQuota: false, + }; + + this.state = { + actionInProgress: false, + sliderMaxMemory: 0, + sliderMaxCpu: 0, + cpuUsage: 0, + cpuUsed: 0, + memoryUsage: 0, + memoryUsed: 0, + activeTab: 0, + currentName: this.$state.$current.name, + showEditorTab: false, + eventsLoading: true, + applicationsLoading: true, + viewReady: false, + eventWarningCount: 0, + }; + + this.state.activeTab = this.LocalStorage.getActiveTab('resourcePool'); + + const name = this.$transition$.params().id; + + const [nodes, pool] = await Promise.all([this.KubernetesNodeService.get(), this.KubernetesResourcePoolService.get(name)]); + + this.pool = pool; + + _.forEach(nodes, (item) => { + this.state.sliderMaxMemory += filesizeParser(item.Memory); + this.state.sliderMaxCpu += item.CPU; + }); + this.state.sliderMaxMemory = KubernetesResourceReservationHelper.megaBytesValue(this.state.sliderMaxMemory); + + const quota = pool.Quota; + if (quota) { + this.formValues.hasQuota = true; + this.formValues.CpuLimit = quota.CpuLimit; + this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit); + + this.state.cpuUsed = quota.CpuLimitUsed; + this.state.memoryUsed = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimitUsed); + } + + await this.getEvents(); + await this.getApplications(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } + + $onDestroy() { + if (this.state.currentName !== this.$state.$current.name) { + this.LocalStorage.storeActiveTab('resourcePool', 0); + } + } +} + +export default KubernetesResourcePoolController; +angular.module('portainer.kubernetes').controller('KubernetesResourcePoolController', KubernetesResourcePoolController); diff --git a/app/kubernetes/views/resource-pools/resourcePools.html b/app/kubernetes/views/resource-pools/resourcePools.html new file mode 100644 index 000000000..1c6dce94d --- /dev/null +++ b/app/kubernetes/views/resource-pools/resourcePools.html @@ -0,0 +1,19 @@ + + Resource pools + + + + +
+
+
+ +
+
+
diff --git a/app/kubernetes/views/resource-pools/resourcePools.js b/app/kubernetes/views/resource-pools/resourcePools.js new file mode 100644 index 000000000..030e1e6eb --- /dev/null +++ b/app/kubernetes/views/resource-pools/resourcePools.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView', { + templateUrl: './resourcePools.html', + controller: 'KubernetesResourcePoolsController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/resource-pools/resourcePoolsController.js b/app/kubernetes/views/resource-pools/resourcePoolsController.js new file mode 100644 index 000000000..0f3de973a --- /dev/null +++ b/app/kubernetes/views/resource-pools/resourcePoolsController.js @@ -0,0 +1,77 @@ +import angular from 'angular'; + +class KubernetesResourcePoolsController { + /* @ngInject */ + constructor($async, $state, Notifications, ModalService, KubernetesResourcePoolService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.ModalService = ModalService; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; + + this.onInit = this.onInit.bind(this); + this.getResourcePools = this.getResourcePools.bind(this); + this.getResourcePoolsAsync = this.getResourcePoolsAsync.bind(this); + this.removeAction = this.removeAction.bind(this); + this.removeActionAsync = this.removeActionAsync.bind(this); + } + + async removeActionAsync(selectedItems) { + let actionCount = selectedItems.length; + for (const pool of selectedItems) { + try { + await this.KubernetesResourcePoolService.delete(pool); + this.Notifications.success('Resource pool successfully removed', pool.Namespace.Name); + const index = this.resourcePools.indexOf(pool); + this.resourcePools.splice(index, 1); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to remove resource pool'); + } finally { + --actionCount; + if (actionCount === 0) { + this.$state.reload(); + } + } + } + } + + removeAction(selectedItems) { + this.ModalService.confirmDeletion( + 'Do you want to remove the selected resource pool(s)? All the resources associated to the selected resource pool(s) will be removed too.', + (confirmed) => { + if (confirmed) { + return this.$async(this.removeActionAsync, selectedItems); + } + } + ); + } + + async getResourcePoolsAsync() { + try { + this.resourcePools = await this.KubernetesResourcePoolService.get(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retreive resource pools'); + } + } + + getResourcePools() { + return this.$async(this.getResourcePoolsAsync); + } + + async onInit() { + this.state = { + viewReady: false, + }; + + await this.getResourcePools(); + + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesResourcePoolsController; +angular.module('portainer.kubernetes').controller('KubernetesResourcePoolsController', KubernetesResourcePoolsController); diff --git a/app/kubernetes/views/stacks/logs/logs.html b/app/kubernetes/views/stacks/logs/logs.html new file mode 100644 index 000000000..cab652fb9 --- /dev/null +++ b/app/kubernetes/views/stacks/logs/logs.html @@ -0,0 +1,60 @@ + + Resource pools + > {{ ctrl.state.transition.namespace }} > + Applications > Stacks > {{ ctrl.state.transition.name }} > Logs + + + + +
+
+
+ + +
+
+ Actions +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+

{{ line.AppName }} {{ line.Line }}

No log line matching the '{{ ctrl.state.search }}' filter

No logs available

+
+
+
diff --git a/app/kubernetes/views/stacks/logs/logs.js b/app/kubernetes/views/stacks/logs/logs.js new file mode 100644 index 000000000..bd8a1dcf1 --- /dev/null +++ b/app/kubernetes/views/stacks/logs/logs.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesStackLogsView', { + templateUrl: './logs.html', + controller: 'KubernetesStackLogsController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/stacks/logs/logsController.js b/app/kubernetes/views/stacks/logs/logsController.js new file mode 100644 index 000000000..bcb7a2c1e --- /dev/null +++ b/app/kubernetes/views/stacks/logs/logsController.js @@ -0,0 +1,122 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import $allSettled from 'Portainer/services/allSettled'; + +const colors = ['red', 'orange', 'lime', 'green', 'darkgreen', 'cyan', 'turquoise', 'teal', 'deepskyblue', 'blue', 'darkblue', 'slateblue', 'magenta', 'darkviolet']; + +class KubernetesStackLogsController { + /* @ngInject */ + constructor($async, $state, $interval, Notifications, KubernetesApplicationService, KubernetesPodService) { + this.$async = $async; + this.$state = $state; + this.$interval = $interval; + this.Notifications = Notifications; + this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesPodService = KubernetesPodService; + + this.onInit = this.onInit.bind(this); + this.stopRepeater = this.stopRepeater.bind(this); + this.generateLogsPromise = this.generateLogsPromise.bind(this); + this.generateAppPromise = this.generateAppPromise.bind(this); + this.getStackLogsAsync = this.getStackLogsAsync.bind(this); + } + + updateAutoRefresh() { + if (this.state.autoRefresh) { + this.setUpdateRepeater(); + return; + } + + this.stopRepeater(); + } + + stopRepeater() { + if (angular.isDefined(this.repeater)) { + this.$interval.cancel(this.repeater); + this.repeater = null; + } + } + + setUpdateRepeater() { + this.repeater = this.$interval(this.getStackLogsAsync, this.state.refreshRate); + } + + async generateLogsPromise(pod) { + const res = { + Pod: pod, + Logs: [], + }; + res.Logs = await this.KubernetesPodService.logs(pod.Namespace, pod.Name); + return res; + } + + async generateAppPromise(app) { + const res = { + Application: app, + Pods: [], + }; + + const promises = _.map(app.Pods, this.generateLogsPromise); + const result = await $allSettled(promises); + res.Pods = result.fulfilled; + return res; + } + + async getStackLogsAsync() { + try { + const applications = await this.KubernetesApplicationService.get(this.state.transition.namespace); + const filteredApplications = _.filter(applications, (app) => app.StackName === this.state.transition.name); + const logsPromises = _.map(filteredApplications, this.generateAppPromise); + const data = await Promise.all(logsPromises); + const logs = _.flatMap(data, (app, index) => { + return _.flatMap(app.Pods, (pod) => { + return _.map(pod.Logs, (line) => { + const res = { + Color: colors[index % colors.length], + Line: line, + AppName: pod.Pod.Name, + }; + return res; + }); + }); + }); + this.stackLogs = logs; + } catch (err) { + this.stopRepeater(); + this.Notifications.error('Failure', err, 'Unable to retrieve application logs'); + } + } + + async onInit() { + this.state = { + autoRefresh: false, + refreshRate: 30000, // 30 seconds + search: '', + viewReady: false, + transition: { + namespace: this.$transition$.params().namespace, + name: this.$transition$.params().name, + }, + }; + + this.stackLogs = []; + try { + await this.getStackLogsAsync(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve stack logs'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } + + $onDestroy() { + this.stopRepeater(); + } +} + +export default KubernetesStackLogsController; +angular.module('portainer.kubernetes').controller('KubernetesStackLogsController', KubernetesStackLogsController); diff --git a/app/kubernetes/views/volumes/edit/volume.html b/app/kubernetes/views/volumes/edit/volume.html new file mode 100644 index 000000000..781457f1f --- /dev/null +++ b/app/kubernetes/views/volumes/edit/volume.html @@ -0,0 +1,99 @@ + + Resource pools > + {{ ctrl.volume.ResourcePool.Namespace.Name }} > + Volumes > {{ ctrl.volume.PersistentVolumeClaim.Name }} + + + + +
+
+
+ + + + + Volume + +
+ + + + + + + + + + + + + + + + + + + + + + + +
Name + {{ ctrl.volume.PersistentVolumeClaim.Name }} + external + unused +
Resource pool + {{ ctrl.volume.ResourcePool.Namespace.Name }} + system +
Storage{{ ctrl.volume.PersistentVolumeClaim.StorageClass.Name }}
Size{{ ctrl.volume.PersistentVolumeClaim.Storage }}
Creation date{{ ctrl.volume.PersistentVolumeClaim.CreationDate | getisodate }}
+
+
+ + + + Events +
+ + {{ ctrl.state.eventWarningCount }} warning(s) +
+
+ + +
+ + + YAML +
+ +
+
+
+
+
+
+
+ +
+
+ + +
+
+
diff --git a/app/kubernetes/views/volumes/edit/volume.js b/app/kubernetes/views/volumes/edit/volume.js new file mode 100644 index 000000000..a3d850a65 --- /dev/null +++ b/app/kubernetes/views/volumes/edit/volume.js @@ -0,0 +1,8 @@ +angular.module('portainer.kubernetes').component('kubernetesVolumeView', { + templateUrl: './volume.html', + controller: 'KubernetesVolumeController', + controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, +}); diff --git a/app/kubernetes/views/volumes/edit/volumeController.js b/app/kubernetes/views/volumes/edit/volumeController.js new file mode 100644 index 000000000..a91be622c --- /dev/null +++ b/app/kubernetes/views/volumes/edit/volumeController.js @@ -0,0 +1,130 @@ +import angular from 'angular'; +import _ from 'lodash-es'; +import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; +import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; + +class KubernetesVolumeController { + /* @ngInject */ + constructor($async, $state, Notifications, LocalStorage, KubernetesVolumeService, KubernetesEventService, KubernetesNamespaceHelper, KubernetesApplicationService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.LocalStorage = LocalStorage; + + this.KubernetesVolumeService = KubernetesVolumeService; + this.KubernetesEventService = KubernetesEventService; + this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + this.KubernetesApplicationService = KubernetesApplicationService; + + this.onInit = this.onInit.bind(this); + this.getVolume = this.getVolume.bind(this); + this.getVolumeAsync = this.getVolumeAsync.bind(this); + this.getEvents = this.getEvents.bind(this); + this.getEventsAsync = this.getEventsAsync.bind(this); + } + + selectTab(index) { + this.LocalStorage.storeActiveTab('volume', index); + } + + showEditor() { + this.state.showEditorTab = true; + this.selectTab(2); + } + + isExternalVolume() { + return KubernetesVolumeHelper.isExternalVolume(this.volume); + } + + isSystemNamespace() { + return this.KubernetesNamespaceHelper.isSystemNamespace(this.volume.ResourcePool.Namespace.Name); + } + + isUsed() { + return KubernetesVolumeHelper.isUsed(this.volume); + } + + /** + * VOLUME + */ + async getVolumeAsync() { + try { + const [volume, applications] = await Promise.all([ + this.KubernetesVolumeService.get(this.state.namespace, this.state.name), + this.KubernetesApplicationService.get(this.state.namespace), + ]); + volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications); + this.volume = volume; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve volume'); + } + } + + getVolume() { + return this.$async(this.getVolumeAsync); + } + + /** + * EVENTS + */ + hasEventWarnings() { + return this.state.eventWarningCount; + } + + async getEventsAsync() { + try { + this.state.eventsLoading = true; + const events = await this.KubernetesEventService.get(this.state.namespace); + this.events = _.filter(events, (event) => event.Involved.uid === this.volume.PersistentVolumeClaim.Id); + this.state.eventWarningCount = KubernetesEventHelper.warningCount(this.events); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve application related events'); + } finally { + this.state.eventsLoading = false; + } + } + + getEvents() { + return this.$async(this.getEventsAsync); + } + + /** + * ON INIT + */ + async onInit() { + this.state = { + activeTab: 0, + currentName: this.$state.$current.name, + showEditorTab: false, + eventsLoading: true, + viewReady: false, + namespace: this.$transition$.params().namespace, + name: this.$transition$.params().name, + eventWarningCount: 0, + }; + + this.state.activeTab = this.LocalStorage.getActiveTab('volume'); + + try { + await this.getVolume(); + await this.getEvents(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to load view data'); + } finally { + this.state.viewReady = true; + } + } + + $onInit() { + return this.$async(this.onInit); + } + + $onDestroy() { + if (this.state.currentName !== this.$state.$current.name) { + this.LocalStorage.storeActiveTab('volume', 0); + } + } +} + +export default KubernetesVolumeController; +angular.module('portainer.kubernetes').controller('KubernetesVolumeController', KubernetesVolumeController); diff --git a/app/kubernetes/views/volumes/volumes.html b/app/kubernetes/views/volumes/volumes.html new file mode 100644 index 000000000..4c9ba2f81 --- /dev/null +++ b/app/kubernetes/views/volumes/volumes.html @@ -0,0 +1,20 @@ + + Volumes + + + + +
+
+
+ + +
+
+
diff --git a/app/kubernetes/views/volumes/volumes.js b/app/kubernetes/views/volumes/volumes.js new file mode 100644 index 000000000..98829ddc1 --- /dev/null +++ b/app/kubernetes/views/volumes/volumes.js @@ -0,0 +1,5 @@ +angular.module('portainer.kubernetes').component('kubernetesVolumesView', { + templateUrl: './volumes.html', + controller: 'KubernetesVolumesController', + controllerAs: 'ctrl', +}); diff --git a/app/kubernetes/views/volumes/volumesController.js b/app/kubernetes/views/volumes/volumesController.js new file mode 100644 index 000000000..2fcb899a3 --- /dev/null +++ b/app/kubernetes/views/volumes/volumesController.js @@ -0,0 +1,82 @@ +import _ from 'lodash-es'; +import angular from 'angular'; +import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; + +class KubernetesVolumesController { + /* @ngInject */ + constructor($async, $state, Notifications, ModalService, KubernetesVolumeService, KubernetesApplicationService) { + this.$async = $async; + this.$state = $state; + this.Notifications = Notifications; + this.ModalService = ModalService; + this.KubernetesVolumeService = KubernetesVolumeService; + this.KubernetesApplicationService = KubernetesApplicationService; + + this.onInit = this.onInit.bind(this); + this.getVolumes = this.getVolumes.bind(this); + this.getVolumesAsync = this.getVolumesAsync.bind(this); + this.removeAction = this.removeAction.bind(this); + this.removeActionAsync = this.removeActionAsync.bind(this); + } + + async removeActionAsync(selectedItems) { + let actionCount = selectedItems.length; + for (const volume of selectedItems) { + try { + await this.KubernetesVolumeService.delete(volume); + this.Notifications.success('Volume successfully removed', volume.PersistentVolumeClaim.Name); + const index = this.volumes.indexOf(volume); + this.volumes.splice(index, 1); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to remove volume'); + } finally { + --actionCount; + if (actionCount === 0) { + this.$state.reload(); + } + } + } + } + + removeAction(selectedItems) { + this.ModalService.confirmDeletion('Do you want to remove the selected volume(s)?', (confirmed) => { + if (confirmed) { + return this.$async(this.removeActionAsync, selectedItems); + } + }); + } + + async getVolumesAsync() { + try { + const [volumes, applications] = await Promise.all([this.KubernetesVolumeService.get(), this.KubernetesApplicationService.get()]); + + this.volumes = _.map(volumes, (volume) => { + volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications); + return volume; + }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retreive resource pools'); + } + } + + getVolumes() { + return this.$async(this.getVolumesAsync); + } + + async onInit() { + this.state = { + viewReady: false, + }; + + await this.getVolumes(); + + this.state.viewReady = true; + } + + $onInit() { + return this.$async(this.onInit); + } +} + +export default KubernetesVolumesController; +angular.module('portainer.kubernetes').controller('KubernetesVolumesController', KubernetesVolumesController); diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 2a43cb0dd..face617cb 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -8,7 +8,7 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat // to have more controls on which URL should trigger the unauthenticated state. $rootScope.$on('unauthenticated', function (event, data) { if (!_.includes(data.config.url, '/v2/') && !_.includes(data.config.url, '/api/v4/')) { - $state.go('portainer.auth', { error: 'Your session has expired' }); + $state.go('portainer.logout', { error: 'Your session has expired' }); } }); @@ -106,8 +106,7 @@ angular.module('portainer.app', []).config([ name: 'portainer.auth', url: '/auth', params: { - logout: false, - error: '', + reload: false, }, views: { 'content@': { @@ -118,6 +117,22 @@ angular.module('portainer.app', []).config([ 'sidebar@': {}, }, }; + const logout = { + name: 'portainer.logout', + url: '/logout', + params: { + error: '', + performApiLogout: false, + }, + views: { + 'content@': { + templateUrl: './views/logout/logout.html', + controller: 'LogoutController', + controllerAs: 'ctrl', + }, + 'sidebar@': {}, + }, + }; var endpoints = { name: 'portainer.endpoints', @@ -141,6 +156,18 @@ angular.module('portainer.app', []).config([ }, }; + const endpointKubernetesConfiguration = { + name: 'portainer.endpoints.endpoint.kubernetesConfig', + url: '/configure', + views: { + 'content@': { + templateUrl: '../kubernetes/views/configure/configure.html', + controller: 'KubernetesConfigureController', + controllerAs: 'ctrl', + }, + }, + }; + var endpointCreation = { name: 'portainer.endpoints.new', url: '/new', @@ -235,6 +262,7 @@ angular.module('portainer.app', []).config([ 'content@': { templateUrl: './views/init/endpoint/initEndpoint.html', controller: 'InitEndpointController', + controllerAs: 'ctrl', }, }, }; @@ -491,10 +519,12 @@ angular.module('portainer.app', []).config([ $stateRegistryProvider.register(about); $stateRegistryProvider.register(account); $stateRegistryProvider.register(authentication); + $stateRegistryProvider.register(logout); $stateRegistryProvider.register(endpoints); $stateRegistryProvider.register(endpoint); $stateRegistryProvider.register(endpointAccess); $stateRegistryProvider.register(endpointCreation); + $stateRegistryProvider.register(endpointKubernetesConfiguration); $stateRegistryProvider.register(groups); $stateRegistryProvider.register(group); $stateRegistryProvider.register(groupAccess); diff --git a/app/portainer/components/access-datatable/accessDatatable.html b/app/portainer/components/access-datatable/accessDatatable.html index 55169a342..3f92dbecc 100644 --- a/app/portainer/components/access-datatable/accessDatatable.html +++ b/app/portainer/components/access-datatable/accessDatatable.html @@ -69,12 +69,14 @@ > - + {{ item.Name }} - inherited - override + inherited + override {{ item.Type }} diff --git a/app/portainer/components/access-datatable/accessDatatableController.js b/app/portainer/components/access-datatable/accessDatatableController.js index ec5846ad5..653c5a436 100644 --- a/app/portainer/components/access-datatable/accessDatatableController.js +++ b/app/portainer/components/access-datatable/accessDatatableController.js @@ -6,7 +6,7 @@ angular.module('portainer.app').controller('AccessDatatableController', [ angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); this.disableRemove = function (item) { - return item.Inherited; + return item.Inherited && this.inheritFrom; }; this.allowSelection = function (item) { diff --git a/app/portainer/components/accessControlPanel/porAccessControlPanelController.js b/app/portainer/components/accessControlPanel/porAccessControlPanelController.js index 97497a7fc..8667482e5 100644 --- a/app/portainer/components/accessControlPanel/porAccessControlPanelController.js +++ b/app/portainer/components/accessControlPanel/porAccessControlPanelController.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership'; -import { ResourceControlTypeString as RCTS, ResourceControlTypeInt as RCTI } from 'Portainer/models/resourceControl/resourceControlTypes'; +import { ResourceControlTypeInt as RCTI, ResourceControlTypeString as RCTS } from 'Portainer/models/resourceControl/resourceControlTypes'; import { AccessControlPanelData } from './porAccessControlPanelModel'; angular.module('portainer.app').controller('porAccessControlPanelController', [ diff --git a/app/portainer/components/accessManagement/porAccessManagement.html b/app/portainer/components/accessManagement/porAccessManagement.html index 1ac0fdb99..5d46339bc 100644 --- a/app/portainer/components/accessManagement/porAccessManagement.html +++ b/app/portainer/components/accessManagement/porAccessManagement.html @@ -27,7 +27,6 @@
-
-
diff --git a/app/portainer/components/accessManagement/porAccessManagementController.js b/app/portainer/components/accessManagement/porAccessManagementController.js index a76a62991..1234d3f3c 100644 --- a/app/portainer/components/accessManagement/porAccessManagementController.js +++ b/app/portainer/components/accessManagement/porAccessManagementController.js @@ -53,27 +53,14 @@ class PorAccessManagementController { } async $onInit() { - const entity = this.accessControlledEntity; - if (!entity) { - this.Notifications.error('Failure', 'Unable to retrieve accesses'); - return; - } - if (!entity.UserAccessPolicies) { - entity.UserAccessPolicies = {}; - } - if (!entity.TeamAccessPolicies) { - entity.TeamAccessPolicies = {}; - } - const parent = this.inheritFrom; - if (parent && !parent.UserAccessPolicies) { - parent.UserAccessPolicies = {}; - } - if (parent && !parent.TeamAccessPolicies) { - parent.TeamAccessPolicies = {}; - } - this.roles = []; - this.rbacEnabled = false; try { + const entity = this.accessControlledEntity; + const parent = this.inheritFrom; + // TODO: refactor + // extract this code and locate it in AccessService.accesses() function + // see resourcePoolAccessController for another usage of AccessService.accesses() + // which needs RBAC support + this.roles = []; this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC); if (this.rbacEnabled) { this.roles = await this.RoleService.roles(); @@ -81,13 +68,7 @@ class PorAccessManagementController { selectedRole: this.roles[0], }; } - const data = await this.AccessService.accesses( - entity.UserAccessPolicies, - entity.TeamAccessPolicies, - parent ? parent.UserAccessPolicies : {}, - parent ? parent.TeamAccessPolicies : {}, - this.roles - ); + const data = await this.AccessService.accesses(entity, parent, this.roles); this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc'); this.authorizedUsersAndTeams = data.authorizedUsersAndTeams; } catch (err) { diff --git a/app/portainer/components/datatables/genericDatatableController.js b/app/portainer/components/datatables/genericDatatableController.js index b12bc5bd4..89367a114 100644 --- a/app/portainer/components/datatables/genericDatatableController.js +++ b/app/portainer/components/datatables/genericDatatableController.js @@ -6,6 +6,7 @@ function isBetween(value, a, b) { return (value >= a && value <= b) || (value >= b && value <= a); } +// TODO: review - refactor to use a class that can be extended angular.module('portainer.app').controller('GenericDatatableController', [ '$interval', 'PaginationService', diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html index fa67430e5..6b4a5aeb3 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html @@ -1,8 +1,14 @@
- + + @@ -22,6 +28,9 @@ {{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }} + + {{ $ctrl.model.Kubernetes.Snapshots[0].Time | getisodatefromtimestamp }} + @@ -69,12 +78,36 @@
-
+
No snapshot available
+
+ + + {{ $ctrl.model.Kubernetes.Snapshots[0].TotalCPU }} CPU + + {{ $ctrl.model.Kubernetes.Snapshots[0].TotalMemory | humansize }} RAM + + + + + Kubernetes {{ $ctrl.model.Kubernetes.Snapshots[0].KubernetesVersion }} + + + {{ $ctrl.model.Kubernetes.Snapshots[0].NodeCount }} {{ $ctrl.model.Kubernetes.Snapshots[0].NodeCount === 1 ? 'node' : 'nodes' }} + + +
+ +
+ + - + +
+
diff --git a/app/portainer/components/header-content.js b/app/portainer/components/header-content.js index 188e28209..448222503 100644 --- a/app/portainer/components/header-content.js +++ b/app/portainer/components/header-content.js @@ -8,7 +8,7 @@ angular.module('portainer.app').directive('rdHeaderContent', [ scope.username = Authentication.getUserDetails().username; }, template: - '', + '', restrict: 'E', }; return directive; diff --git a/app/portainer/components/slider/sliderController.js b/app/portainer/components/slider/sliderController.js index 4541a2762..4ac47d9f6 100644 --- a/app/portainer/components/slider/sliderController.js +++ b/app/portainer/components/slider/sliderController.js @@ -1,21 +1,45 @@ -angular.module('portainer.app').controller('SliderController', function () { - var ctrl = this; +// TODO: k8s merge - TEST WITH EXISTING SLIDERS ! +// Not sure if this is not breaking existing sliders on docker views +// Or sliders with onChange call (docker service update view) +import angular from 'angular'; - ctrl.options = { - floor: ctrl.floor, - ceil: ctrl.ceil, - step: ctrl.step, - precision: ctrl.precision, - showSelectionBar: true, - enforceStep: false, - translate: function (value, sliderId, label) { - if ((label === 'floor' && ctrl.floor === 0) || value === 0) { - return 'unlimited'; - } - return value; - }, - onChange: function () { - ctrl.onChange(); - }, - }; -}); +class SliderController { + /* @ngInject */ + constructor($scope) { + this.$scope = $scope; + + this.buildOptions = this.buildOptions.bind(this); + this.translate = this.translate.bind(this); + } + + $onChanges() { + this.buildOptions(); + } + + translate(value, sliderId, label) { + if ((label === 'floor' && this.floor === 0) || value === 0) { + return 'unlimited'; + } + return value; + } + + buildOptions() { + this.options = { + floor: this.floor, + ceil: this.ceil, + step: this.step, + precision: this.precision, + showSelectionBar: true, + enforceStep: false, + translate: this.translate, + onChange: () => this.onChange(), + }; + } + + $onInit() { + this.buildOptions(); + } +} + +export default SliderController; +angular.module('portainer.app').controller('SliderController', SliderController); diff --git a/app/portainer/components/tooltip.js b/app/portainer/components/tooltip.js index 6609986da..3c9c9e4c4 100644 --- a/app/portainer/components/tooltip.js +++ b/app/portainer/components/tooltip.js @@ -4,9 +4,12 @@ angular.module('portainer.app').directive('portainerTooltip', [ scope: { message: '@', position: '@', + customStyle: '', + template: ` + + + `, restrict: 'E', }; return directive; diff --git a/app/portainer/error.js b/app/portainer/error.js new file mode 100644 index 000000000..88c57d72b --- /dev/null +++ b/app/portainer/error.js @@ -0,0 +1,6 @@ +export default class PortainerError { + constructor(msg, err) { + this.msg = msg; + this.err = err; + } +} diff --git a/app/portainer/filters/filters.js b/app/portainer/filters/filters.js index 034dba32c..f31705979 100644 --- a/app/portainer/filters/filters.js +++ b/app/portainer/filters/filters.js @@ -126,11 +126,13 @@ angular return function (type) { if (type === 1) { return 'Docker'; - } else if (type === 2) { + } else if (type === 2 || type === 6) { return 'Agent'; } else if (type === 3) { return 'Azure ACI'; - } else if (type === 4) { + } else if (type === 5) { + return 'Kubernetes'; + } else if (type === 4 || type === 7) { return 'Edge Agent'; } return ''; @@ -143,6 +145,8 @@ angular return 'fab fa-microsoft'; } else if (type === 4) { return 'fa fa-cloud'; + } else if (type === 5 || type === 6 || type === 7) { + return 'fas fa-dharmachakra'; } return 'fab fa-docker'; }; diff --git a/app/portainer/models/endpoint/formValues.js b/app/portainer/models/endpoint/formValues.js new file mode 100644 index 000000000..f7268ffb9 --- /dev/null +++ b/app/portainer/models/endpoint/formValues.js @@ -0,0 +1,41 @@ +import { PortainerEndpointConnectionTypes } from 'Portainer/models/endpoint/models'; + +export class PortainerEndpointInitFormValues { + constructor() { + this.ConnectionType = PortainerEndpointConnectionTypes.KUBERNETES_LOCAL; + this.Name = ''; + this.URL = ''; + this.TLS = false; + this.TLSSkipVerify = false; + this.TLSSKipClientVerify = false; + this.TLSCACert = null; + this.TLSCert = null; + this.TLSKey = null; + this.AzureApplicationId = ''; + this.AzureTenantId = ''; + this.AzureAuthenticationKey = ''; + } +} + +class PortainerEndpointInitFormValueEndpointSection { + constructor(value, title, classes, description) { + this.Id = value; + this.Value = value; + this.Title = title; + this.Classes = classes; + this.Description = description; + } +} + +export const PortainerEndpointInitFormValueEndpointSections = Object.freeze([ + new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.DOCKER_LOCAL, 'Docker', 'fab fa-docker', 'Manage the local Docker environment'), + new PortainerEndpointInitFormValueEndpointSection( + PortainerEndpointConnectionTypes.KUBERNETES_LOCAL, + 'Kubernetes', + 'fas fa-dharmachakra', + 'Manage the local Kubernetes environment' + ), + new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.REMOTE, 'Remote', 'fab fa-docker', 'Manage a remote Docker environment'), + new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.AGENT, 'Agent', 'fa fa-bolt', 'Connect to a Portainer agent'), + new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.AZURE, 'Azure', 'fab fa-microsoft', 'Connect to Microsoft Azure ACI'), +]); diff --git a/app/portainer/models/endpoint/models.js b/app/portainer/models/endpoint/models.js new file mode 100644 index 000000000..feae2b9ab --- /dev/null +++ b/app/portainer/models/endpoint/models.js @@ -0,0 +1,28 @@ +/** + * JS reference of portainer.go#EndpointType iota + */ +export const PortainerEndpointTypes = Object.freeze({ + // DockerEnvironment represents an endpoint connected to a Docker environment + DockerEnvironment: 1, + // AgentOnDockerEnvironment represents an endpoint connected to a Portainer agent deployed on a Docker environment + AgentOnDockerEnvironment: 2, + // AzureEnvironment represents an endpoint connected to an Azure environment + AzureEnvironment: 3, + // EdgeAgentOnDockerEnvironment represents an endpoint connected to an Edge agent deployed on a Docker environment + EdgeAgentOnDockerEnvironment: 4, + // KubernetesLocalEnvironment represents an endpoint connected to a local Kubernetes environment + KubernetesLocalEnvironment: 5, + // AgentOnKubernetesEnvironment represents an endpoint connected to a Portainer agent deployed on a Kubernetes environment + AgentOnKubernetesEnvironment: 6, + // EdgeAgentOnKubernetesEnvironment represents an endpoint connected to an Edge agent deployed on a Kubernetes environment + EdgeAgentOnKubernetesEnvironment: 7, +}); + +export const PortainerEndpointConnectionTypes = Object.freeze({ + DOCKER_LOCAL: 1, + KUBERNETES_LOCAL: 2, + REMOTE: 3, + AZURE: 4, + AGENT: 5, + EDGE: 6, +}); diff --git a/app/portainer/rest/auth.js b/app/portainer/rest/auth.js index 5169084c2..1723ccd1f 100644 --- a/app/portainer/rest/auth.js +++ b/app/portainer/rest/auth.js @@ -4,13 +4,11 @@ angular.module('portainer.app').factory('Auth', [ function AuthFactory($resource, API_ENDPOINT_AUTH) { 'use strict'; return $resource( - API_ENDPOINT_AUTH, + API_ENDPOINT_AUTH + '/:action', {}, { - login: { - method: 'POST', - ignoreLoadingBar: true, - }, + login: { method: 'POST', ignoreLoadingBar: true }, + logout: { method: 'POST', params: { action: 'logout' }, ignoreLoadingBar: true }, } ); }, diff --git a/app/portainer/services/allSettled.js b/app/portainer/services/allSettled.js new file mode 100644 index 000000000..020869dfa --- /dev/null +++ b/app/portainer/services/allSettled.js @@ -0,0 +1,34 @@ +import _ from 'lodash-es'; + +/** + * + * @param {any[]} promises + */ +export default async function $allSettled(promises) { + const res = { + fulfilled: [], + rejected: [], + }; + const data = await Promise.allSettled(promises); + res.fulfilled = _.reduce( + data, + (acc, item) => { + if (item.status === 'fulfilled') { + acc.push(item.value); + } + return acc; + }, + [] + ); + res.rejected = _.reduce( + data, + (acc, item) => { + if (item.status === 'rejected') { + acc.push(item.reason); + } + return acc; + }, + [] + ); + return res; +} diff --git a/app/portainer/services/api/accessService.js b/app/portainer/services/api/accessService.js index 457d6bbfc..2c9acd0dd 100644 --- a/app/portainer/services/api/accessService.js +++ b/app/portainer/services/api/accessService.js @@ -4,9 +4,10 @@ import { TeamAccessViewModel } from '../../models/access'; angular.module('portainer.app').factory('AccessService', [ '$q', + '$async', 'UserService', 'TeamService', - function AccessServiceFactory($q, UserService, TeamService) { + function AccessServiceFactory($q, $async, UserService, TeamService) { 'use strict'; var service = {}; @@ -15,6 +16,7 @@ angular.module('portainer.app').factory('AccessService', [ const role = _.find(roles, (role) => role.Id === roleId); return role ? role : { Id: 0, Name: '-' }; } + return { Id: 0, Name: '-' }; } function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies, roles) { @@ -50,7 +52,7 @@ angular.module('portainer.app').factory('AccessService', [ }; } - service.accesses = function (authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies, roles) { + function getAccesses(authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies, roles) { var deferred = $q.defer(); $q.all({ @@ -80,7 +82,36 @@ angular.module('portainer.app').factory('AccessService', [ }); return deferred.promise; - }; + } + + async function accessesAsync(entity, parent, roles) { + try { + if (!entity) { + throw { msg: 'Unable to retrieve accesses' }; + } + if (!entity.UserAccessPolicies) { + entity.UserAccessPolicies = {}; + } + if (!entity.TeamAccessPolicies) { + entity.TeamAccessPolicies = {}; + } + if (parent && !parent.UserAccessPolicies) { + parent.UserAccessPolicies = {}; + } + if (parent && !parent.TeamAccessPolicies) { + parent.TeamAccessPolicies = {}; + } + return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {}, roles); + } catch (err) { + throw err; + } + } + + function accesses(entity, parent, roles) { + return $async(accessesAsync, entity, parent, roles); + } + + service.accesses = accesses; service.generateAccessPolicies = function (userAccessPolicies, teamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId) { const newUserPolicies = _.clone(userAccessPolicies); diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 1501e9ed8..3bbdf567f 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -1,3 +1,5 @@ +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; + angular.module('portainer.app').factory('EndpointService', [ '$q', 'Endpoints', @@ -57,7 +59,7 @@ angular.module('portainer.app').factory('EndpointService', [ service.createLocalEndpoint = function () { var deferred = $q.defer(); - FileUploadService.createEndpoint('local', 1, '', '', 1, [], false) + FileUploadService.createEndpoint('local', PortainerEndpointTypes.DockerEnvironment, '', '', 1, [], false) .then(function success(response) { deferred.resolve(response.data); }) @@ -86,7 +88,11 @@ angular.module('portainer.app').factory('EndpointService', [ var deferred = $q.defer(); var endpointURL = URL; - if (type !== 4) { + if ( + type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment && + type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment && + type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment + ) { endpointURL = 'tcp://' + URL; } @@ -115,6 +121,20 @@ angular.module('portainer.app').factory('EndpointService', [ return deferred.promise; }; + service.createLocalKubernetesEndpoint = function () { + var deferred = $q.defer(); + + FileUploadService.createEndpoint('local', 5, '', '', 1, [], true, true, true) + .then(function success(response) { + deferred.resolve(response.data); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to create endpoint', err: err }); + }); + + return deferred.promise; + }; + service.createAzureEndpoint = function (name, applicationId, tenantId, authenticationKey, groupId, tagIds) { var deferred = $q.defer(); diff --git a/app/portainer/services/api/registryService.js b/app/portainer/services/api/registryService.js index 375b6ee4f..b59280d50 100644 --- a/app/portainer/services/api/registryService.js +++ b/app/portainer/services/api/registryService.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; -import { RegistryViewModel, RegistryCreateRequest } from '../../models/registry'; +import { RegistryCreateRequest, RegistryViewModel } from '../../models/registry'; angular.module('portainer.app').factory('RegistryService', [ '$q', diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js index 31bf27548..47a9b6751 100644 --- a/app/portainer/services/api/stackService.js +++ b/app/portainer/services/api/stackService.js @@ -1,17 +1,17 @@ import _ from 'lodash-es'; -import { StackViewModel, ExternalStackViewModel } from '../../models/stack'; +import { ExternalStackViewModel, StackViewModel } from '../../models/stack'; angular.module('portainer.app').factory('StackService', [ '$q', + '$async', 'Stack', - 'ResourceControlService', 'FileUploadService', 'StackHelper', 'ServiceService', 'ContainerService', 'SwarmService', 'EndpointProvider', - function StackServiceFactory($q, Stack, ResourceControlService, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService, EndpointProvider) { + function StackServiceFactory($q, $async, Stack, FileUploadService, StackHelper, ServiceService, ContainerService, SwarmService, EndpointProvider) { 'use strict'; var service = {}; @@ -252,7 +252,6 @@ angular.module('portainer.app').factory('StackService', [ return deferred.promise; }; - service.createComposeStackFromFileContent = function (name, stackFileContent, env, endpointId) { var payload = { Name: name, @@ -333,6 +332,23 @@ angular.module('portainer.app').factory('StackService', [ return action(name, stackFileContent, env, endpointId); }; + async function kubernetesDeployAsync(endpointId, namespace, content, compose) { + try { + const payload = { + StackFileContent: content, + ComposeFormat: compose, + Namespace: namespace, + }; + await Stack.create({ method: 'undefined', type: 3, endpointId: endpointId }, payload).$promise; + } catch (err) { + throw { err: err }; + } + } + + service.kubernetesDeploy = function (endpointId, namespace, content, compose) { + return $async(kubernetesDeployAsync, endpointId, namespace, content, compose); + }; + return service; }, ]); diff --git a/app/portainer/services/api/tagService.js b/app/portainer/services/api/tagService.js index a62dfc3ff..ccc4551b0 100644 --- a/app/portainer/services/api/tagService.js +++ b/app/portainer/services/api/tagService.js @@ -2,8 +2,9 @@ import { TagViewModel } from '../../models/tag'; angular.module('portainer.app').factory('TagService', [ '$q', + '$async', 'Tags', - function TagServiceFactory($q, Tags) { + function TagServiceFactory($q, $async, Tags) { 'use strict'; var service = {}; @@ -37,7 +38,7 @@ angular.module('portainer.app').factory('TagService', [ return deferred.promise; }; - service.createTag = async function (name) { + async function createTagAsync(name) { var payload = { Name: name, }; @@ -47,7 +48,12 @@ angular.module('portainer.app').factory('TagService', [ } catch (err) { throw { msg: 'Unable to create tag', err }; } - }; + } + + function createTag(name) { + return $async(createTagAsync, name); + } + service.createTag = createTag; service.deleteTag = function (id) { return Tags.remove({ id: id }).$promise; diff --git a/app/portainer/services/authentication.js b/app/portainer/services/authentication.js index ecb6b610d..a5f1294fa 100644 --- a/app/portainer/services/authentication.js +++ b/app/portainer/services/authentication.js @@ -34,13 +34,21 @@ angular.module('portainer.app').factory('Authentication', [ } } - function logout() { + async function logoutAsync(performApiLogout) { + if (performApiLogout) { + await Auth.logout().$promise; + } + StateManager.clean(); EndpointProvider.clean(); LocalStorage.clean(); LocalStorage.storeLoginStateUUID(''); } + function logout(performApiLogout) { + return $async(logoutAsync, performApiLogout); + } + function init() { return $async(initAsync); } diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index c3b448f49..1cd99cb6d 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -126,6 +126,13 @@ angular.module('portainer.app').factory('LocalStorage', [ getJobImage: function () { return localStorageService.get('job_image'); }, + storeActiveTab: function (key, index) { + return localStorageService.set('active_tab_' + key, index); + }, + getActiveTab: function (key) { + const activeTab = localStorageService.get('active_tab_' + key); + return activeTab === null ? 0 : activeTab; + }, storeLogoutReason: (reason) => localStorageService.set('logout_reason', reason), getLogoutReason: () => localStorageService.get('logout_reason'), cleanLogoutReason: () => localStorageService.remove('logout_reason'), diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 2a9e40f64..663f564d7 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -128,6 +128,21 @@ angular.module('portainer.app').factory('ModalService', [ }); }; + service.confirmUpdate = function (message, callback) { + message = $sanitize(message); + service.confirm({ + title: 'Are you sure ?', + message: message, + buttons: { + confirm: { + label: 'Update', + className: 'btn-warning', + }, + }, + callback: callback, + }); + }; + service.confirmContainerDeletion = function (title, callback) { title = $sanitize(title); prompt({ diff --git a/app/portainer/services/notifications.js b/app/portainer/services/notifications.js index 3fb564b53..fdfd85e85 100644 --- a/app/portainer/services/notifications.js +++ b/app/portainer/services/notifications.js @@ -16,7 +16,9 @@ angular.module('portainer.app').factory('Notifications', [ service.error = function (title, e, fallbackText) { var msg = fallbackText; - if (e.err && e.err.data && e.err.data.details) { + if (e.err && e.err.data && e.err.data.message) { + msg = e.err.data.message; + } else if (e.err && e.err.data && e.err.data.details) { msg = e.err.data.details; } else if (e.data && e.data.details) { msg = e.data.details; @@ -26,8 +28,6 @@ angular.module('portainer.app').factory('Notifications', [ msg = e.data.content; } else if (e.message) { msg = e.message; - } else if (e.err && e.err.data && e.err.data.message) { - msg = e.err.data.message; } else if (e.err && e.err.data && e.err.data.length > 0 && e.err.data[0].message) { msg = e.err.data[0].message; } else if (e.err && e.err.data && e.err.data.err) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index e46502334..8b71ca6ee 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -173,6 +173,12 @@ angular.module('portainer.app').factory('StateManager', [ LocalStorage.storeEndpointState(state.endpoint); deferred.resolve(); return deferred.promise; + } else if (endpoint.Type === 5 || endpoint.Type === 6 || endpoint.Type === 7) { + state.endpoint.name = endpoint.Name; + state.endpoint.mode = { provider: 'KUBERNETES' }; + LocalStorage.storeEndpointState(state.endpoint); + deferred.resolve(); + return deferred.promise; } $q.all({ diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index ce5d31d60..21b39af9d 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -1,3 +1,4 @@ +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel'; angular @@ -19,6 +20,7 @@ angular $scope.state = { EnvironmentType: 'agent', actionInProgress: false, + deploymentTab: 0, allowCreateTag: Authentication.isAdmin(), availableEdgeAgentCheckinOptions: [ { key: 'Use default interval', value: 0 }, @@ -54,9 +56,12 @@ angular }; $scope.copyAgentCommand = function () { - clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent'); - $('#copyNotification').show(); - $('#copyNotification').fadeOut(2000); + if ($scope.state.deploymentTab === 0) { + clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent'); + } else { + clipboard.copyText('curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml'); + } + $('#copyNotification').show().fadeOut(2500); }; $scope.setDefaultPortainerInstanceURL = function () { @@ -67,6 +72,20 @@ angular $scope.formValues.URL = ''; }; + $scope.onCreateTag = function onCreateTag(tagName) { + return $async(onCreateTagAsync, tagName); + }; + + async function onCreateTagAsync(tagName) { + try { + const tag = await TagService.createTag(tagName); + $scope.availableTags = $scope.availableTags.concat(tag); + $scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id); + } catch (err) { + Notifications.error('Failue', err, 'Unable to create tag'); + } + } + $scope.addDockerEndpoint = function () { var name = $scope.formValues.Name; var URL = $filter('stripprotocol')($scope.formValues.URL); @@ -83,7 +102,7 @@ angular var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert; var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; - addEndpoint(name, 1, URL, publicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); + addEndpoint(name, PortainerEndpointTypes.DockerEnvironment, URL, publicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); }; $scope.addAgentEndpoint = function () { @@ -93,7 +112,9 @@ angular var groupId = $scope.formValues.GroupId; var tagIds = $scope.formValues.TagIds; - addEndpoint(name, 2, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); + addEndpoint(name, PortainerEndpointTypes.AgentOnDockerEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); + // TODO: k8s merge - temporarily updated to AgentOnKubernetesEnvironment, breaking Docker agent support + // addEndpoint(name, PortainerEndpointTypes.AgentOnKubernetesEnvironment, URL, publicURL, groupId, tags, true, true, true, null, null, null); }; $scope.addEdgeAgentEndpoint = function () { @@ -102,7 +123,9 @@ angular var tagIds = $scope.formValues.TagIds; var URL = $scope.formValues.URL; - addEndpoint(name, 4, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); + addEndpoint(name, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); + // TODO: k8s merge - temporarily updated to EdgeAgentOnKubernetesEnvironment, breaking Docker Edge agent support + // addEndpoint(name, PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment, URL, "", groupId, tags, false, false, false, null, null, null); }; $scope.addAzureEndpoint = function () { @@ -116,20 +139,6 @@ angular createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds); }; - $scope.onCreateTag = function onCreateTag(tagName) { - return $async(onCreateTagAsync, tagName); - }; - - async function onCreateTagAsync(tagName) { - try { - const tag = await TagService.createTag(tagName); - $scope.availableTags = $scope.availableTags.concat(tag); - $scope.formValues.TagIds = $scope.formValues.TagIds.concat(tag.Id); - } catch (err) { - Notifications.error('Failue', err, 'Unable to create tag'); - } - } - function createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) { $scope.state.actionInProgress = true; EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, groupId, tagIds) @@ -164,8 +173,10 @@ angular ) .then(function success(data) { Notifications.success('Endpoint created', name); - if (type === 4) { + if (type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { $state.go('portainer.endpoints.endpoint', { id: data.Id }); + } else if (type === PortainerEndpointTypes.AgentOnKubernetesEnvironment) { + $state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: data.Id }); } else { $state.go('portainer.endpoints', {}, { reload: true }); } diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index dd01e6142..06a1c62dd 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -75,13 +75,25 @@ Ensure that you have deployed the Portainer agent in your cluster first. You can use execute the following command on any manager node to deploy it.
- - curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent - - Copy - - - + + + + curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml + + + + + + curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent + + + +
+ Copy command + + + +
@@ -92,8 +104,11 @@
- Allows you to create an endpoint that can be registered with an Edge agent. The Edge agent will initiate the communications with the Portainer instance. All the - required information on how to connect an Edge agent to this endpoint will be available after endpoint creation. +

+ Allows you to create an endpoint that can be registered with an Edge agent. The Edge agent will initiate the communications with the Portainer instance. All the + required information on how to connect an Edge agent to this endpoint will be available after endpoint creation. +

+

You can read more about the Edge agent in the userguide available here.

@@ -127,7 +142,15 @@
- +
@@ -146,7 +169,8 @@ + > +
+
-
@@ -319,13 +345,8 @@
- + +
diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 367ace6f8..48fabddf8 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -10,11 +10,11 @@
- +

- This Edge endpoint is associated to an Edge environment. + This Edge endpoint is associated to an Edge environment {{ state.kubernetesEndpoint ? '(Kubernetes)' : '(Docker)' }}.

Edge key: {{ endpoint.EdgeKey }} @@ -24,11 +24,11 @@

- +

- Deploy the Edge agent on your remote Docker environment using the following command(s) + Deploy the Edge agent on your remote Docker/Kubernetes environment using the following command(s)

The agent will communicate with Portainer via {{ edgeKeyDetails.instanceURL }} and tcp://{{ edgeKeyDetails.tunnelServerAddr }} @@ -41,6 +41,11 @@ {{ dockerCommands.swarm }} + + + curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }} + +

Copy command @@ -66,6 +71,13 @@
+ + + + You should configure the features available in this Kubernetes environment in the + Kubernetes configuration view. + +
@@ -80,22 +92,23 @@
- +
-
+
-
+
-
+
+ -
Metadata
@@ -161,7 +175,7 @@
-
+
Security
diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 561f38893..30f6c900d 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -1,5 +1,6 @@ import _ from 'lodash-es'; import uuidv4 from 'uuid/v4'; +import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel'; angular @@ -24,6 +25,10 @@ angular uploadInProgress: false, actionInProgress: false, deploymentTab: 0, + azureEndpoint: false, + kubernetesEndpoint: false, + agentEndpoint: false, + edgeEndpoint: false, allowCreate: Authentication.isAdmin(), availableEdgeAgentCheckinOptions: [ { key: 'Use default interval', value: 0 }, @@ -58,7 +63,7 @@ angular $scope.endpoint.EdgeKey + ' -e CAP_HOST_MANAGEMENT=1 -v portainer_agent_data:/data --name portainer_edge_agent portainer/agent' ); - } else { + } else if ($scope.state.deploymentTab === 1) { clipboard.copyText( 'docker network create --driver overlay portainer_agent_network; docker service create --name portainer_edge_agent --network portainer_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e EDGE_ID=' + $scope.randomEdgeID + @@ -66,6 +71,8 @@ angular $scope.endpoint.EdgeKey + " -e CAP_HOST_MANAGEMENT=1 --mode global --constraint 'node.platform.os == linux' --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock --mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes --mount type=bind,src=//,dst=/host --mount type=volume,src=portainer_agent_data,dst=/data portainer/agent" ); + } else { + clipboard.copyText('curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | bash -s -- ' + $scope.randomEdgeID + ' ' + $scope.endpoint.EdgeKey); } $('#copyNotificationDeploymentCommand').show().fadeOut(2500); }; @@ -114,7 +121,12 @@ angular AzureAuthenticationKey: endpoint.AzureCredentials.AuthenticationKey, }; - if ($scope.endpointType !== 'local' && endpoint.Type !== 3) { + if ( + $scope.endpointType !== 'local' && + endpoint.Type !== PortainerEndpointTypes.AzureEnvironment && + endpoint.Type !== PortainerEndpointTypes.KubernetesLocalEnvironment && + endpoint.Type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment + ) { payload.URL = 'tcp://' + endpoint.URL; } @@ -151,6 +163,30 @@ angular return keyInformation; } + function configureState() { + if ( + $scope.endpoint.Type === PortainerEndpointTypes.KubernetesLocalEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment + ) { + $scope.state.kubernetesEndpoint = true; + } + if ($scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { + $scope.state.edgeEndpoint = true; + } + if ($scope.endpoint.Type === PortainerEndpointTypes.AzureEnvironment) { + $scope.state.azureEndpoint = true; + } + if ( + $scope.endpoint.Type === PortainerEndpointTypes.AgentOnDockerEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment || + $scope.endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment + ) { + $scope.state.agentEndpoint = true; + } + } + function initView() { $q.all({ endpoint: EndpointService.endpoint($transition$.params().id), @@ -166,7 +202,7 @@ angular $scope.endpointType = 'remote'; } endpoint.URL = $filter('stripprotocol')(endpoint.URL); - if (endpoint.Type === 4) { + if (endpoint.Type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || endpoint.Type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { $scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey); $scope.randomEdgeID = uuidv4(); $scope.dockerCommands = { @@ -180,6 +216,7 @@ angular $scope.endpoint = endpoint; $scope.groups = data.groups; $scope.availableTags = data.tags; + configureState(); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoint details'); diff --git a/app/portainer/views/home/home.html b/app/portainer/views/home/home.html index 489254d9b..1b14629ec 100644 --- a/app/portainer/views/home/home.html +++ b/app/portainer/views/home/home.html @@ -10,6 +10,8 @@ + +

diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index af41523f7..24d2efcf6 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -15,7 +15,8 @@ angular LegacyExtensionManager, ModalService, MotdService, - SystemService + SystemService, + KubernetesHealthService ) { $scope.state = { connectingToEdgeEndpoint: false, @@ -30,6 +31,10 @@ angular return switchToAzureEndpoint(endpoint); } else if (endpoint.Type === 4) { return switchToEdgeEndpoint(endpoint); + } else if (endpoint.Type === 5 || endpoint.Type === 6) { + return switchToKubernetesEndpoint(endpoint); + } else if (endpoint.Type === 7) { + return switchToKubernetesEdgeEndpoint(endpoint); } checkEndpointStatus(endpoint) @@ -102,6 +107,17 @@ angular }); } + function switchToKubernetesEndpoint(endpoint) { + EndpointProvider.setEndpointID(endpoint.Id); + StateManager.updateEndpointState(endpoint, []) + .then(function success() { + $state.go('kubernetes.dashboard'); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to connect to the Kubernetes endpoint'); + }); + } + function switchToEdgeEndpoint(endpoint) { if (!endpoint.EdgeID) { $state.go('portainer.endpoints.endpoint', { id: endpoint.Id }); @@ -121,6 +137,26 @@ angular }); } + function switchToKubernetesEdgeEndpoint(endpoint) { + if (!endpoint.EdgeID) { + $state.go('portainer.endpoints.endpoint', { id: endpoint.Id }); + return; + } + + EndpointProvider.setEndpointID(endpoint.Id); + $scope.state.connectingToEdgeEndpoint = true; + KubernetesHealthService.ping() + .then(function success() { + endpoint.Status = 1; + }) + .catch(function error() { + endpoint.Status = 2; + }) + .finally(function final() { + switchToKubernetesEndpoint(endpoint); + }); + } + function switchToDockerEndpoint(endpoint) { if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) { $scope.state.connectingToEdgeEndpoint = false; diff --git a/app/portainer/views/init/endpoint/includes/agent.html b/app/portainer/views/init/endpoint/includes/agent.html new file mode 100644 index 000000000..535355cdd --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/agent.html @@ -0,0 +1,34 @@ +

+ Information +
+
+
+ +

+ Connect directly to a Portainer agent running inside a Docker or Kubernetes environment. +

+
+
+
+
+ Environment +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ diff --git a/app/portainer/views/init/endpoint/includes/azure.html b/app/portainer/views/init/endpoint/includes/azure.html new file mode 100644 index 000000000..dca7e4953 --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/azure.html @@ -0,0 +1,63 @@ +
+ Information +
+
+
+ +

This feature is experimental.

+

+ Connect to Microsoft Azure to manage Azure Container Instances (ACI). +

+

+ + Have a look at + the Azure documentation + to retrieve the credentials required below. +

+
+
+
+
+ Environment +
+ +
+ +
+ +
+
+ +
+ Azure credentials +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ diff --git a/app/portainer/views/init/endpoint/includes/localDocker.html b/app/portainer/views/init/endpoint/includes/localDocker.html new file mode 100644 index 000000000..aea934557 --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/localDocker.html @@ -0,0 +1,21 @@ +
+ Information +
+
+
+ +

+ Manage the Docker environment where Portainer is running. +

+

+ + Ensure that you have started the Portainer container with the following Docker flag: +

+

-v "/var/run/docker.sock:/var/run/docker.sock" (Linux).

+

+ or +

+

-v \\.\pipe\docker_engine:\\.\pipe\docker_engine (Windows).

+
+
+
diff --git a/app/portainer/views/init/endpoint/includes/localKubernetes.html b/app/portainer/views/init/endpoint/includes/localKubernetes.html new file mode 100644 index 000000000..8c2204f56 --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/localKubernetes.html @@ -0,0 +1,12 @@ +
+ Information +
+
+
+ +

+ Manage the Kubernetes environment where Portainer is running. +

+
+
+
diff --git a/app/portainer/views/init/endpoint/includes/remote.html b/app/portainer/views/init/endpoint/includes/remote.html new file mode 100644 index 000000000..a6e23ab15 --- /dev/null +++ b/app/portainer/views/init/endpoint/includes/remote.html @@ -0,0 +1,120 @@ +
+ Information +
+
+
+ +

+ Connect Portainer to a remote Docker environment using the Docker API over TCP. +

+

+ + The Docker API must be exposed over TCP. You can find more information about how to expose the Docker API over TCP + in the Docker documentation. +

+
+
+
+
+ Environment +
+ +
+ +
+ +
+
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ Required TLS files +
+ +
+ +
+ + + {{ ctrl.formValues.TLSCACert.name }} + + + +
+
+ +
+ +
+ +
+ + + {{ ctrl.formValues.TLSCert.name }} + + + +
+
+ + +
+ +
+ + + {{ ctrl.formValues.TLSKey.name }} + + + +
+
+ +
+
+ diff --git a/app/portainer/views/init/endpoint/initEndpoint.html b/app/portainer/views/init/endpoint/initEndpoint.html index c67f678b6..3f3c50f99 100644 --- a/app/portainer/views/init/endpoint/initEndpoint.html +++ b/app/portainer/views/init/endpoint/initEndpoint.html @@ -4,8 +4,8 @@
- - + +
@@ -17,7 +17,7 @@
- Connect Portainer to the Docker environment you want to manage. + Connect Portainer to the container environment you want to manage.
@@ -25,377 +25,52 @@
-
- -
- -
-
- Information -
-
-
- -

- Manage the Docker environment where Portainer is running. -

-

- - Ensure that you have started the Portainer container with the following Docker flag: -

-

-v "/var/run/docker.sock:/var/run/docker.sock" (Linux).

-

- or -

-

-v \\.\pipe\docker_engine:\\.\pipe\docker_engine (Windows).

-
-
-
- -
-
- -
-
- + +
+
- - -
-
- Information -
-
-
- -

- Connect directly to a Portainer agent running inside a Swarm cluster. -

-

- - If you have started Portainer in the same overlay network as the agent, you can use tasks.AGENT_SERVICE_NAME:AGENT_SERVICE_PORT as the endpoint - URL format. -

-
-
-
-
- Environment -
- -
- -
- -
-
- - -
- -
- -
-
- - -
-
- -
-
- +
+
- - -
-
- Information -
-
-
- -

This feature is experimental.

-

- Connect to Microsoft Azure to manage Azure Container Instances (ACI). -

-

- - Have a look at - the Azure documentation - to retrieve the credentials required below. -

-
-
-
-
- Environment -
- -
- -
- -
-
- -
- Azure credentials -
- -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
-
- -
-
- +
+
- - -
-
- Information -
-
-
- -

- Connect Portainer to a remote Docker environment using the Docker API over TCP. -

-

- - The Docker API must be exposed over TCP. You can find more information about how to expose the Docker API over TCP - in the Docker documentation. -

-
-
-
-
- Environment -
- -
- -
- -
-
- - -
- -
- -
-
- - -
-
- - -
-
- - -
- -
-
- - -
-
- - -
-
- - -
-
- -
- Required TLS files -
- -
- -
- - - {{ formValues.TLSCACert.name }} - - - -
-
- -
- -
- -
- - - {{ formValues.TLSCert.name }} - - - -
-
- - -
- -
- - - {{ formValues.TLSKey.name }} - - - -
-
- -
-
- - -
-
- -
-
- +
+
- +
+ +
+ + +
+
+ +
+
+
diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index fc4ff905a..ca2d94ca3 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -1,109 +1,223 @@ import _ from 'lodash-es'; +import angular from 'angular'; +import { PortainerEndpointInitFormValues, PortainerEndpointInitFormValueEndpointSections } from 'Portainer/models/endpoint/formValues'; +import { PortainerEndpointTypes, PortainerEndpointConnectionTypes } from 'Portainer/models/endpoint/models'; -angular.module('portainer.app').controller('InitEndpointController', [ - '$scope', - '$state', - 'EndpointService', - 'StateManager', - 'Notifications', - function ($scope, $state, EndpointService, StateManager, Notifications) { - if (!_.isEmpty($scope.applicationState.endpoint)) { - $state.go('portainer.home'); +require('./includes/localDocker.html'); +require('./includes/localKubernetes.html'); +require('./includes/remote.html'); +require('./includes/azure.html'); +require('./includes/agent.html'); + +class InitEndpointController { + /* @ngInject */ + constructor($async, $scope, $state, EndpointService, EndpointProvider, StateManager, Notifications) { + this.$async = $async; + this.$scope = $scope; + this.$state = $state; + this.EndpointService = EndpointService; + this.EndpointProvider = EndpointProvider; + this.StateManager = StateManager; + this.Notifications = Notifications; + + this.createLocalEndpointAsync = this.createLocalEndpointAsync.bind(this); + this.createLocalKubernetesEndpointAsync = this.createLocalKubernetesEndpointAsync.bind(this); + this.createAgentEndpointAsync = this.createAgentEndpointAsync.bind(this); + this.createAzureEndpointAsync = this.createAzureEndpointAsync.bind(this); + this.createRemoteEndpointAsync = this.createRemoteEndpointAsync.bind(this); + } + + $onInit() { + if (!_.isEmpty(this.$scope.applicationState.endpoint)) { + this.$state.go('portainer.home'); } + this.logo = this.StateManager.getState().application.logo; - $scope.logo = StateManager.getState().application.logo; - - $scope.state = { + this.state = { uploadInProgress: false, actionInProgress: false, }; - $scope.formValues = { - EndpointType: 'remote', - Name: '', - URL: '', - TLS: false, - TLSSkipVerify: false, - TLSSKipClientVerify: false, - TLSCACert: null, - TLSCert: null, - TLSKey: null, - AzureApplicationId: '', - AzureTenantId: '', - AzureAuthenticationKey: '', - }; + this.formValues = new PortainerEndpointInitFormValues(); + this.endpointSections = PortainerEndpointInitFormValueEndpointSections; + this.PortainerEndpointConnectionTypes = PortainerEndpointConnectionTypes; + } - $scope.createLocalEndpoint = function () { - $scope.state.actionInProgress = true; - EndpointService.createLocalEndpoint() - .then(function success() { - $state.go('portainer.home'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; + isRemoteConnectButtonDisabled() { + return ( + this.state.actionInProgress || + !this.formValues.Name || + !this.formValues.URL || + (this.formValues.TLS && + ((this.formValues.TLSVerify && !this.formValues.TLSCACert) || (!this.formValues.TLSSKipClientVerify && (!this.formValues.TLSCert || !this.formValues.TLSKey)))) + ); + } - $scope.createAzureEndpoint = function () { - var name = $scope.formValues.Name; - var applicationId = $scope.formValues.AzureApplicationId; - var tenantId = $scope.formValues.AzureTenantId; - var authenticationKey = $scope.formValues.AzureAuthenticationKey; + isAzureConnectButtonDisabled() { + return this.state.actionInProgress || !this.formValues.Name || !this.formValues.AzureApplicationId || !this.formValues.AzureTenantId || !this.formValues.AzureAuthenticationKey; + } - createAzureEndpoint(name, applicationId, tenantId, authenticationKey); - }; - - $scope.createAgentEndpoint = function () { - var name = $scope.formValues.Name; - var URL = $scope.formValues.URL; - var PublicURL = URL.split(':')[0]; - - createRemoteEndpoint(name, 2, URL, PublicURL, true, true, true, null, null, null); - }; - - $scope.createRemoteEndpoint = function () { - var name = $scope.formValues.Name; - var URL = $scope.formValues.URL; - var PublicURL = URL.split(':')[0]; - var TLS = $scope.formValues.TLS; - var TLSSkipVerify = TLS && $scope.formValues.TLSSkipVerify; - var TLSSKipClientVerify = TLS && $scope.formValues.TLSSKipClientVerify; - var TLSCAFile = TLSSkipVerify ? null : $scope.formValues.TLSCACert; - var TLSCertFile = TLSSKipClientVerify ? null : $scope.formValues.TLSCert; - var TLSKeyFile = TLSSKipClientVerify ? null : $scope.formValues.TLSKey; - - createRemoteEndpoint(name, 1, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); - }; - - function createAzureEndpoint(name, applicationId, tenantId, authenticationKey) { - $scope.state.actionInProgress = true; - EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, []) - .then(function success() { - $state.go('portainer.home'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Azure environment'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); + isConnectButtonDisabled() { + switch (this.formValues.ConnectionType) { + case PortainerEndpointConnectionTypes.DOCKER_LOCAL: + return this.state.actionInProgress; + case PortainerEndpointConnectionTypes.KUBERNETES_LOCAL: + return this.state.actionInProgress; + case PortainerEndpointConnectionTypes.REMOTE: + return this.isRemoteConnectButtonDisabled(); + case PortainerEndpointConnectionTypes.AZURE: + return this.isAzureConnectButtonDisabled(); + case PortainerEndpointConnectionTypes.AGENT: + return this.state.actionInProgress || !this.formValues.Name || !this.formValues.URL; + default: + break; } + } - function createRemoteEndpoint(name, type, URL, PublicURL, TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { - $scope.state.actionInProgress = true; - EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) - .then(function success() { - $state.go('portainer.home'); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); + createEndpoint() { + switch (this.formValues.ConnectionType) { + case PortainerEndpointConnectionTypes.DOCKER_LOCAL: + return this.createLocalEndpoint(); + case PortainerEndpointConnectionTypes.KUBERNETES_LOCAL: + return this.createLocalKubernetesEndpoint(); + case PortainerEndpointConnectionTypes.REMOTE: + return this.createRemoteEndpoint(); + case PortainerEndpointConnectionTypes.AZURE: + return this.createAzureEndpoint(); + case PortainerEndpointConnectionTypes.AGENT: + return this.createAgentEndpoint(); + default: + this.Notifications.error('Failure', 'Unable to determine wich action to do'); } - }, -]); + } + + /** + * DOCKER_LOCAL (1) + */ + async createLocalEndpointAsync() { + try { + this.state.actionInProgress = true; + await this.EndpointService.createLocalEndpoint(); + this.$state.go('portainer.home'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createLocalEndpoint() { + return this.$async(this.createLocalEndpointAsync); + } + + /** + * KUBERNETES_LOCAL (5) + */ + async createLocalKubernetesEndpointAsync() { + try { + this.state.actionInProgress = true; + const endpoint = await this.EndpointService.createLocalKubernetesEndpoint(); + this.$state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: endpoint.Id }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Kubernetes environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createLocalKubernetesEndpoint() { + return this.$async(this.createLocalKubernetesEndpointAsync); + } + + /** + * DOCKER / KUBERNETES AGENT (2 / 6) + */ + async createAgentEndpointAsync() { + try { + this.state.actionInProgress = true; + const name = this.formValues.Name; + const URL = this.formValues.URL; + const PublicURL = URL.split(':')[0]; + // TODO: k8s merge - change type ID for agent on kube (6) or agent on swarm (2) + const endpoint = await this.EndpointService.createRemoteEndpoint( + name, + PortainerEndpointTypes.AgentOnKubernetesEnvironment, + URL, + PublicURL, + 1, + [], + true, + true, + true, + null, + null, + null + ); + // TODO: k8s merge - go on home whith agent on swarm (2) + this.$state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: endpoint.Id }); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createAgentEndpoint() { + return this.$async(this.createAgentEndpointAsync); + } + + /** + * DOCKER REMOTE (1) + */ + async createRemoteEndpointAsync() { + try { + this.state.actionInProgress = true; + const name = this.formValues.Name; + const type = PortainerEndpointTypes.DockerEnvironment; + const URL = this.formValues.URL; + const PublicURL = URL.split(':')[0]; + const TLS = this.formValues.TLS; + const TLSSkipVerify = TLS && this.formValues.TLSSkipVerify; + const TLSSKipClientVerify = TLS && this.formValues.TLSSKipClientVerify; + const TLSCAFile = TLSSkipVerify ? null : this.formValues.TLSCACert; + const TLSCertFile = TLSSKipClientVerify ? null : this.formValues.TLSCert; + const TLSKeyFile = TLSSKipClientVerify ? null : this.formValues.TLSKey; + await this.EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); + this.$state.go('portainer.home'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createRemoteEndpoint() { + return this.$async(this.createAgentEndpointAsync); + } + + /** + * AZURE (4) + */ + async createAzureEndpointAsync() { + try { + this.state.actionInProgress = true; + var name = this.formValues.Name; + var applicationId = this.formValues.AzureApplicationId; + var tenantId = this.formValues.AzureTenantId; + var authenticationKey = this.formValues.AzureAuthenticationKey; + await this.EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, []); + this.$state.go('portainer.home'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to connect to the Azure environment'); + } finally { + this.state.actionInProgress = false; + } + } + + createAzureEndpoint() { + return this.$async(this.createAgentEndpointAsync); + } +} + +export default InitEndpointController; +angular.module('portainer.app').controller('InitEndpointController', InitEndpointController); diff --git a/app/portainer/views/logout/logout.html b/app/portainer/views/logout/logout.html new file mode 100644 index 000000000..60311e9da --- /dev/null +++ b/app/portainer/views/logout/logout.html @@ -0,0 +1,16 @@ +
+ +
+
+ +
+ + +
+
+ Logout in progress... + +
+
+
+
diff --git a/app/portainer/views/logout/logoutController.js b/app/portainer/views/logout/logoutController.js new file mode 100644 index 000000000..21d51bc84 --- /dev/null +++ b/app/portainer/views/logout/logoutController.js @@ -0,0 +1,61 @@ +import angular from 'angular'; + +class LogoutController { + /* @ngInject */ + constructor($async, $state, $transition$, Authentication, StateManager, Notifications, LocalStorage) { + this.$async = $async; + this.$state = $state; + this.$transition$ = $transition$; + + this.Authentication = Authentication; + this.StateManager = StateManager; + this.Notifications = Notifications; + this.LocalStorage = LocalStorage; + + this.logo = this.StateManager.getState().application.logo; + this.logoutAsync = this.logoutAsync.bind(this); + + this.onInit = this.onInit.bind(this); + } + + /** + * UTILS FUNCTIONS SECTION + */ + async logoutAsync() { + const error = this.$transition$.params().error; + const performApiLogout = this.$transition$.params().performApiLogout; + try { + await this.Authentication.logout(performApiLogout); + } finally { + this.LocalStorage.storeLogoutReason(error); + this.$state.go('portainer.auth', { reload: true }); + } + } + + logout() { + return this.$async(this.logoutAsync); + } + + /** + * END UTILS FUNCTIONS SECTION + */ + + async onInit() { + try { + await this.logout(); + } catch (err) { + this.Notifications.error('Failure', err, 'An error occured during logout'); + } + } + + $onInit() { + return this.$async(this.onInit); + } + + /** + * END ON INIT SECTION + */ +} + +export default LogoutController; +angular.module('portainer.app').controller('LogoutController', LogoutController); diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 18f8d5df9..a8a5ea1de 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -13,9 +13,11 @@ Home + +
- +
@@ -123,7 +123,7 @@ >
- +
diff --git a/app/vendors.js b/app/vendors.js index f4d8ce6a1..db03b38b4 100644 --- a/app/vendors.js +++ b/app/vendors.js @@ -12,9 +12,9 @@ import 'angular-json-tree/dist/angular-json-tree.css'; import 'angular-loading-bar/build/loading-bar.css'; import 'angular-moment-picker/dist/angular-moment-picker.min.css'; import 'angular-multiselect/isteven-multi-select.css'; +import 'spinkit/spinkit.min.css'; import angular from 'angular'; -window.angular = angular; import 'moment'; import '@uirouter/angularjs'; import 'ui-select'; @@ -38,3 +38,5 @@ import 'js-yaml/dist/js-yaml.js'; import 'angular-ui-bootstrap'; import 'angular-moment-picker'; import 'angular-multiselect/isteven-multi-select.js'; + +window.angular = angular; diff --git a/build/download_kompose_binary.sh b/build/download_kompose_binary.sh new file mode 100755 index 000000000..b5021e70d --- /dev/null +++ b/build/download_kompose_binary.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +PLATFORM=$1 +ARCH=$2 +KOMPOSE_VERSION=$3 + +wget -O "dist/kompose" "https://github.com/kubernetes/kompose/releases/download/${KOMPOSE_VERSION}/kompose-${PLATFORM}-${ARCH}" +chmod +x "dist/kompose" + +exit 0 diff --git a/build/download_kubectl_binary.sh b/build/download_kubectl_binary.sh new file mode 100755 index 000000000..b9dd72f73 --- /dev/null +++ b/build/download_kubectl_binary.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +PLATFORM=$1 +ARCH=$2 +KUBECTL_VERSION=$3 + +wget -O "dist/kubectl" "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/${PLATFORM}/${ARCH}/kubectl" +chmod +x "dist/kubectl" + +exit 0 diff --git a/gruntfile.js b/gruntfile.js index bd7855302..919fc6d93 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -16,8 +16,12 @@ module.exports = function (grunt) { grunt.initConfig({ root: 'dist', distdir: 'dist/public', - shippedDockerVersion: '18.09.3', - shippedDockerVersionWindows: '17.09.0-ce', + binaries: { + dockerLinuxVersion: '18.09.3', + dockerWindowsVersion: '17.09.0-ce', + komposeVersion: 'v1.21.0', + kubectlVersion: 'v1.18.0', + }, config: gruntfile_cfg.config, env: gruntfile_cfg.env, src: gruntfile_cfg.src, @@ -30,7 +34,12 @@ module.exports = function (grunt) { grunt.registerTask('lint', ['eslint']); - grunt.registerTask('build:server', ['shell:build_binary:linux:' + arch, 'shell:download_docker_binary:linux:' + arch]); + grunt.registerTask('build:server', [ + 'shell:build_binary:linux:' + arch, + 'shell:download_docker_binary:linux:' + arch, + 'shell:download_kompose_binary:linux:' + arch, + 'shell:download_kubectl_binary:linux:' + arch, + ]); grunt.registerTask('build:client', ['config:dev', 'env:dev', 'webpack:dev']); @@ -47,7 +56,17 @@ module.exports = function (grunt) { grunt.registerTask('start:toolkit', ['start:localserver', 'start:client']); grunt.task.registerTask('release', 'release::', function (p = 'linux', a = arch) { - grunt.task.run(['config:prod', 'env:prod', 'clean:all', 'copy:assets', 'shell:build_binary:' + p + ':' + a, 'shell:download_docker_binary:' + p + ':' + a, 'webpack:prod']); + grunt.task.run([ + 'config:prod', + 'env:prod', + 'clean:all', + 'copy:assets', + 'shell:build_binary:' + p + ':' + a, + 'shell:download_docker_binary:' + p + ':' + a, + 'shell:download_kompose_binary:' + p + ':' + a, + 'shell:download_kubectl_binary:' + p + ':' + a, + 'webpack:prod', + ]); }); grunt.task.registerTask('devopsbuild', 'devopsbuild::', function (p, a) { @@ -58,6 +77,8 @@ module.exports = function (grunt) { 'copy:assets', 'shell:build_binary_azuredevops:' + p + ':' + a, 'shell:download_docker_binary:' + p + ':' + a, + 'shell:download_kompose_binary:' + p + ':' + a, + 'shell:download_kubectl_binary:' + p + ':' + a, 'webpack:prod', ]); }); @@ -97,7 +118,6 @@ gruntfile_cfg.src = { gruntfile_cfg.clean = { server: ['<%= root %>/portainer'], client: ['<%= distdir %>/*'], - docker: ['<%= root %>/docker'], all: ['<%= root %>/*'], }; @@ -122,6 +142,8 @@ gruntfile_cfg.shell = { build_binary: { command: shell_build_binary }, build_binary_azuredevops: { command: shell_build_binary_azuredevops }, download_docker_binary: { command: shell_download_docker_binary }, + download_kompose_binary: { command: shell_download_kompose_binary }, + download_kubectl_binary: { command: shell_download_kubectl_binary }, run_container: { command: shell_run_container }, run_localserver: { command: shell_run_localserver, options: { async: true } }, install_yarndeps: { command: shell_install_yarndeps }, @@ -172,9 +194,10 @@ function shell_download_docker_binary(p, a) { var as = { amd64: 'x86_64', arm: 'armhf', arm64: 'aarch64' }; var ip = ps[p] === undefined ? p : ps[p]; var ia = as[a] === undefined ? a : as[a]; - var binaryVersion = p === 'windows' ? '<%= shippedDockerVersionWindows %>' : '<%= shippedDockerVersion %>'; + var binaryVersion = p === 'windows' ? '<%= binaries.dockerWindowsVersion %>' : '<%= binaries.dockerLinuxVersion %>'; + if (p === 'linux' || p === 'mac') { - return ['if [ -f dist/docker ]; then', 'echo "Docker binary exists";', 'else', 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'fi'].join(' '); + return ['if [ -f dist/docker ]; then', 'echo "docker binary exists";', 'else', 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'fi'].join(' '); } else { return [ 'powershell -Command "& {if (Get-Item -Path dist/docker.exe -ErrorAction:SilentlyContinue) {', @@ -185,3 +208,35 @@ function shell_download_docker_binary(p, a) { ].join(' '); } } + +function shell_download_kompose_binary(p, a) { + var binaryVersion = '<%= binaries.komposeVersion %>'; + + if (p === 'linux' || p === 'darwin') { + return ['if [ -f dist/kompose ]; then', 'echo "kompose binary exists";', 'else', 'build/download_kompose_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' '); + } else { + return [ + 'powershell -Command "& {if (Get-Item -Path dist/kompose.exe -ErrorAction:SilentlyContinue) {', + 'Write-Host "Docker binary exists"', + '} else {', + '& ".\\build\\download_kompose_binary.ps1" -docker_version ' + binaryVersion + '', + '}}"', + ].join(' '); + } +} + +function shell_download_kubectl_binary(p, a) { + var binaryVersion = '<%= binaries.kubectlVersion %>'; + + if (p === 'linux' || p === 'darwin') { + return ['if [ -f dist/kubectl ]; then', 'echo "kubectl binary exists";', 'else', 'build/download_kubectl_binary.sh ' + p + ' ' + a + ' ' + binaryVersion + ';', 'fi'].join(' '); + } else { + return [ + 'powershell -Command "& {if (Get-Item -Path dist/kubectl.exe -ErrorAction:SilentlyContinue) {', + 'Write-Host "Docker binary exists"', + '} else {', + '& ".\\build\\download_kubectl_binary.ps1" -docker_version ' + binaryVersion + '', + '}}"', + ].join(' '); + } +} diff --git a/jsconfig.json b/jsconfig.json index e78006415..c1b062209 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -8,6 +8,7 @@ "Azure/*": ["azure/*"], "Docker/*": ["docker/*"], "Extensions/*": ["extensions/*"], + "Kubernetes/*": ["kubernetes/*"], "Portainer/*": ["portainer/*"] } }, diff --git a/package.json b/package.json index f663fd108..293fd6c14 100644 --- a/package.json +++ b/package.json @@ -77,17 +77,22 @@ "bootstrap": "^3.4.0", "chart.js": "~2.6.0", "codemirror": "~5.30.0", + "fast-json-patch": "^3.0.0-1", "filesize": "~3.3.0", + "filesize-parser": "^1.5.0", "jquery": "^3.5.1", "js-yaml": "^3.14.0", "lodash-es": "^4.17.15", "moment": "^2.21.0", "ng-file-upload": "~12.2.13", + "spinkit": "^2.0.1", "splitargs": "github:deviantony/splitargs#semver:~0.2.0", + "strip-ansi": "^6.0.0", "toastr": "^2.1.4", "ui-select": "^0.19.8", "uuid": "^3.3.2", - "xterm": "^3.8.0" + "xterm": "^3.8.0", + "yaml": "^1.10.0" }, "devDependencies": { "@babel/core": "^7.1.2", @@ -158,4 +163,4 @@ "*.js": "eslint --cache --fix", "*.{js,css,md,html}": "prettier --write" } -} +} \ No newline at end of file diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index 3603afe87..f0258a201 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -114,6 +114,7 @@ module.exports = { Agent: path.resolve(projectRoot, 'app/agent'), Azure: path.resolve(projectRoot, 'app/azure'), Docker: path.resolve(projectRoot, 'app/docker'), + Kubernetes: path.resolve(projectRoot, 'app/kubernetes'), Extensions: path.resolve(projectRoot, 'app/extensions'), Portainer: path.resolve(projectRoot, 'app/portainer'), '@': path.resolve(projectRoot, 'app'), diff --git a/yarn.lock b/yarn.lock index d46f49040..77b495a16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,35 +2,35 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" - integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.3.tgz#324bcfd8d35cd3d47dae18cde63d752086435e9a" + integrity sha512-fDx9eNW0qz0WkUeqL6tXEXzVlPh6Y5aCDEZesl0xBGA8ndRukX91Uk44ZqnkECp01NAZUdCAl+aiQNGi0k88Eg== dependencies: - "@babel/highlight" "^7.8.3" + "@babel/highlight" "^7.10.3" -"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c" - integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g== +"@babel/compat-data@^7.10.1", "@babel/compat-data@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.3.tgz#9af3e033f36e8e2d6e47570db91e64a846f5d382" + integrity sha512-BDIfJ9uNZuI0LajPfoYV28lX8kyCPMHY6uY4WH1lJdcicmAfxCK5ASzaeV0D/wsUaRH/cLk+amuxtC37sZ8TUg== dependencies: - browserslist "^4.9.1" + browserslist "^4.12.0" invariant "^2.2.4" semver "^5.5.0" "@babel/core@^7.1.2": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" - integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.3.tgz#73b0e8ddeec1e3fdd7a2de587a60e17c440ec77e" + integrity sha512-5YqWxYE3pyhIi84L84YcwjeEgS+fa7ZjK6IBVGTjDVfm64njkR2lfDhVR5OudLk8x2GK59YoSyVv+L/03k1q9w== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.0" - "@babel/parser" "^7.9.0" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/code-frame" "^7.10.3" + "@babel/generator" "^7.10.3" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helpers" "^7.10.1" + "@babel/parser" "^7.10.3" + "@babel/template" "^7.10.3" + "@babel/traverse" "^7.10.3" + "@babel/types" "^7.10.3" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -40,283 +40,312 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce" - integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA== +"@babel/generator@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.3.tgz#32b9a0d963a71d7a54f5f6c15659c3dbc2a523a5" + integrity sha512-drt8MUHbEqRzNR0xnF8nMehbY11b1SDkRw03PSNH/3Rb2Z35oxkddVSi3rcaak0YJQ86PCuE7Qx1jSFhbLNBMA== dependencies: - "@babel/types" "^7.9.0" + "@babel/types" "^7.10.3" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" - integrity sha512-6o+mJrZBxOoEX77Ezv9zwW7WV8DdluouRKNY/IR5u/YTMuKHgugHOzYWlYvYLpLA9nPsQCAAASpCIbjI9Mv+Uw== +"@babel/helper-annotate-as-pure@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.1.tgz#f6d08acc6f70bbd59b436262553fb2e259a1a268" + integrity sha512-ewp3rvJEwLaHgyWGe4wQssC2vjks3E80WiUe2BpMb0KhreTjMROCbxXcEovTrbeGVdQct5VjQfrv9EgC+xMzCw== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.1" -"@babel/helper-builder-binary-assignment-operator-visitor@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.8.3.tgz#c84097a427a061ac56a1c30ebf54b7b22d241503" - integrity sha512-5eFOm2SyFPK4Rh3XMMRDjN7lBH0orh3ss0g3rTYZnBQ+r6YPj7lgDyCvPphynHvUrobJmeMignBr6Acw9mAPlw== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.10.1": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.3.tgz#4e9012d6701bef0030348d7f9c808209bd3e8687" + integrity sha512-lo4XXRnBlU6eRM92FkiZxpo1xFLmv3VsPFk61zJKMm7XYJfwqXHsYJTY6agoc4a3L8QPw1HqWehO18coZgbT6A== dependencies: - "@babel/helper-explode-assignable-expression" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-explode-assignable-expression" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/helper-compilation-targets@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde" - integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw== +"@babel/helper-compilation-targets@^7.10.2": + version "7.10.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.2.tgz#a17d9723b6e2c750299d2a14d4637c76936d8285" + integrity sha512-hYgOhF4To2UTB4LTaZepN/4Pl9LD4gfbJx8A34mqoluT8TLbof1mhUlYuNWTEebONa8+UlCC4X0TEXu7AOUyGA== dependencies: - "@babel/compat-data" "^7.8.6" - browserslist "^4.9.1" + "@babel/compat-data" "^7.10.1" + browserslist "^4.12.0" invariant "^2.2.4" levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" - integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg== +"@babel/helper-create-class-features-plugin@^7.10.1": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.3.tgz#2783daa6866822e3d5ed119163b50f0fc3ae4b35" + integrity sha512-iRT9VwqtdFmv7UheJWthGc/h2s7MqoweBF9RUj77NFZsg9VfISvBTum3k6coAhJ8RWv2tj3yUjA03HxPd0vfpQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-regex" "^7.8.3" + "@babel/helper-function-name" "^7.10.3" + "@babel/helper-member-expression-to-functions" "^7.10.3" + "@babel/helper-optimise-call-expression" "^7.10.3" + "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-split-export-declaration" "^7.10.1" + +"@babel/helper-create-regexp-features-plugin@^7.10.1", "@babel/helper-create-regexp-features-plugin@^7.8.3": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.1.tgz#1b8feeab1594cbcfbf3ab5a3bbcabac0468efdbd" + integrity sha512-Rx4rHS0pVuJn5pJOqaqcZR4XSgeF9G/pO/79t+4r7380tXFJdzImFnxMU19f83wjSrmKHq6myrM10pFHTGzkUA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.10.1" + "@babel/helper-regex" "^7.10.1" regexpu-core "^4.7.0" -"@babel/helper-define-map@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15" - integrity sha512-PoeBYtxoZGtct3md6xZOCWPcKuMuk3IHhgxsRRNtnNShebf4C8YonTSblsK4tvDbm+eJAw2HAPOfCr+Q/YRG/g== +"@babel/helper-define-map@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.10.3.tgz#d27120a5e57c84727b30944549b2dfeca62401a8" + integrity sha512-bxRzDi4Sin/k0drWCczppOhov1sBSdBvXJObM1NLHQzjhXhwRtn7aRWGvLJWCYbuu2qUk3EKs6Ci9C9ps8XokQ== dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-function-name" "^7.10.3" + "@babel/types" "^7.10.3" lodash "^4.17.13" -"@babel/helper-explode-assignable-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.8.3.tgz#a728dc5b4e89e30fc2dfc7d04fa28a930653f982" - integrity sha512-N+8eW86/Kj147bO9G2uclsg5pwfs/fqqY5rwgIL7eTBklgXjcOJ3btzS5iM6AitJcftnY7pm2lGsrJVYLGjzIw== +"@babel/helper-explode-assignable-expression@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.3.tgz#9dc14f0cfa2833ea830a9c8a1c742b6e7461b05e" + integrity sha512-0nKcR64XrOC3lsl+uhD15cwxPvaB6QKUDlD84OT9C3myRbhJqTMYir69/RWItUvHpharv0eJ/wk7fl34ONSwZw== dependencies: - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/traverse" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/helper-function-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" - integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== +"@babel/helper-function-name@^7.10.1", "@babel/helper-function-name@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.3.tgz#79316cd75a9fa25ba9787ff54544307ed444f197" + integrity sha512-FvSj2aiOd8zbeqijjgqdMDSyxsGHaMt5Tr0XjQsGKHD3/1FP3wksjnLAWzxw7lvXiej8W1Jt47SKTZ6upQNiRw== dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-get-function-arity" "^7.10.3" + "@babel/template" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/helper-get-function-arity@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" - integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== +"@babel/helper-get-function-arity@^7.10.1", "@babel/helper-get-function-arity@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.3.tgz#3a28f7b28ccc7719eacd9223b659fdf162e4c45e" + integrity sha512-iUD/gFsR+M6uiy69JA6fzM5seno8oE85IYZdbVVEuQaZlEzMO2MXblh+KSPJgsZAUx0EEbWXU0yJaW7C9CdAVg== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-hoist-variables@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.8.3.tgz#1dbe9b6b55d78c9b4183fc8cdc6e30ceb83b7134" - integrity sha512-ky1JLOjcDUtSc+xkt0xhYff7Z6ILTAHKmZLHPxAhOP0Nd77O+3nCsd6uSVYur6nJnCI029CrNbYlc0LoPfAPQg== +"@babel/helper-hoist-variables@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.3.tgz#d554f52baf1657ffbd7e5137311abc993bb3f068" + integrity sha512-9JyafKoBt5h20Yv1+BXQMdcXXavozI1vt401KBiRc2qzUepbVnd7ogVNymY1xkQN9fekGwfxtotH2Yf5xsGzgg== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-member-expression-to-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" - integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA== +"@babel/helper-member-expression-to-functions@^7.10.1", "@babel/helper-member-expression-to-functions@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.3.tgz#bc3663ac81ac57c39148fef4c69bf48a77ba8dd6" + integrity sha512-q7+37c4EPLSjNb2NmWOjNwj0+BOyYlssuQ58kHEWk1Z78K5i8vTUsteq78HMieRPQSl/NtpQyJfdjt3qZ5V2vw== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-module-imports@^7.0.0-beta.49", "@babel/helper-module-imports@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" - integrity sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg== +"@babel/helper-module-imports@^7.0.0-beta.49", "@babel/helper-module-imports@^7.10.1", "@babel/helper-module-imports@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.3.tgz#766fa1d57608e53e5676f23ae498ec7a95e1b11a" + integrity sha512-Jtqw5M9pahLSUWA+76nhK9OG8nwYXzhQzVIGFoNaHnXF/r4l7kz4Fl0UAW7B6mqC5myoJiBP5/YQlXQTMfHI9w== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-module-transforms@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5" - integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA== +"@babel/helper-module-transforms@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.1.tgz#24e2f08ee6832c60b157bb0936c86bef7210c622" + integrity sha512-RLHRCAzyJe7Q7sF4oy2cB+kRnU4wDZY/H2xJFGof+M+SJEGhZsb+GFj5j1AD8NiSaVBJ+Pf0/WObiXu/zxWpFg== dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-simple-access" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/template" "^7.8.6" - "@babel/types" "^7.9.0" + "@babel/helper-module-imports" "^7.10.1" + "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-simple-access" "^7.10.1" + "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/template" "^7.10.1" + "@babel/types" "^7.10.1" lodash "^4.17.13" -"@babel/helper-optimise-call-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" - integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ== +"@babel/helper-optimise-call-expression@^7.10.1", "@babel/helper-optimise-call-expression@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.3.tgz#f53c4b6783093195b0f69330439908841660c530" + integrity sha512-kT2R3VBH/cnSz+yChKpaKRJQJWxdGoc6SjioRId2wkeV3bK0wLLioFpJROrX0U4xr/NmxSSAWT/9Ih5snwIIzg== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.3" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" - integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.1", "@babel/helper-plugin-utils@^7.10.3", "@babel/helper-plugin-utils@^7.8.0": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.3.tgz#aac45cccf8bc1873b99a85f34bceef3beb5d3244" + integrity sha512-j/+j8NAWUTxOtx4LKHybpSClxHoq6I91DQ/mKgAXn5oNUPIUiGppjPIX3TDtJWPrdfP9Kfl7e4fgVMiQR9VE/g== -"@babel/helper-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.8.3.tgz#139772607d51b93f23effe72105b319d2a4c6965" - integrity sha512-BWt0QtYv/cg/NecOAZMdcn/waj/5P26DR4mVLXfFtDokSR6fyuG0Pj+e2FqtSME+MqED1khnSMulkmGl8qWiUQ== +"@babel/helper-regex@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.1.tgz#021cf1a7ba99822f993222a001cc3fec83255b96" + integrity sha512-7isHr19RsIJWWLLFn21ubFt223PjQyg1HY7CZEMRr820HttHPpVvrsIN3bUOo44DEfFV4kBXO7Abbn9KTUZV7g== dependencies: lodash "^4.17.13" -"@babel/helper-remap-async-to-generator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.8.3.tgz#273c600d8b9bf5006142c1e35887d555c12edd86" - integrity sha512-kgwDmw4fCg7AVgS4DukQR/roGp+jP+XluJE5hsRZwxCYGg+Rv9wSGErDWhlI90FODdYfd4xG4AQRiMDjjN0GzA== +"@babel/helper-remap-async-to-generator@^7.10.1", "@babel/helper-remap-async-to-generator@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.10.3.tgz#18564f8a6748be466970195b876e8bba3bccf442" + integrity sha512-sLB7666ARbJUGDO60ZormmhQOyqMX/shKBXZ7fy937s+3ID8gSrneMvKSSb+8xIM5V7Vn6uNVtOY1vIm26XLtA== dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-wrap-function" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-annotate-as-pure" "^7.10.1" + "@babel/helper-wrap-function" "^7.10.1" + "@babel/template" "^7.10.3" + "@babel/traverse" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" - integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== +"@babel/helper-replace-supers@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.1.tgz#ec6859d20c5d8087f6a2dc4e014db7228975f13d" + integrity sha512-SOwJzEfpuQwInzzQJGjGaiG578UYmyi2Xw668klPWV5n07B73S0a9btjLk/52Mlcxa+5AdIYqws1KyXRfMoB7A== dependencies: - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" + "@babel/helper-member-expression-to-functions" "^7.10.1" + "@babel/helper-optimise-call-expression" "^7.10.1" + "@babel/traverse" "^7.10.1" + "@babel/types" "^7.10.1" -"@babel/helper-simple-access@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae" - integrity sha512-VNGUDjx5cCWg4vvCTR8qQ7YJYZ+HBjxOgXEl7ounz+4Sn7+LMD3CFrCTEU6/qXKbA2nKg21CwhhBzO0RpRbdCw== +"@babel/helper-simple-access@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.1.tgz#08fb7e22ace9eb8326f7e3920a1c2052f13d851e" + integrity sha512-VSWpWzRzn9VtgMJBIWTZ+GP107kZdQ4YplJlCmIrjoLVSi/0upixezHCDG8kpPVTBJpKfxTH01wDhh+jS2zKbw== dependencies: - "@babel/template" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/template" "^7.10.1" + "@babel/types" "^7.10.1" -"@babel/helper-split-export-declaration@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" - integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== +"@babel/helper-split-export-declaration@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.1.tgz#c6f4be1cbc15e3a868e4c64a17d5d31d754da35f" + integrity sha512-UQ1LVBPrYdbchNhLwj6fetj46BcFwfS4NllJo/1aJsT+1dLTEnXJL0qHqtY7gPzF8S2fXBJamf1biAXV3X077g== dependencies: - "@babel/types" "^7.8.3" + "@babel/types" "^7.10.1" -"@babel/helper-validator-identifier@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed" - integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw== +"@babel/helper-validator-identifier@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.3.tgz#60d9847f98c4cea1b279e005fdb7c28be5412d15" + integrity sha512-bU8JvtlYpJSBPuj1VUmKpFGaDZuLxASky3LhaKj3bmpSTY6VWooSM8msk+Z0CZoErFye2tlABF6yDkT3FOPAXw== -"@babel/helper-wrap-function@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610" - integrity sha512-LACJrbUET9cQDzb6kG7EeD7+7doC3JNvUgTEQOx2qaO1fKlzE/Bf05qs9w1oXQMmXlPO65lC3Tq9S6gZpTErEQ== +"@babel/helper-wrap-function@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.10.1.tgz#956d1310d6696257a7afd47e4c42dfda5dfcedc9" + integrity sha512-C0MzRGteVDn+H32/ZgbAv5r56f2o1fZSA/rj/TYo8JEJNHg+9BdSmKBUND0shxWRztWhjlT2cvHYuynpPsVJwQ== dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.8.3" - "@babel/types" "^7.8.3" + "@babel/helper-function-name" "^7.10.1" + "@babel/template" "^7.10.1" + "@babel/traverse" "^7.10.1" + "@babel/types" "^7.10.1" -"@babel/helpers@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f" - integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA== +"@babel/helpers@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.1.tgz#a6827b7cb975c9d9cef5fd61d919f60d8844a973" + integrity sha512-muQNHF+IdU6wGgkaJyhhEmI54MOZBKsFfsXFhboz1ybwJ1Kl7IHlbm2a++4jwrmY5UYsgitt5lfqo1wMFcHmyw== dependencies: - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/template" "^7.10.1" + "@babel/traverse" "^7.10.1" + "@babel/types" "^7.10.1" -"@babel/highlight@^7.8.3": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079" - integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ== +"@babel/highlight@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.3.tgz#c633bb34adf07c5c13156692f5922c81ec53f28d" + integrity sha512-Ih9B/u7AtgEnySE2L2F0Xm0GaM729XqqLfHkalTsbjXGyqmf/6M0Cu0WpvqueUlW+xk88BHw9Nkpj49naU+vWw== dependencies: - "@babel/helper-validator-identifier" "^7.9.0" + "@babel/helper-validator-identifier" "^7.10.3" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.8.6", "@babel/parser@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" - integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== +"@babel/parser@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.3.tgz#7e71d892b0d6e7d04a1af4c3c79d72c1f10f5315" + integrity sha512-oJtNJCMFdIMwXGmx+KxuaD7i3b8uS7TTFYW/FNG2BT8m+fmGHoiPYoH0Pe3gya07WuFmM5FCDIr1x0irkD/hyA== -"@babel/plugin-proposal-async-generator-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" - integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw== +"@babel/plugin-proposal-async-generator-functions@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.10.3.tgz#5a02453d46e5362e2073c7278beab2e53ad7d939" + integrity sha512-WUUWM7YTOudF4jZBAJIW9D7aViYC/Fn0Pln4RIHlQALyno3sXSjqmTA4Zy1TKC2D49RCR8Y/Pn4OIUtEypK3CA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-remap-async-to-generator" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-remap-async-to-generator" "^7.10.3" "@babel/plugin-syntax-async-generators" "^7.8.0" -"@babel/plugin-proposal-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054" - integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w== +"@babel/plugin-proposal-class-properties@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.1.tgz#046bc7f6550bb08d9bd1d4f060f5f5a4f1087e01" + integrity sha512-sqdGWgoXlnOdgMXU+9MbhzwFRgxVLeiGBqTrnuS7LC2IBU31wSsESbTUreT2O418obpfPdGUR2GbEufZF1bpqw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-create-class-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + +"@babel/plugin-proposal-dynamic-import@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.10.1.tgz#e36979dc1dc3b73f6d6816fc4951da2363488ef0" + integrity sha512-Cpc2yUVHTEGPlmiQzXj026kqwjEQAD9I4ZC16uzdbgWgitg/UHKHLffKNCQZ5+y8jpIZPJcKcwsr2HwPh+w3XA== + dependencies: + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b" - integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q== +"@babel/plugin-proposal-json-strings@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.1.tgz#b1e691ee24c651b5a5e32213222b2379734aff09" + integrity sha512-m8r5BmV+ZLpWPtMY2mOKN7wre6HIO4gfIiV+eOmsnZABNenrt/kzYBwrh+KOfgumSWpnlGs5F70J8afYMSJMBg== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-json-strings" "^7.8.0" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2" - integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.10.1.tgz#02dca21673842ff2fe763ac253777f235e9bbf78" + integrity sha512-56cI/uHYgL2C8HVuHOuvVowihhX0sxb3nnfVRzUeVHTWmRHTZrKuAh/OBIMggGU/S1g/1D2CRCXqP+3u7vX7iA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8" - integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ== +"@babel/plugin-proposal-numeric-separator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.10.1.tgz#a9a38bc34f78bdfd981e791c27c6fdcec478c123" + integrity sha512-jjfym4N9HtCiNfyyLAVD8WqPYeHUrw4ihxuAynWj6zzp2gf9Ey2f7ImhFm6ikB3CLf5Z/zmcJDri6B4+9j9RsA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/plugin-syntax-numeric-separator" "^7.10.1" -"@babel/plugin-proposal-object-rest-spread@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f" - integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow== +"@babel/plugin-proposal-object-rest-spread@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.3.tgz#b8d0d22f70afa34ad84b7a200ff772f9b9fce474" + integrity sha512-ZZh5leCIlH9lni5bU/wB/UcjtcVLgR8gc+FAgW2OOY+m9h1II3ItTO1/cewNUcsIDZSYcSaz/rYVls+Fb0ExVQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" + "@babel/plugin-transform-parameters" "^7.10.1" -"@babel/plugin-proposal-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9" - integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw== +"@babel/plugin-proposal-optional-catch-binding@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.10.1.tgz#c9f86d99305f9fa531b568ff5ab8c964b8b223d2" + integrity sha512-VqExgeE62YBqI3ogkGoOJp1R6u12DFZjqwJhqtKc2o5m1YTUuUWnos7bZQFBhwkxIFpWYJ7uB75U7VAPPiKETA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58" - integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w== +"@babel/plugin-proposal-optional-chaining@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.3.tgz#9a726f94622b653c0a3a7a59cdce94730f526f7c" + integrity sha512-yyG3n9dJ1vZ6v5sfmIlMMZ8azQoqx/5/nZTSWX1td6L1H1bsjzA8TInDChpafCZiJkeOFzp/PtrfigAQXxI1Ng== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.3" "@babel/plugin-syntax-optional-chaining" "^7.8.0" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" - integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A== +"@babel/plugin-proposal-private-methods@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.10.1.tgz#ed85e8058ab0fe309c3f448e5e1b73ca89cdb598" + integrity sha512-RZecFFJjDiQ2z6maFprLgrdnm0OzoC23Mx89xf1CcEsxmHuzuXOdniEuI+S3v7vjQG4F5sa6YtUp+19sZuSxHg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.8" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-create-class-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + +"@babel/plugin-proposal-unicode-property-regex@^7.10.1", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.1.tgz#dc04feb25e2dd70c12b05d680190e138fa2c0c6f" + integrity sha512-JjfngYRvwmPwmnbRZyNiPFI8zxCZb8euzbCG/LxyKdeTb59tVciKo9GK9bi6JYKInk1H11Dq9j/zRqIH4KigfQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-async-generators@^7.8.0": version "7.8.4" @@ -325,6 +354,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-class-properties@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.1.tgz#d5bc0645913df5b17ad7eda0fa2308330bde34c5" + integrity sha512-Gf2Yx/iRs1JREDtVZ56OrjjgFHCaldpTnuy9BHla10qyVT3YkIIGEtoDWhyop0ksu1GvNjHIoYRBqm3zoR1jyQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/plugin-syntax-dynamic-import@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -346,12 +382,12 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f" - integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw== +"@babel/plugin-syntax-numeric-separator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.1.tgz#25761ee7410bc8cf97327ba741ee94e4a61b7d99" + integrity sha512-uTd0OsHrpe3tH5gRPTxG8Voh99/WCU78vIm5NMRYPAqC8lR4vajt6KkCAknCHrx24vkPdd/05yfdGSB4EIY2mg== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/plugin-syntax-object-rest-spread@^7.8.0": version "7.8.3" @@ -374,326 +410,337 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391" - integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g== +"@babel/plugin-syntax-top-level-await@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.10.1.tgz#8b8733f8c57397b3eaa47ddba8841586dcaef362" + integrity sha512-hgA5RYkmZm8FTFT3yu2N9Bx7yVVOKYT6yEdXXo6j2JTm0wNxgqaGeQVaSHRjhfnQbX91DtjFB6McRFSlcJH3xQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-arrow-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" - integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg== +"@babel/plugin-transform-arrow-functions@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.10.1.tgz#cb5ee3a36f0863c06ead0b409b4cc43a889b295b" + integrity sha512-6AZHgFJKP3DJX0eCNJj01RpytUa3SOGawIxweHkNX2L6PYikOZmoh5B0d7hIHaIgveMjX990IAa/xK7jRTN8OA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-async-to-generator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086" - integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ== +"@babel/plugin-transform-async-to-generator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.10.1.tgz#e5153eb1a3e028f79194ed8a7a4bf55f862b2062" + integrity sha512-XCgYjJ8TY2slj6SReBUyamJn3k2JLUIiiR5b6t1mNCMSvv7yx+jJpaewakikp0uWFQSF7ChPPoe3dHmXLpISkg== dependencies: - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-remap-async-to-generator" "^7.8.3" + "@babel/helper-module-imports" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-remap-async-to-generator" "^7.10.1" -"@babel/plugin-transform-block-scoped-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3" - integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg== +"@babel/plugin-transform-block-scoped-functions@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.10.1.tgz#146856e756d54b20fff14b819456b3e01820b85d" + integrity sha512-B7K15Xp8lv0sOJrdVAoukKlxP9N59HS48V1J3U/JGj+Ad+MHq+am6xJVs85AgXrQn4LV8vaYFOB+pr/yIuzW8Q== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-block-scoping@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a" - integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w== +"@babel/plugin-transform-block-scoping@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.1.tgz#47092d89ca345811451cd0dc5d91605982705d5e" + integrity sha512-8bpWG6TtF5akdhIm/uWTyjHqENpy13Fx8chg7pFH875aNLwX8JxIxqm08gmAT+Whe6AOmaTeLPe7dpLbXt+xUw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" lodash "^4.17.13" -"@babel/plugin-transform-classes@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d" - integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ== +"@babel/plugin-transform-classes@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.10.3.tgz#8d9a656bc3d01f3ff69e1fccb354b0f9d72ac544" + integrity sha512-irEX0ChJLaZVC7FvvRoSIxJlmk0IczFLcwaRXUArBKYHCHbOhe57aG8q3uw/fJsoSXvZhjRX960hyeAGlVBXZw== dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-define-map" "^7.8.3" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/helper-annotate-as-pure" "^7.10.1" + "@babel/helper-define-map" "^7.10.3" + "@babel/helper-function-name" "^7.10.3" + "@babel/helper-optimise-call-expression" "^7.10.3" + "@babel/helper-plugin-utils" "^7.10.3" + "@babel/helper-replace-supers" "^7.10.1" + "@babel/helper-split-export-declaration" "^7.10.1" globals "^11.1.0" -"@babel/plugin-transform-computed-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b" - integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA== +"@babel/plugin-transform-computed-properties@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.10.3.tgz#d3aa6eef67cb967150f76faff20f0abbf553757b" + integrity sha512-GWzhaBOsdbjVFav96drOz7FzrcEW6AP5nax0gLIpstiFaI3LOb2tAg06TimaWU6YKOfUACK3FVrxPJ4GSc5TgA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.3" -"@babel/plugin-transform-destructuring@^7.8.3": - version "7.8.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b" - integrity sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ== +"@babel/plugin-transform-destructuring@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.1.tgz#abd58e51337815ca3a22a336b85f62b998e71907" + integrity sha512-V/nUc4yGWG71OhaTH705pU8ZSdM6c1KmmLP8ys59oOYbT7RpMYAR3MsVOt6OHL0WzG7BlTU076va9fjJyYzJMA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e" - integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw== +"@babel/plugin-transform-dotall-regex@^7.10.1", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.1.tgz#920b9fec2d78bb57ebb64a644d5c2ba67cc104ee" + integrity sha512-19VIMsD1dp02RvduFUmfzj8uknaO3uiHHF0s3E1OHnVsNj8oge8EQ5RzHRbJjGSetRnkEuBYO7TG1M5kKjGLOA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-create-regexp-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-duplicate-keys@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1" - integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ== +"@babel/plugin-transform-duplicate-keys@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.10.1.tgz#c900a793beb096bc9d4d0a9d0cde19518ffc83b9" + integrity sha512-wIEpkX4QvX8Mo9W6XF3EdGttrIPZWozHfEaDTU0WJD/TDnXMvdDh30mzUl/9qWhnf7naicYartcEfUghTCSNpA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-exponentiation-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7" - integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ== +"@babel/plugin-transform-exponentiation-operator@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.10.1.tgz#279c3116756a60dd6e6f5e488ba7957db9c59eb3" + integrity sha512-lr/przdAbpEA2BUzRvjXdEDLrArGRRPwbaF9rvayuHRvdQ7lUTTkZnhZrJ4LE2jvgMRFF4f0YuPQ20vhiPYxtA== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-for-of@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e" - integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ== +"@babel/plugin-transform-for-of@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.1.tgz#ff01119784eb0ee32258e8646157ba2501fcfda5" + integrity sha512-US8KCuxfQcn0LwSCMWMma8M2R5mAjJGsmoCBVwlMygvmDUMkTCykc84IqN1M7t+agSfOmLYTInLCHJM+RUoz+w== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-function-name@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b" - integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ== +"@babel/plugin-transform-function-name@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.10.1.tgz#4ed46fd6e1d8fde2a2ec7b03c66d853d2c92427d" + integrity sha512-//bsKsKFBJfGd65qSNNh1exBy5Y9gD9ZN+DvrJ8f7HXr4avE5POW6zB7Rj6VnqHV33+0vXWUwJT0wSHubiAQkw== dependencies: - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-function-name" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1" - integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A== +"@babel/plugin-transform-literals@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.10.1.tgz#5794f8da82846b22e4e6631ea1658bce708eb46a" + integrity sha512-qi0+5qgevz1NHLZroObRm5A+8JJtibb7vdcPQF1KQE12+Y/xxl8coJ+TpPW9iRq+Mhw/NKLjm+5SHtAHCC7lAw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-member-expression-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410" - integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA== +"@babel/plugin-transform-member-expression-literals@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.10.1.tgz#90347cba31bca6f394b3f7bd95d2bbfd9fce2f39" + integrity sha512-UmaWhDokOFT2GcgU6MkHC11i0NQcL63iqeufXWfRy6pUOGYeCGEKhvfFO6Vz70UfYJYHwveg62GS83Rvpxn+NA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-modules-amd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4" - integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q== +"@babel/plugin-transform-modules-amd@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.10.1.tgz#65950e8e05797ebd2fe532b96e19fc5482a1d52a" + integrity sha512-31+hnWSFRI4/ACFr1qkboBbrTxoBIzj7qA69qlq8HY8p7+YCzkCT6/TvQ1a4B0z27VeWtAeJd6pr5G04dc1iHw== dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940" - integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g== +"@babel/plugin-transform-modules-commonjs@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.10.1.tgz#d5ff4b4413ed97ffded99961056e1fb980fb9301" + integrity sha512-AQG4fc3KOah0vdITwt7Gi6hD9BtQP/8bhem7OjbaMoRNCH5Djx42O2vYMfau7QnAzQCa+RJnhJBmFFMGpQEzrg== dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-simple-access" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-simple-access" "^7.10.1" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90" - integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ== +"@babel/plugin-transform-modules-systemjs@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.3.tgz#004ae727b122b7b146b150d50cba5ffbff4ac56b" + integrity sha512-GWXWQMmE1GH4ALc7YXW56BTh/AlzvDWhUNn9ArFF0+Cz5G8esYlVbXfdyHa1xaD1j+GnBoCeoQNlwtZTVdiG/A== dependencies: - "@babel/helper-hoist-variables" "^7.8.3" - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + "@babel/helper-hoist-variables" "^7.10.3" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.3" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-umd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697" - integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ== +"@babel/plugin-transform-modules-umd@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.10.1.tgz#ea080911ffc6eb21840a5197a39ede4ee67b1595" + integrity sha512-EIuiRNMd6GB6ulcYlETnYYfgv4AxqrswghmBRQbWLHZxN4s7mupxzglnHqk9ZiUpDI4eRWewedJJNj67PWOXKA== dependencies: - "@babel/helper-module-transforms" "^7.9.0" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-module-transforms" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c" - integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw== +"@babel/plugin-transform-named-capturing-groups-regex@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.10.3.tgz#a4f8444d1c5a46f35834a410285f2c901c007ca6" + integrity sha512-I3EH+RMFyVi8Iy/LekQm948Z4Lz4yKT7rK+vuCAeRm0kTa6Z5W7xuhRxDNJv0FPya/her6AUgrDITb70YHtTvA== dependencies: "@babel/helper-create-regexp-features-plugin" "^7.8.3" -"@babel/plugin-transform-new-target@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43" - integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw== +"@babel/plugin-transform-new-target@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.10.1.tgz#6ee41a5e648da7632e22b6fb54012e87f612f324" + integrity sha512-MBlzPc1nJvbmO9rPr1fQwXOM2iGut+JC92ku6PbiJMMK7SnQc1rytgpopveE3Evn47gzvGYeCdgfCDbZo0ecUw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-object-super@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725" - integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ== +"@babel/plugin-transform-object-super@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.10.1.tgz#2e3016b0adbf262983bf0d5121d676a5ed9c4fde" + integrity sha512-WnnStUDN5GL+wGQrJylrnnVlFhFmeArINIR9gjhSeYyvroGhBrSAXYg/RHsnfzmsa+onJrTJrEClPzgNmmQ4Gw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-replace-supers" "^7.10.1" -"@babel/plugin-transform-parameters@^7.8.7": - version "7.9.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a" - integrity sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg== +"@babel/plugin-transform-parameters@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.10.1.tgz#b25938a3c5fae0354144a720b07b32766f683ddd" + integrity sha512-tJ1T0n6g4dXMsL45YsSzzSDZCxiHXAQp/qHrucOq5gEHncTA3xDxnd5+sZcoQp+N1ZbieAaB8r/VUCG0gqseOg== dependencies: - "@babel/helper-get-function-arity" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-get-function-arity" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-property-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263" - integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg== +"@babel/plugin-transform-property-literals@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.10.1.tgz#cffc7315219230ed81dc53e4625bf86815b6050d" + integrity sha512-Kr6+mgag8auNrgEpbfIWzdXYOvqDHZOF0+Bx2xh4H2EDNwcbRb9lY6nkZg8oSjsX+DH9Ebxm9hOqtKW+gRDeNA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-regenerator@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8" - integrity sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA== +"@babel/plugin-transform-regenerator@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.10.3.tgz#6ec680f140a5ceefd291c221cb7131f6d7e8cb6d" + integrity sha512-H5kNeW0u8mbk0qa1jVIVTeJJL6/TJ81ltD4oyPx0P499DhMJrTmmIFCmJ3QloGpQG8K9symccB7S7SJpCKLwtw== dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5" - integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A== +"@babel/plugin-transform-reserved-words@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.10.1.tgz#0fc1027312b4d1c3276a57890c8ae3bcc0b64a86" + integrity sha512-qN1OMoE2nuqSPmpTqEM7OvJ1FkMEV+BjVeZZm9V9mq/x1JLKQ4pcv8riZJMNN3u2AUGl0ouOMjRr2siecvHqUQ== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-shorthand-properties@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8" - integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w== +"@babel/plugin-transform-shorthand-properties@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.10.1.tgz#e8b54f238a1ccbae482c4dce946180ae7b3143f3" + integrity sha512-AR0E/lZMfLstScFwztApGeyTHJ5u3JUKMjneqRItWeEqDdHWZwAOKycvQNCasCK/3r5YXsuNG25funcJDu7Y2g== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8" - integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g== +"@babel/plugin-transform-spread@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.1.tgz#0c6d618a0c4461a274418460a28c9ccf5239a7c8" + integrity sha512-8wTPym6edIrClW8FI2IoaePB91ETOtg36dOkj3bYcNe7aDMN2FXEoUa+WrmPc4xa1u2PQK46fUX2aCb+zo9rfw== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-sticky-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100" - integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw== +"@babel/plugin-transform-sticky-regex@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.10.1.tgz#90fc89b7526228bed9842cff3588270a7a393b00" + integrity sha512-j17ojftKjrL7ufX8ajKvwRilwqTok4q+BjkknmQw9VNHnItTyMP5anPFzxFJdCQs7clLcWpCV3ma+6qZWLnGMA== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-regex" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" + "@babel/helper-regex" "^7.10.1" -"@babel/plugin-transform-template-literals@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80" - integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ== +"@babel/plugin-transform-template-literals@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.10.3.tgz#69d39b3d44b31e7b4864173322565894ce939b25" + integrity sha512-yaBn9OpxQra/bk0/CaA4wr41O0/Whkg6nqjqApcinxM7pro51ojhX6fv1pimAnVjVfDy14K0ULoRL70CA9jWWA== dependencies: - "@babel/helper-annotate-as-pure" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-annotate-as-pure" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.3" -"@babel/plugin-transform-typeof-symbol@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412" - integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg== +"@babel/plugin-transform-typeof-symbol@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.10.1.tgz#60c0239b69965d166b80a84de7315c1bc7e0bb0e" + integrity sha512-qX8KZcmbvA23zDi+lk9s6hC1FM7jgLHYIjuLgULgc8QtYnmB3tAVIYkNoKRQ75qWBeyzcoMoK8ZQmogGtC/w0g== dependencies: - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" -"@babel/plugin-transform-unicode-regex@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" - integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw== +"@babel/plugin-transform-unicode-escapes@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.1.tgz#add0f8483dab60570d9e03cecef6c023aa8c9940" + integrity sha512-zZ0Poh/yy1d4jeDWpx/mNwbKJVwUYJX73q+gyh4bwtG0/iUlzdEu0sLMda8yuDFS6LBQlT/ST1SJAR6zYwXWgw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-plugin-utils" "^7.10.1" + +"@babel/plugin-transform-unicode-regex@^7.10.1": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.10.1.tgz#6b58f2aea7b68df37ac5025d9c88752443a6b43f" + integrity sha512-Y/2a2W299k0VIUdbqYm9X2qS6fE0CUBhhiPpimK6byy7OJ/kORLlIX+J6UrjgNu5awvs62k+6RSslxhcvVw2Tw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.10.1" + "@babel/helper-plugin-utils" "^7.10.1" "@babel/polyfill@^7.2.5": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.8.7.tgz#151ec24c7135481336168c3bd8b8bf0cf91c032f" - integrity sha512-LeSfP9bNZH2UOZgcGcZ0PIHUt1ZuHub1L3CVmEyqLxCeDLm4C5Gi8jRH8ZX2PNpDhQCo0z6y/+DIs2JlliXW8w== + version "7.10.1" + resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.10.1.tgz#d56d4c8be8dd6ec4dce2649474e9b707089f739f" + integrity sha512-TviueJ4PBW5p48ra8IMtLXVkDucrlOZAIZ+EXqS3Ot4eukHbWiqcn7DcqpA1k5PcKtmJ4Xl9xwdv6yQvvcA+3g== dependencies: core-js "^2.6.5" regenerator-runtime "^0.13.4" "@babel/preset-env@^7.1.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8" - integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ== + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.3.tgz#3e58c9861bbd93b6a679987c7e4bd365c56c80c9" + integrity sha512-jHaSUgiewTmly88bJtMHbOd1bJf2ocYxb5BWKSDQIP5tmgFuS/n0gl+nhSrYDhT33m0vPxp+rP8oYYgPgMNQlg== dependencies: - "@babel/compat-data" "^7.9.0" - "@babel/helper-compilation-targets" "^7.8.7" - "@babel/helper-module-imports" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/plugin-proposal-async-generator-functions" "^7.8.3" - "@babel/plugin-proposal-dynamic-import" "^7.8.3" - "@babel/plugin-proposal-json-strings" "^7.8.3" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-proposal-numeric-separator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.9.0" - "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" - "@babel/plugin-proposal-optional-chaining" "^7.9.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" + "@babel/compat-data" "^7.10.3" + "@babel/helper-compilation-targets" "^7.10.2" + "@babel/helper-module-imports" "^7.10.3" + "@babel/helper-plugin-utils" "^7.10.3" + "@babel/plugin-proposal-async-generator-functions" "^7.10.3" + "@babel/plugin-proposal-class-properties" "^7.10.1" + "@babel/plugin-proposal-dynamic-import" "^7.10.1" + "@babel/plugin-proposal-json-strings" "^7.10.1" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.10.1" + "@babel/plugin-proposal-numeric-separator" "^7.10.1" + "@babel/plugin-proposal-object-rest-spread" "^7.10.3" + "@babel/plugin-proposal-optional-catch-binding" "^7.10.1" + "@babel/plugin-proposal-optional-chaining" "^7.10.3" + "@babel/plugin-proposal-private-methods" "^7.10.1" + "@babel/plugin-proposal-unicode-property-regex" "^7.10.1" "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.10.1" "@babel/plugin-syntax-dynamic-import" "^7.8.0" "@babel/plugin-syntax-json-strings" "^7.8.0" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" - "@babel/plugin-syntax-numeric-separator" "^7.8.0" + "@babel/plugin-syntax-numeric-separator" "^7.10.1" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - "@babel/plugin-transform-arrow-functions" "^7.8.3" - "@babel/plugin-transform-async-to-generator" "^7.8.3" - "@babel/plugin-transform-block-scoped-functions" "^7.8.3" - "@babel/plugin-transform-block-scoping" "^7.8.3" - "@babel/plugin-transform-classes" "^7.9.0" - "@babel/plugin-transform-computed-properties" "^7.8.3" - "@babel/plugin-transform-destructuring" "^7.8.3" - "@babel/plugin-transform-dotall-regex" "^7.8.3" - "@babel/plugin-transform-duplicate-keys" "^7.8.3" - "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.9.0" - "@babel/plugin-transform-function-name" "^7.8.3" - "@babel/plugin-transform-literals" "^7.8.3" - "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.9.0" - "@babel/plugin-transform-modules-commonjs" "^7.9.0" - "@babel/plugin-transform-modules-systemjs" "^7.9.0" - "@babel/plugin-transform-modules-umd" "^7.9.0" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" - "@babel/plugin-transform-new-target" "^7.8.3" - "@babel/plugin-transform-object-super" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.8.7" - "@babel/plugin-transform-property-literals" "^7.8.3" - "@babel/plugin-transform-regenerator" "^7.8.7" - "@babel/plugin-transform-reserved-words" "^7.8.3" - "@babel/plugin-transform-shorthand-properties" "^7.8.3" - "@babel/plugin-transform-spread" "^7.8.3" - "@babel/plugin-transform-sticky-regex" "^7.8.3" - "@babel/plugin-transform-template-literals" "^7.8.3" - "@babel/plugin-transform-typeof-symbol" "^7.8.4" - "@babel/plugin-transform-unicode-regex" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.10.1" + "@babel/plugin-transform-arrow-functions" "^7.10.1" + "@babel/plugin-transform-async-to-generator" "^7.10.1" + "@babel/plugin-transform-block-scoped-functions" "^7.10.1" + "@babel/plugin-transform-block-scoping" "^7.10.1" + "@babel/plugin-transform-classes" "^7.10.3" + "@babel/plugin-transform-computed-properties" "^7.10.3" + "@babel/plugin-transform-destructuring" "^7.10.1" + "@babel/plugin-transform-dotall-regex" "^7.10.1" + "@babel/plugin-transform-duplicate-keys" "^7.10.1" + "@babel/plugin-transform-exponentiation-operator" "^7.10.1" + "@babel/plugin-transform-for-of" "^7.10.1" + "@babel/plugin-transform-function-name" "^7.10.1" + "@babel/plugin-transform-literals" "^7.10.1" + "@babel/plugin-transform-member-expression-literals" "^7.10.1" + "@babel/plugin-transform-modules-amd" "^7.10.1" + "@babel/plugin-transform-modules-commonjs" "^7.10.1" + "@babel/plugin-transform-modules-systemjs" "^7.10.3" + "@babel/plugin-transform-modules-umd" "^7.10.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.10.3" + "@babel/plugin-transform-new-target" "^7.10.1" + "@babel/plugin-transform-object-super" "^7.10.1" + "@babel/plugin-transform-parameters" "^7.10.1" + "@babel/plugin-transform-property-literals" "^7.10.1" + "@babel/plugin-transform-regenerator" "^7.10.3" + "@babel/plugin-transform-reserved-words" "^7.10.1" + "@babel/plugin-transform-shorthand-properties" "^7.10.1" + "@babel/plugin-transform-spread" "^7.10.1" + "@babel/plugin-transform-sticky-regex" "^7.10.1" + "@babel/plugin-transform-template-literals" "^7.10.3" + "@babel/plugin-transform-typeof-symbol" "^7.10.1" + "@babel/plugin-transform-unicode-escapes" "^7.10.1" + "@babel/plugin-transform-unicode-regex" "^7.10.1" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.9.0" - browserslist "^4.9.1" + "@babel/types" "^7.10.3" + browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" levenary "^1.1.1" @@ -711,57 +758,57 @@ esutils "^2.0.2" "@babel/runtime-corejs3@^7.9.2": - version "7.10.2" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.2.tgz#3511797ddf9a3d6f3ce46b99cc835184817eaa4e" - integrity sha512-+a2M/u7r15o3dV1NEizr9bRi+KUVnrs/qYxF0Z06DAPx/4VCWaz1WA7EcbE+uqGgt39lp5akWGmHsTseIkHkHg== + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a" + integrity sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw== dependencies: core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" - integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== +"@babel/runtime@^7.8.4": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" + integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.8.3", "@babel/template@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" - integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== +"@babel/template@^7.10.1", "@babel/template@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.3.tgz#4d13bc8e30bf95b0ce9d175d30306f42a2c9a7b8" + integrity sha512-5BjI4gdtD+9fHZUsaxPHPNpwa+xRkDO7c7JbhYn2afvrkDu5SfAAbi9AIMXw2xEhO/BR35TqiW97IqNvCo/GqA== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" + "@babel/code-frame" "^7.10.3" + "@babel/parser" "^7.10.3" + "@babel/types" "^7.10.3" -"@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892" - integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w== +"@babel/traverse@^7.10.1", "@babel/traverse@^7.10.3": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.3.tgz#0b01731794aa7b77b214bcd96661f18281155d7e" + integrity sha512-qO6623eBFhuPm0TmmrUFMT1FulCmsSeJuVGhiLodk2raUDFhhTECLd9E9jC4LBIWziqt4wgF6KuXE4d+Jz9yug== dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" - "@babel/helper-function-name" "^7.8.3" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/code-frame" "^7.10.3" + "@babel/generator" "^7.10.3" + "@babel/helper-function-name" "^7.10.3" + "@babel/helper-split-export-declaration" "^7.10.1" + "@babel/parser" "^7.10.3" + "@babel/types" "^7.10.3" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0-beta.49", "@babel/types@^7.2.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5" - integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng== +"@babel/types@^7.0.0-beta.49", "@babel/types@^7.10.1", "@babel/types@^7.10.3", "@babel/types@^7.2.0", "@babel/types@^7.4.4": + version "7.10.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.3.tgz#6535e3b79fea86a6b09e012ea8528f935099de8e" + integrity sha512-nZxaJhBXBQ8HVoIcGsf9qWep3Oh3jCENK54V4mRF7qaJabVsAYdbTtmSD8WmAp1R6ytPiu5apMwSXyxB1WlaBA== dependencies: - "@babel/helper-validator-identifier" "^7.9.0" + "@babel/helper-validator-identifier" "^7.10.3" lodash "^4.17.13" to-fast-properties "^2.0.0" "@fortawesome/fontawesome-free@^5.11.2": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9" - integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg== + version "5.13.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.1.tgz#c53b4066edae16cd1fd669f687baf031b45fb9d6" + integrity sha512-D819f34FLHeBN/4xvw0HR0u7U2G7RqjPSggXqf7LktsxWQ48VAfGwvMrhcVuaZV2fF069c/619RdgCCms0DHhw== "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -784,13 +831,6 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" -"@samverschueren/stream-to-observable@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" - integrity sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg== - dependencies: - any-observable "^0.3.0" - "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" @@ -801,22 +841,16 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== -"@types/events@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - "@types/fined@*": version "1.1.2" resolved "https://registry.yarnpkg.com/@types/fined/-/fined-1.1.2.tgz#05d2b9f93d144855c97c18c9675f424ed01400c4" integrity sha512-hzzTS+X9EqDhx4vwdch/DOZci/bfh5J6Nyz8lqvyfBg2ROx2fPafX+LpDfpVgSvQKj0EYkwTYpBO3z2etwbkOw== "@types/glob@^7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" - integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.2.tgz#06ca26521353a545d94a0adc74f38a59d232c987" + integrity sha512-VgNIkxK+j7Nz5P7jvUZlRvhuPSmsEfS03b0alKcq5V/STUKAa3Plemsn5mrQUO7am6OErJ4rhGEGJbACclrtRA== dependencies: - "@types/events" "*" "@types/minimatch" "*" "@types/node" "*" @@ -835,6 +869,16 @@ dependencies: "@types/node" "*" +"@types/json-schema@^7.0.4": + version "7.0.5" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" + integrity sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/liftoff@^2.5.0": version "2.5.0" resolved "https://registry.yarnpkg.com/@types/liftoff/-/liftoff-2.5.0.tgz#aa5f030ae0952d1b86225f3e9f27f6d5b69714aa" @@ -850,9 +894,9 @@ integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== "@types/node@*": - version "12.12.31" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.31.tgz#d6b4f9645fee17f11319b508fb1001797425da51" - integrity sha512-T+wnJno8uh27G9c+1T+a1/WYCHzLeDqtsGJkoEdSp2X8RTh3oOCZQcUnjAx90CS8cmmADX51O0FI/tu9s0yssg== + version "14.0.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce" + integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ== "@types/parse-json@^4.0.0": version "4.0.0" @@ -860,9 +904,9 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/q@^1.5.1": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" - integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" + integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== "@types/through@*": version "0.0.30" @@ -1085,7 +1129,7 @@ acorn@^5.5.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== -acorn@^6.0.7, acorn@^6.2.1: +acorn@^6.0.7, acorn@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== @@ -1116,9 +1160,9 @@ ajv-keywords@^1.0.0: integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw= ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" - integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + version "3.5.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.0.tgz#5c894537098785926d71e696114a53ce768ed773" + integrity sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw== ajv@^4.7.0: version "4.11.8" @@ -1128,10 +1172,10 @@ ajv@^4.7.0: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.9.1: - version "6.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" - integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== +ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.9.1: + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== dependencies: fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" @@ -1248,9 +1292,9 @@ angular-utils-pagination@~0.11.1: integrity sha1-7618iHm+swrT13cH+T49DvUfLGY= angular@1.x, angular@^1.3: - version "1.7.9" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.9.tgz#e52616e8701c17724c3c238cfe4f9446fd570bc4" - integrity sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ== + version "1.8.0" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.8.0.tgz#b1ec179887869215cab6dfd0df2e42caa65b1b51" + integrity sha512-VdaMx+Qk0Skla7B5gw77a8hzlcOakwF8mjlW13DpIWIDlfqwAbSSLfd8N/qZnzEmQF4jC4iofInd3gE7vL8ZZg== angular@~1.5.0: version "1.5.11" @@ -1267,7 +1311,7 @@ angularjs-slider@^6.4.0: resolved "https://registry.yarnpkg.com/angularjs-slider/-/angularjs-slider-6.7.0.tgz#eb2229311b81b79315a36e7b5eb700e128f50319" integrity sha512-Cizsuax65wN2Y+htmA3safE5ALOSCyWcKyWkziaO8vCVymi26bQQs6kKDhkYc8GFix/KE7Oc9gH3QLlTUgD38w== -ansi-colors@^3.0.0: +ansi-colors@^3.0.0, ansi-colors@^3.2.1: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== @@ -1277,12 +1321,12 @@ ansi-escapes@^1.1.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= -ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: +ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^4.2.1: +ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== @@ -1326,7 +1370,7 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== @@ -1334,11 +1378,6 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" -any-observable@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" - integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog== - anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -1347,6 +1386,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + applause@1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/applause/-/applause-1.2.2.tgz#a8468579e81f67397bb5634c29953bedcd0f56c0" @@ -1362,9 +1409,9 @@ aproba@^1.1.1: integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== arch@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e" - integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg== + version "2.1.2" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf" + integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ== archive-type@^4.0.0: version "4.0.0" @@ -1420,7 +1467,7 @@ array-flatten@^2.1.0: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== -array-includes@^3.0.3: +array-includes@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== @@ -1456,7 +1503,7 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.1: +array.prototype.flat@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== @@ -1501,6 +1548,11 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + async-each@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" @@ -1597,10 +1649,10 @@ babel-plugin-angularjs-annotate@^0.10.0: "@babel/types" "^7.2.0" simple-is "~0.2.0" -babel-plugin-dynamic-import-node@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f" - integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ== +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== dependencies: object.assign "^4.1.0" @@ -1716,6 +1768,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -1741,10 +1798,15 @@ bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + +bn.js@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0" + integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA== body-parser@1.19.0: version "1.19.0" @@ -1822,7 +1884,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1865,7 +1927,7 @@ browserify-des@^1.0.0: inherits "^2.0.1" safe-buffer "^5.1.2" -browserify-rsa@^4.0.0: +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= @@ -1874,17 +1936,19 @@ browserify-rsa@^4.0.0: randombytes "^2.0.1" browserify-sign@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" - integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= + version "4.2.0" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.0.tgz#545d0b1b07e6b2c99211082bf1b12cce7a0b0e11" + integrity sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA== dependencies: - bn.js "^4.1.1" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.2" - elliptic "^6.0.0" - inherits "^2.0.1" - parse-asn1 "^5.0.0" + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.2" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" browserify-zlib@^0.2.0: version "0.2.0" @@ -1909,15 +1973,15 @@ browserslist@^2.11.3: caniuse-lite "^1.0.30000792" electron-to-chromium "^1.3.30" -browserslist@^4.8.3, browserslist@^4.9.1: - version "4.11.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad" - integrity sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A== +browserslist@^4.12.0, browserslist@^4.8.5: + version "4.12.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d" + integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg== dependencies: - caniuse-lite "^1.0.30001035" - electron-to-chromium "^1.3.380" - node-releases "^1.1.52" - pkg-up "^3.1.0" + caniuse-lite "^1.0.30001043" + electron-to-chromium "^1.3.413" + node-releases "^1.1.53" + pkg-up "^2.0.0" buffer-alloc-unsafe@^1.1.0: version "1.1.0" @@ -1967,9 +2031,9 @@ buffer@^4.3.0: isarray "^1.0.0" buffer@^5.2.1: - version "5.5.0" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.5.0.tgz#9c3caa3d623c33dd1c7ef584b89b88bf9c9bc1ce" - integrity sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww== + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" @@ -2111,14 +2175,14 @@ caniuse-api@^1.5.2: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30001037" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001037.tgz#359b13c5545fca8885da3d2a79f1a61eaef3cb93" - integrity sha512-TMs8GQUrZG0i+qCRxHS3zV5Ivlk1fFkBebtYdCeBgupGx1zX3vTFI4IEuqchlqUVmqZE3YKFeWFRxd+jUZ38yA== + version "1.0.30001088" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001088.tgz#13490ed989b20f4b199168371219a8ba657bce03" + integrity sha512-Sqkmd4oi1oiyOn2EkTauy1Bx1sVFv+drLtjkK6q4vTQpaI9wCjGGU/87MQXCiIh/SMU0FrdSnnA358yHkz8p/Q== -caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30001035: - version "1.0.30001037" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001037.tgz#cf666560b14f8dfa18abc235db1ef2699273af6e" - integrity sha512-qQP40FzWQ1i9RTjxppOUnpM8OwTBFL5DQbjoR9Az32EtM7YUZOw9orFO6rj1C+xWAGzz+X3bUe09Jf5Ep+zpuA== +caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.30001043: + version "1.0.30001088" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001088.tgz#23a6b9e192106107458528858f2c0e0dba0d9073" + integrity sha512-6eYUrlShRYveyqKG58HcyOfPgh3zb2xqs7NvT2VVtP3hEUeeWvc3lqhpeMTxYWBBeeaT9A4bKsrtjATm66BTHg== caw@^2.0.0, caw@^2.0.1: version "2.0.1" @@ -2130,15 +2194,6 @@ caw@^2.0.0, caw@^2.0.1: tunnel-agent "^0.6.0" url-to-options "^1.0.1" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2150,6 +2205,15 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.2, chalk@~2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -2158,6 +2222,14 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + change-case@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/change-case/-/change-case-3.1.0.tgz#0e611b7edc9952df2e8513b27b42de72647dd17e" @@ -2210,7 +2282,7 @@ chartjs-color@^2.1.0: chartjs-color-string "^0.6.0" color-convert "^1.9.3" -chokidar@^2.0.2, chokidar@^2.1.8: +chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== @@ -2229,6 +2301,21 @@ chokidar@^2.0.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" + integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + chokidar@~0.6: version "0.6.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-0.6.3.tgz#e85968fa235f21773d388c617af085bf2104425a" @@ -2312,7 +2399,7 @@ cli-cursor@^1.0.1: dependencies: restore-cursor "^1.0.1" -cli-cursor@^2.0.0, cli-cursor@^2.1.0: +cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= @@ -2327,22 +2414,22 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-spinners@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" - integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.3.0.tgz#0632239a4b5aa4c958610142c34bb7a651fc8df5" + integrity sha512-Xs2Hf2nzrvJMFKimOR7YR0QwZ8fc0u98kdtwN1eNAZzNQgH3vK2pXzff6GJtKh7S5hoJ87ECiAiZFS2fb5Ii2w== -cli-truncate@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" - integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= +cli-truncate@2.1.0, cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== dependencies: - slice-ansi "0.0.4" - string-width "^1.0.1" + slice-ansi "^3.0.0" + string-width "^4.2.0" cli-width@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" - integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" + integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== cliui@^5.0.0: version "5.0.0" @@ -2433,12 +2520,12 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3, color-name@^1.0.0: +color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -2483,15 +2570,15 @@ commander@2.17.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.20.0, commander@~2.20.3: +commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" - integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== commander@~2.1.0: version "2.1.0" @@ -2515,7 +2602,7 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -compare-versions@^3.5.1: +compare-versions@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== @@ -2648,11 +2735,11 @@ copy-descriptor@^0.1.0: integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= core-js-compat@^3.6.2: - version "3.6.4" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17" - integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA== + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.5.tgz#2a51d9a4e25dfd6e690251aa81f99e3c05481f1c" + integrity sha512-7ItTKOhOZbznhXAQ2g/slGg1PJV5zDO/WdkTwi7UEOJmkvsE32PWvx6mKtDjiMpjnR2CNf6BAD6sSxIlv7ptng== dependencies: - browserslist "^4.8.3" + browserslist "^4.8.5" semver "7.0.0" core-js-pure@^3.0.0: @@ -2699,7 +2786,7 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-hash@^1.1.0, create-hash@^1.1.2: +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -2710,7 +2797,7 @@ create-hash@^1.1.0, create-hash@^1.1.2: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -2722,7 +2809,16 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== @@ -2733,19 +2829,10 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" - integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= - dependencies: - lru-cache "^4.0.1" - shebang-command "^1.2.0" - which "^1.2.9" - cross-spawn@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" - integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg== + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -2854,9 +2941,9 @@ css-what@2.1: integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== css-what@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" - integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== + version "3.3.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.3.0.tgz#10fec696a9ece2e591ac772d759aacabac38cd39" + integrity sha512-pv9JPyatiPaQ6pf4OvD/dbfm0o5LviWmwxNWzblYf/1u9QZd0ihV+PMwy5jdQWQ3349kZmKEx9WXuSka2dM4cg== cssesc@^3.0.0: version "3.0.0" @@ -2945,11 +3032,6 @@ d@1, d@^1.0.1: es5-ext "^0.10.50" type "^1.0.1" -date-fns@^1.27.2: - version "1.30.1" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" - integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== - dateformat@1.0.2-1.2.3: version "1.0.2-1.2.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.2-1.2.3.tgz#b0220c02de98617433b72851cf47de3df2cdbee9" @@ -2970,7 +3052,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6. dependencies: ms "2.0.0" -debug@^3.0.0, debug@^3.1.1, debug@^3.2.5: +debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== @@ -3370,20 +3452,15 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.380: - version "1.3.384" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.384.tgz#ca1d5710a4c53168431f1cbef39c8a971b646bf8" - integrity sha512-9jGNF78o450ymPf63n7/j1HrRAD4xGTsDkKY2X6jtCAWaYgph2A9xQjwfwRpj+AovkARMO+JfZuVCFTdandD6w== +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.413: + version "1.3.481" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.481.tgz#0d59e72a0aaeb876b43fb1d6e84bf0dfc99617e8" + integrity sha512-q2PeCP2PQXSYadDo9uNY+uHXjdB9PcsUpCVoGlY8TZOPHGlXdevlqW9PkKeqCxn2QBkGB8b6AcMO++gh8X82bA== -elegant-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" - integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= - -elliptic@^6.0.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" - integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== +elliptic@^6.0.0, elliptic@^6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== dependencies: bn.js "^4.4.0" brorand "^1.0.1" @@ -3425,33 +3502,31 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -enhanced-resolve@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" - integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.4.0" - tapable "^1.0.0" - -enhanced-resolve@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" - integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== +enhanced-resolve@^4.1.0, enhanced-resolve@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.2.0.tgz#5d43bda4a0fd447cb0ebbe71bef8deff8805ad0d" + integrity sha512-S7eiFb/erugyd1rLb6mQ3Vuq+EXHv5cpCkNqqIkYkBgN2QdFnyCZzFBleqwGEx4lgNGYij81BWnCrFNK7vxvjQ== dependencies: graceful-fs "^4.1.2" memory-fs "^0.5.0" tapable "^1.0.0" +enquirer@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.5.tgz#3ab2b838df0a9d8ab9e7dff235b0e8712ef92381" + integrity sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA== + dependencies: + ansi-colors "^3.2.1" + entities@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== entities@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" - integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" + integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== errno@^0.1.3, errno@~0.1.7: version "0.1.7" @@ -3467,22 +3542,22 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: - version "1.17.5" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" - integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== dependencies: es-to-primitive "^1.2.1" function-bind "^1.1.1" has "^1.0.3" has-symbols "^1.0.1" - is-callable "^1.1.5" - is-regex "^1.0.5" + is-callable "^1.2.0" + is-regex "^1.1.0" object-inspect "^1.7.0" object-keys "^1.1.1" object.assign "^4.1.0" - string.prototype.trimleft "^2.1.1" - string.prototype.trimright "^2.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" es-to-primitive@^1.2.1: version "1.2.1" @@ -3599,16 +3674,16 @@ escope@^3.6.0: estraverse "^4.1.1" eslint-config-prettier@^6.10.1: - version "6.10.1" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz#129ef9ec575d5ddc0e269667bf09defcd898642a" - integrity sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ== + version "6.11.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1" + integrity sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA== dependencies: get-stdin "^6.0.0" -eslint-import-resolver-node@^0.3.2: - version "0.3.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404" - integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg== +eslint-import-resolver-node@^0.3.3: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== dependencies: debug "^2.6.9" resolve "^1.13.1" @@ -3624,7 +3699,7 @@ eslint-loader@^2.1.2: object-hash "^1.1.4" rimraf "^2.6.1" -eslint-module-utils@^2.4.1: +eslint-module-utils@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== @@ -3633,22 +3708,23 @@ eslint-module-utils@^2.4.1: pkg-dir "^2.0.0" eslint-plugin-import@^2.20.2: - version "2.20.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" - integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== + version "2.21.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz#8fef77475cc5510801bedc95f84b932f7f334a7c" + integrity sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA== dependencies: - array-includes "^3.0.3" - array.prototype.flat "^1.2.1" + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" contains-path "^0.1.0" debug "^2.6.9" doctrine "1.5.0" - eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.4.1" + eslint-import-resolver-node "^0.3.3" + eslint-module-utils "^2.6.0" has "^1.0.3" minimatch "^3.0.4" - object.values "^1.1.0" + object.values "^1.1.1" read-pkg-up "^2.0.0" - resolve "^1.12.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" eslint-scope@^4.0.3: version "4.0.3" @@ -3666,9 +3742,9 @@ eslint-utils@^1.3.1: eslint-visitor-keys "^1.1.0" eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" - integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== eslint@5.16.0: version "5.16.0" @@ -3786,11 +3862,11 @@ esprima@~3.1.0: integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= esquery@^1.0.0, esquery@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.2.0.tgz#a010a519c0288f2530b3404124bfb5f02e9797fe" - integrity sha512-weltsSqdeWIX9G2qQZz7KlTRJdkkOCTPgLYJUz1Hacf48R4YOwGPHO3+ORfWedqJKbq5WQmsgK90n+pFLIKt/Q== + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== dependencies: - estraverse "^5.0.0" + estraverse "^5.1.0" esrecurse@^4.1.0: version "4.2.1" @@ -3804,10 +3880,10 @@ estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== -estraverse@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.0.0.tgz#ac81750b482c11cca26e4b07e83ed8f75fbcdc22" - integrity sha512-j3acdrMzqrxmJTNj5dbr1YbjacrYgAxVMeF0gK16E3j494mOe7xygM/ZLIguEQ0ETwAg2hlJCtHRGav+y0Ny5A== +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== estraverse@~0.0.4: version "0.0.4" @@ -3838,9 +3914,9 @@ eventemitter2@~0.4.13: integrity sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas= eventemitter3@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" - integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + version "4.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" + integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== events@^3.0.0: version "3.1.0" @@ -3912,10 +3988,10 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" - integrity sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g== +execa@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" + integrity sha512-QI2zLa6CjGWdiQsmSkZoGtDx2N+cQIGb3yNolGTdjSQzydzLgYYf8LRuagp7S7fPimjcrzUDSUFd/MgzELMi4Q== dependencies: cross-spawn "^7.0.0" get-stream "^5.0.0" @@ -3924,7 +4000,6 @@ execa@^3.4.0: merge-stream "^2.0.0" npm-run-path "^4.0.0" onetime "^5.1.0" - p-finally "^2.0.0" signal-exit "^3.0.2" strip-final-newline "^2.0.0" @@ -4067,14 +4142,14 @@ extglob@^2.0.4: to-regex "^3.0.1" fast-deep-equal@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" - integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.0.3: - version "3.2.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" - integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4083,6 +4158,11 @@ fast-glob@^3.0.3: micromatch "^4.0.2" picomatch "^2.2.1" +fast-json-patch@^3.0.0-1: + version "3.0.0-1" + resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.0.0-1.tgz#4c68f2e7acfbab6d29d1719c44be51899c93dabb" + integrity sha512-6pdFb07cknxvPzCeLsFHStEy+MysPJPgZQ9LbQ/2O67unQF93SNqfdSqnPPl71YMHX+AD8gbl7iuoGFzHEdDuw== + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -4099,9 +4179,9 @@ fastparse@^1.1.1, fastparse@^1.1.2: integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== fastq@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.1.tgz#4570c74f2ded173e71cf0beb08ac70bb85826791" - integrity sha512-mpIH5sKYueh3YyeJwqtVo8sORi0CgtmkVbK6kZStpQlZBYQuTzG2CZ7idSiJuA7bY0SFCWUc5WIs+oYumGCQNw== + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== dependencies: reusify "^1.0.4" @@ -4131,7 +4211,7 @@ figgy-pudding@^3.5.1: resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== -figures@^1.3.5, figures@^1.7.0: +figures@^1.3.5: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= @@ -4146,7 +4226,7 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.0.0: +figures@^3.0.0, figures@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== @@ -4243,6 +4323,11 @@ fileset@0.1.x: glob "3.x" minimatch "0.x" +filesize-parser@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/filesize-parser/-/filesize-parser-1.5.0.tgz#97ad66d5b0d7154b2e8b1b4e83f526aed33c62f3" + integrity sha512-UTDpJB22VvozK7t31slU9WCAPSdcUWuwD7P7S6LBXswdgzUz+YhoziLOohknFcx0Kq5LWCAj4MEKY9q3zGq47Q== + filesize@~3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.3.0.tgz#53149ea3460e3b2e024962a51648aa572cf98122" @@ -4333,16 +4418,6 @@ find-versions@^3.0.0, find-versions@^3.2.0: dependencies: semver-regex "^2.0.0" -findup-sync@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - findup-sync@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" @@ -4353,6 +4428,16 @@ findup-sync@^2.0.0: micromatch "^3.0.4" resolve-dir "^1.0.1" +findup-sync@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + findup-sync@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-0.3.0.tgz#37930aa5d816b777c03445e1966cc6790a4c0b16" @@ -4396,9 +4481,9 @@ flat-cache@^2.0.1: write "1.0.3" flatted@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" - integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== flatten@^1.0.2: version "1.0.3" @@ -4414,11 +4499,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.0.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.10.0.tgz#01f5263aee921c6a54fb91667f08f4155ce169eb" - integrity sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ== - dependencies: - debug "^3.0.0" + version "1.12.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" + integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" @@ -4478,13 +4561,18 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.7: - version "1.2.12" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c" - integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q== + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== dependencies: bindings "^1.5.0" nan "^2.12.1" +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4596,7 +4684,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.0: +glob-parent@^5.1.0, glob-parent@~5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== @@ -4655,13 +4743,6 @@ glob@~7.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -global-modules@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -4671,6 +4752,13 @@ global-modules@^1.0.0: is-windows "^1.0.1" resolve-dir "^1.0.0" +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + global-prefix@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" @@ -4770,9 +4858,9 @@ got@^8.3.1: url-to-options "^1.0.1" graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== graceful-fs@~1, graceful-fs@~1.2.0: version "1.2.3" @@ -4978,9 +5066,9 @@ gruntify-eslint@^3.1.0: eslint "^3.0.0" handle-thing@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" - integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== handlebars@1.0.x: version "1.0.12" @@ -4991,13 +5079,14 @@ handlebars@1.0.x: uglify-js "~2.3" handlebars@^4.4.3: - version "4.7.3" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.3.tgz#8ece2797826886cf8082d1726ff21d2a022550ee" - integrity sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg== + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== dependencies: + minimist "^1.2.5" neo-async "^2.6.0" - optimist "^0.6.1" source-map "^0.6.1" + wordwrap "^1.0.0" optionalDependencies: uglify-js "^3.1.4" @@ -5079,12 +5168,13 @@ has@^1.0.1, has@^1.0.3: function-bind "^1.1.1" hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" @@ -5244,10 +5334,10 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -"http-parser-js@>=0.4.0 <0.4.11": - version "0.4.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" - integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= +http-parser-js@>=0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" + integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== http-proxy-middleware@0.19.1: version "0.19.1" @@ -5279,13 +5369,13 @@ human-signals@^1.1.1: integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== husky@>=4: - version "4.2.3" - resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.3.tgz#3b18d2ee5febe99e27f2983500202daffbc3151e" - integrity sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ== + version "4.2.5" + resolved "https://registry.yarnpkg.com/husky/-/husky-4.2.5.tgz#2b4f7622673a71579f901d9885ed448394b5fa36" + integrity sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ== dependencies: - chalk "^3.0.0" + chalk "^4.0.0" ci-info "^2.0.0" - compare-versions "^3.5.1" + compare-versions "^3.6.0" cosmiconfig "^6.0.0" find-versions "^3.2.0" opencollective-postinstall "^2.0.2" @@ -5334,9 +5424,9 @@ ignore@^4.0.6: integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== ignore@^5.1.1: - version "5.1.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" - integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== image-webpack-loader@^4.5.0: version "4.6.0" @@ -5454,7 +5544,7 @@ import-lazy@^3.1.0: resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-3.1.0.tgz#891279202c8a2280fdbd6674dbd8da1a1dfc67cc" integrity sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ== -import-local@2.0.0, import-local@^2.0.0: +import-local@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== @@ -5474,11 +5564,6 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -5507,7 +5592,7 @@ inherits@1: resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" integrity sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js= -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5566,9 +5651,9 @@ inquirer@^6.2.2: through "^2.3.6" inquirer@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" - integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== + version "7.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.2.0.tgz#63ce99d823090de7eb420e4bb05e6f3449aa389a" + integrity sha512-E0c4rPwr9ByePfNlTIB8z51kK1s2n6jrHuJeEHENl/sbq2G/S1auvibgEwNR4uSyiU+PiYHqSwsgGiXjG8p5ZQ== dependencies: ansi-escapes "^4.2.1" chalk "^3.0.0" @@ -5592,10 +5677,10 @@ internal-ip@^4.3.0: default-gateway "^4.2.0" ipaddr.js "^1.9.0" -interpret@1.2.0, interpret@^1.0.0, interpret@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" - integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== +interpret@^1.0.0, interpret@^1.2.0, interpret@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== interpret@~1.1.0: version "1.1.0" @@ -5617,11 +5702,6 @@ invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -invert-kv@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" - integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== - ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -5686,15 +5766,22 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" - integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== +is-callable@^1.1.4, is-callable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== is-cwebp-readable@^2.0.1: version "2.0.1" @@ -5798,7 +5885,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -5860,13 +5947,6 @@ is-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA= -is-observable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" - integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA== - dependencies: - symbol-observable "^1.1.0" - is-path-cwd@^2.0.0, is-path-cwd@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" @@ -5908,22 +5988,17 @@ is-png@^1.0.0: resolved "https://registry.yarnpkg.com/is-png/-/is-png-1.1.0.tgz#d574b12bf275c0350455570b0e5b57ab062077ce" integrity sha1-1XSxK/J1wDUEVVcLDltXqwYgd84= -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= - is-property@^1.0.0, is-property@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= -is-regex@^1.0.4, is-regex@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" - integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== +is-regex@^1.0.4, is-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" + integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== dependencies: - has "^1.0.3" + has-symbols "^1.0.1" is-regexp@^1.0.0: version "1.0.0" @@ -6023,9 +6098,9 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= isbinaryfile@^4.0.2: - version "4.0.5" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.5.tgz#7193454fdd7fc0b12855c36c48d4ac7368fa3ec9" - integrity sha512-Jvz0gpTh1AILHMCBUyqq7xv1ZOQrxTDwyp1/QUq1xFpOBvp4AH5uEobPePJht8KnBGqQIH7We6OR73mXsjG0cA== + version "4.0.6" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" + integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== isexe@^2.0.0: version "2.0.0" @@ -6075,9 +6150,9 @@ jquery@>=1.12.0, jquery@^3.4.1, jquery@^3.5.1: integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== js-base64@^2.1.9: - version "2.5.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.2.tgz#313b6274dda718f714d00b3330bbae6e38e90209" - integrity sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ== + version "2.6.1" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.1.tgz#c328374225d2e65569791ded73c258e2c59334c7" + integrity sha512-G5x2saUTupU9D/xBY9snJs3TxvwX8EkpLFiYlPpDt/VmMHOXprnSU1nxiTmFbijCX4BLF/cMRIfAcC5BiMYgFQ== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" @@ -6152,9 +6227,9 @@ json5@^1.0.1: minimist "^1.2.0" json5@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e" - integrity sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ== + version "2.1.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" + integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== dependencies: minimist "^1.2.5" @@ -6229,13 +6304,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -lcid@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" - integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== - dependencies: - invert-kv "^2.0.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -6276,67 +6344,39 @@ lines-and-columns@^1.1.6: integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= lint-staged@>=10: - version "10.0.9" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.0.9.tgz#185aabb2432e9467c84add306c990f1c20da3cdb" - integrity sha512-NKJHYgRa8oI9c4Ic42ZtF2XA6Ps7lFbXwg3q0ZEP0r55Tw3YWykCW1RzW6vu+QIGqbsy7DxndvKu93Wtr5vPQw== + version "10.2.11" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-10.2.11.tgz#713c80877f2dc8b609b05bc59020234e766c9720" + integrity sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA== dependencies: - chalk "^3.0.0" - commander "^4.0.1" + chalk "^4.0.0" + cli-truncate "2.1.0" + commander "^5.1.0" cosmiconfig "^6.0.0" debug "^4.1.1" dedent "^0.7.0" - execa "^3.4.0" - listr "^0.14.3" - log-symbols "^3.0.0" + enquirer "^2.3.5" + execa "^4.0.1" + listr2 "^2.1.0" + log-symbols "^4.0.0" micromatch "^4.0.2" normalize-path "^3.0.0" please-upgrade-node "^3.2.0" string-argv "0.3.1" stringify-object "^3.3.0" -listr-silent-renderer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" - integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= - -listr-update-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" - integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA== +listr2@^2.1.0: + version "2.1.8" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.1.8.tgz#8af7ebc70cdbe866ddbb6c80909142bd45758f1f" + integrity sha512-Op+hheiChfAphkJ5qUxZtHgyjlX9iNnAeFS/S134xw7mVSg0YVrQo1IY4/K+ElY6XgOPg2Ij4z07urUXR+YEew== dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^2.3.0" - strip-ansi "^3.0.1" - -listr-verbose-renderer@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" - integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw== - dependencies: - chalk "^2.4.1" - cli-cursor "^2.1.0" - date-fns "^1.27.2" - figures "^2.0.0" - -listr@^0.14.3: - version "0.14.3" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" - integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== - dependencies: - "@samverschueren/stream-to-observable" "^0.3.0" - is-observable "^1.1.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.5.0" - listr-verbose-renderer "^0.5.0" - p-map "^2.0.0" - rxjs "^6.3.3" + chalk "^4.0.0" + cli-truncate "^2.1.0" + figures "^3.2.0" + indent-string "^4.0.0" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.5.5" + through "^2.3.8" load-grunt-tasks@^3.5.2: version "3.5.2" @@ -6382,15 +6422,6 @@ loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== -loader-utils@1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" - integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== - dependencies: - big.js "^5.2.2" - emojis-list "^2.0.0" - json5 "^1.0.1" - loader-utils@^0.2.16: version "0.2.17" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" @@ -6470,13 +6501,6 @@ lodash@^3.10.0, lodash@^3.6.0, lodash@^4.0.0, lodash@^4.11.0, lodash@^4.17.10, l resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -log-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" - integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= - dependencies: - chalk "^1.0.0" - log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" @@ -6484,21 +6508,22 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== dependencies: - chalk "^2.4.2" + chalk "^4.0.0" -log-update@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" - integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg= +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== dependencies: - ansi-escapes "^3.0.0" - cli-cursor "^2.0.0" - wrap-ansi "^3.0.1" + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" log4js@~0.6.3: version "0.6.38" @@ -6615,13 +6640,6 @@ make-iterator@^1.0.0: dependencies: kind-of "^6.0.2" -map-age-cleaner@^0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" - integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== - dependencies: - p-defer "^1.0.0" - map-cache@^0.2.0, map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -6668,16 +6686,7 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mem@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" - integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== - dependencies: - map-age-cleaner "^0.1.1" - mimic-fn "^2.0.0" - p-is-promise "^2.0.0" - -memory-fs@^0.4.0, memory-fs@^0.4.1: +memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= @@ -6720,9 +6729,9 @@ merge-stream@^2.0.0: integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.2.3, merge2@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" - integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== methods@~1.1.2: version "1.1.2" @@ -6764,17 +6773,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.43.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: - version "1.43.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" - integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== +mime-db@1.44.0, "mime-db@>= 1.43.0 < 2", mime-db@^1.28.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== mime-types@~2.1.17, mime-types@~2.1.24: - version "2.1.26" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" - integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: - mime-db "1.43.0" + mime-db "1.44.0" mime@1.6.0: version "1.6.0" @@ -6782,9 +6791,9 @@ mime@1.6.0: integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.0.3, mime@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" - integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== mime@~1.2: version "1.2.11" @@ -6796,7 +6805,7 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-fn@^2.0.0, mimic-fn@^2.1.0: +mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -6856,7 +6865,7 @@ minimatch@~0.2, minimatch@~0.2.11: lru-cache "2" sigmund "~1.0.0" -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~0.0.1: +minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -6891,9 +6900,9 @@ mkdirp@0.3.x: integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: - version "0.5.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" - integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" @@ -6903,9 +6912,9 @@ mkdirp@~1.0.3: integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== moment@^2.10.6, moment@^2.16.0, moment@^2.21.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== move-concurrently@^1.0.1: version "1.0.1" @@ -6982,9 +6991,9 @@ mute-stream@0.0.8: integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== nan@^2.12.1: - version "2.14.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" - integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + version "2.14.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" + integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== nan@~1.0.0: version "1.0.0" @@ -7114,12 +7123,10 @@ node-plop@~0.26.0: mkdirp "^0.5.1" resolve "^1.12.0" -node-releases@^1.1.52: - version "1.1.52" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.52.tgz#bcffee3e0a758e92e44ecfaecd0a47554b0bcba9" - integrity sha512-snSiT1UypkgGt2wxPqS6ImEUICbNCMb31yaxWrOLXjhlt2z2/IBpaOxzONExqSm4y5oLnAqjjRWu+wsDzK5yNQ== - dependencies: - semver "^6.3.0" +node-releases@^1.1.53: + version "1.1.58" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.58.tgz#8ee20eef30fa60e52755fcc0942def5a734fe935" + integrity sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg== nopt@2.1.x: version "2.1.2" @@ -7160,7 +7167,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -7248,14 +7255,17 @@ object-hash@^1.1.4: integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== object-inspect@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" - integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== object-is@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" - integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + version "1.1.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" + integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" @@ -7312,7 +7322,7 @@ object.pick@^1.2.0, object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0: +object.values@^1.1.0, object.values@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== @@ -7366,9 +7376,9 @@ onetime@^5.1.0: mimic-fn "^2.1.0" opencollective-postinstall@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" - integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== + version "2.0.3" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" + integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== opn@^5.5.0: version "5.5.0" @@ -7384,14 +7394,6 @@ optimist@0.3.5: dependencies: wordwrap "~0.0.2" -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - optimist@~0.3, optimist@~0.3.5: version "0.3.7" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.3.7.tgz#c90941ad59e4273328923074d2cf2e7cbc6ec0d9" @@ -7461,15 +7463,6 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" - integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== - dependencies: - execa "^1.0.0" - lcid "^2.0.0" - mem "^4.0.0" - os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -7493,11 +7486,6 @@ p-cancelable@^0.4.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" integrity sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ== -p-defer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" - integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= - p-event@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-event/-/p-event-1.3.0.tgz#8e6b4f4f65c72bc5b6fe28b75eda874f96a4a085" @@ -7517,21 +7505,11 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== - p-is-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" integrity sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4= -p-is-promise@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" - integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -7540,9 +7518,9 @@ p-limit@^1.1.0: p-try "^1.0.0" p-limit@^2.0.0, p-limit@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" - integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" @@ -7586,6 +7564,13 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-pipe@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" @@ -7655,7 +7640,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0: +parse-asn1@^5.0.0, parse-asn1@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== @@ -7783,7 +7768,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.5, path-parse@^1.0.6: +path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== @@ -7832,9 +7817,9 @@ pause@0.0.1: integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= pbkdf2@^3.0.3: - version "3.0.17" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" - integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== + version "3.1.1" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" + integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== dependencies: create-hash "^1.1.2" create-hmac "^1.1.4" @@ -7847,7 +7832,7 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= -picomatch@^2.0.5, picomatch@^2.2.1: +picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== @@ -7914,12 +7899,12 @@ pkg-up@^1.0.0: dependencies: find-up "^1.0.0" -pkg-up@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" - integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== +pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" + integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= dependencies: - find-up "^3.0.0" + find-up "^2.1.0" please-upgrade-node@^3.2.0: version "3.2.0" @@ -8279,9 +8264,9 @@ postcss@^6.0.1, postcss@^6.0.17, postcss@^6.0.23: supports-color "^5.4.0" postcss@^7.0.0: - version "7.0.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" - integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== + version "7.0.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" + integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -8308,9 +8293,9 @@ prepend-http@^2.0.0: integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= prettier@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.2.tgz#1ba8f3eb92231e769b7fcd7cb73ae1b6b74ade08" - integrity sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg== + version "2.0.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" + integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg== pretty-error@^2.0.2: version "2.1.1" @@ -8472,7 +8457,7 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -8549,7 +8534,7 @@ read-pkg@^2.0.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -8577,6 +8562,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + readline2@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" @@ -8640,9 +8632,9 @@ regenerate-unicode-properties@^8.2.0: regenerate "^1.4.0" regenerate@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" - integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== + version "1.4.1" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f" + integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A== regenerator-runtime@^0.13.4: version "0.13.5" @@ -8691,9 +8683,9 @@ regexpu-core@^4.6.0, regexpu-core@^4.7.0: unicode-match-property-value-ecmascript "^1.2.0" regjsgen@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" - integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== regjsparser@^0.6.4: version "0.6.4" @@ -8741,9 +8733,9 @@ repeating@^2.0.0: is-finite "^1.0.0" replace-ext@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" - integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== require-directory@^2.1.1: version "2.1.1" @@ -8825,24 +8817,17 @@ resolve@0.5.x: resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.5.1.tgz#15e4a222c4236bcd4cf85454412c2d0fb6524576" integrity sha1-FeSiIsQja81M+FRUQSwtD7ZSRXY= -resolve@^1.1.6, resolve@^1.1.7, resolve@~1.1.0: - version "1.1.7" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" - integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= - -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1: - version "1.15.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" - integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" -resolve@^1.3.2: - version "1.8.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26" - integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA== - dependencies: - path-parse "^1.0.5" +resolve@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= responselike@1.0.2: version "1.0.2" @@ -8934,11 +8919,9 @@ run-async@^0.1.0: once "^1.3.0" run-async@^2.2.0, run-async@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8" - integrity sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg== - dependencies: - is-promise "^2.1.0" + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-parallel@^1.1.9: version "1.1.9" @@ -8957,17 +8940,10 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= -rxjs@^6.3.3, rxjs@^6.5.3: - version "6.5.4" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" - integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== - dependencies: - tslib "^1.9.0" - -rxjs@^6.4.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" - integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== +rxjs@^6.4.0, rxjs@^6.5.3, rxjs@^6.5.5: + version "6.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" + integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== dependencies: tslib "^1.9.0" @@ -8976,10 +8952,10 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-regex@^1.1.0: version "1.1.0" @@ -9016,11 +8992,12 @@ schema-utils@^1.0.0: ajv-keywords "^3.1.0" schema-utils@^2.6.5: - version "2.6.5" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.5.tgz#c758f0a7e624263073d396e29cd40aa101152d8a" - integrity sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ== + version "2.7.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== dependencies: - ajv "^6.12.0" + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" ajv-keywords "^3.4.1" seek-bzip@^1.0.5: @@ -9106,10 +9083,12 @@ sentence-case@^2.1.0: no-case "^2.2.0" upper-case-first "^1.1.2" -serialize-javascript@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" - integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== +serialize-javascript@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" + integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== + dependencies: + randombytes "^2.1.0" serve-index@^1.9.1: version "1.9.1" @@ -9216,9 +9195,9 @@ sigmund@~1.0.0: integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== simple-is@~0.2.0: version "0.2.0" @@ -9244,6 +9223,24 @@ slice-ansi@^2.1.0: astral-regex "^1.0.0" is-fullwidth-code-point "^2.0.0" +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snake-case@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-2.1.0.tgz#41bdb1b73f30ec66a04d4e2cad1b76387d4d6d9f" @@ -9361,9 +9358,9 @@ source-map-resolve@^0.5.0: urix "^0.1.0" source-map-support@~0.5.12: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -9396,22 +9393,22 @@ source-map@~0.1.7: amdefine ">=0.0.4" spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" @@ -9451,6 +9448,11 @@ speed-measure-webpack-plugin@^1.2.3: dependencies: chalk "^2.0.1" +spinkit@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/spinkit/-/spinkit-2.0.1.tgz#aefcd0acfdf15a90aa8e1f069d7e618515891f74" + integrity sha512-oYBGY0GV1H1dX+ZdKnB6JVsYC1w/Xl20H111eb+WSS8nUYmlHgGb4y5buFSkzzceEeYYh5kMhXoAmoTpiQauiA== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" @@ -9557,7 +9559,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +string-width@^2.0.0, string-width@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -9574,7 +9576,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== @@ -9583,30 +9585,23 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trimleft@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" - integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== dependencies: define-properties "^1.1.3" - function-bind "^1.1.1" + es-abstract "^1.17.5" -string.prototype.trimright@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" - integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== dependencies: define-properties "^1.1.3" - function-bind "^1.1.1" + es-abstract "^1.17.5" -string_decoder@^1.0.0, string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -string_decoder@^1.1.1: +string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -9618,6 +9613,13 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + stringify-object@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" @@ -9711,13 +9713,6 @@ style-loader@^0.23.1: loader-utils "^1.1.0" schema-utils "^1.0.0" -supports-color@6.1.0, supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -9737,6 +9732,13 @@ supports-color@^5.3.0, supports-color@^5.4.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" @@ -9784,11 +9786,6 @@ swap-case@^1.1.0: lower-case "^1.1.1" upper-case "^1.1.1" -symbol-observable@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - table@^3.7.8: version "3.8.3" resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" @@ -9843,24 +9840,24 @@ tempfile@^2.0.0: uuid "^3.0.1" terser-webpack-plugin@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" - integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA== + version "1.4.4" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz#2c63544347324baafa9a56baaddf1634c8abfc2f" + integrity sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA== dependencies: cacache "^12.0.2" find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^2.1.2" + serialize-javascript "^3.1.0" source-map "^0.6.1" terser "^4.1.2" webpack-sources "^1.4.0" worker-farm "^1.7.0" terser@^4.1.2: - version "4.6.7" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.7.tgz#478d7f9394ec1907f0e488c5f6a6a9a2bad55e72" - integrity sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g== + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -9997,10 +9994,20 @@ trim-repeated@^1.0.0: dependencies: escape-string-regexp "^1.0.2" +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tslib@^1.9.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" - integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== tty-browserify@0.0.0: version "0.0.0" @@ -10063,12 +10070,9 @@ uglify-js@3.4.x: source-map "~0.6.1" uglify-js@^3.1.4: - version "3.8.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.8.0.tgz#f3541ae97b2f048d7e7e3aa4f39fd8a1f5d7a805" - integrity sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ== - dependencies: - commander "~2.20.3" - source-map "~0.6.1" + version "3.10.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.0.tgz#397a7e6e31ce820bfd1cb55b804ee140c587a9e7" + integrity sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA== uglify-js@~2.3: version "2.3.6" @@ -10085,9 +10089,9 @@ ui-select@^0.19.8: integrity sha1-dIYISKf9i8SU2YVtL2J3bqmGN8E= unbzip2-stream@^1.0.9: - version "1.3.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" - integrity sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg== + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== dependencies: buffer "^5.2.1" through "^2.3.8" @@ -10322,10 +10326,10 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -v8-compile-cache@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" - integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== +v8-compile-cache@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== v8flags@^2.0.10: version "2.1.1" @@ -10369,14 +10373,23 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== -watchpack@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" - integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== + dependencies: + chokidar "^2.1.8" + +watchpack@^1.6.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa" + integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g== dependencies: - chokidar "^2.0.2" graceful-fs "^4.1.2" neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.0" + watchpack-chokidar2 "^2.0.0" wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" @@ -10402,21 +10415,21 @@ webpack-build-notifier@^0.1.30: strip-ansi "^3.0.1" webpack-cli@^3.1.2: - version "3.3.11" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.11.tgz#3bf21889bf597b5d82c38f215135a411edfdc631" - integrity sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g== + version "3.3.12" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" + integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== dependencies: - chalk "2.4.2" - cross-spawn "6.0.5" - enhanced-resolve "4.1.0" - findup-sync "3.0.0" - global-modules "2.0.0" - import-local "2.0.0" - interpret "1.2.0" - loader-utils "1.2.3" - supports-color "6.1.0" - v8-compile-cache "2.0.3" - yargs "13.2.4" + chalk "^2.4.2" + cross-spawn "^6.0.5" + enhanced-resolve "^4.1.1" + findup-sync "^3.0.0" + global-modules "^2.0.0" + import-local "^2.0.0" + interpret "^1.4.0" + loader-utils "^1.4.0" + supports-color "^6.1.0" + v8-compile-cache "^2.1.1" + yargs "^13.3.2" webpack-dev-middleware@^3.7.2: version "3.7.2" @@ -10492,15 +10505,15 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-map "~0.6.1" webpack@^4.26.0: - version "4.42.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.42.1.tgz#ae707baf091f5ca3ef9c38b884287cfe8f1983ef" - integrity sha512-SGfYMigqEfdGchGhFFJ9KyRpQKnipvEvjc1TwrXEPCM6H5Wywu10ka8o3KGrMzSMxMQKt8aCHUFh5DaQ9UmyRg== + version "4.43.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.43.0.tgz#c48547b11d563224c561dad1172c8aa0b8a678e6" + integrity sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g== dependencies: "@webassemblyjs/ast" "1.9.0" "@webassemblyjs/helper-module-context" "1.9.0" "@webassemblyjs/wasm-edit" "1.9.0" "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.2.1" + acorn "^6.4.1" ajv "^6.10.2" ajv-keywords "^3.4.1" chrome-trace-event "^1.0.2" @@ -10517,7 +10530,7 @@ webpack@^4.26.0: schema-utils "^1.0.0" tapable "^1.1.3" terser-webpack-plugin "^1.4.3" - watchpack "^1.6.0" + watchpack "^1.6.1" webpack-sources "^1.4.1" websocket-driver@0.6.5: @@ -10528,18 +10541,18 @@ websocket-driver@0.6.5: websocket-extensions ">=0.1.1" websocket-driver@>=0.5.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" - integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: - http-parser-js ">=0.4.0 <0.4.11" + http-parser-js ">=0.5.1" safe-buffer ">=5.1.0" websocket-extensions ">=0.1.1" websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== whet.extend@~0.9.9: version "0.9.9" @@ -10585,6 +10598,11 @@ wordwrap@0.0.x, wordwrap@~0.0.2: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" @@ -10592,14 +10610,6 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -wrap-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba" - integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -10609,6 +10619,15 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -10680,14 +10699,12 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yaml@^1.7.2: - version "1.8.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.8.3.tgz#2f420fca58b68ce3a332d0ca64be1d191dd3f87a" - integrity sha512-X/v7VDnK+sxbQ2Imq4Jt2PRUsRsP7UcpSl3Llg6+NRRqWLIvxkMFYtH1FmvwNGYRKKPa+EPA4qDBlI9WVG1UKw== - dependencies: - "@babel/runtime" "^7.8.7" +yaml@^1.10.0, yaml@^1.7.2: + version "1.10.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== -yargs-parser@^13.1.0, yargs-parser@^13.1.2: +yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== @@ -10695,23 +10712,6 @@ yargs-parser@^13.1.0, yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs@13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" - integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - os-locale "^3.1.0" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.0" - yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" From 42aa8ceb0003f300ecab4e24b711b8341a992101 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 6 Jul 2020 10:35:13 +0300 Subject: [PATCH 043/195] refactor(edge-compute): enforce es6 good practices (#3961) * refactor(edge-groups): use es6 imports * refactor(edge-jobs): es6 imports * refactor(edge-stacks): use es6 imports * refactor(edge-compute): use es6 imports in components * refactor(edge-compute): use named imports --- .../associatedEndpointsDatatableController.js | 5 +---- .../{associatedEndpointsDatatable.js => index.js} | 6 +++++- ...{edge-groups-selector.html => edgeGroupsSelector.html} | 0 .../{edge-groups-selector.js => index.js} | 4 +++- .../components/edge-job-form/edgeJobFormController.js | 6 +----- .../components/edge-job-form/{edgeJobForm.js => index.js} | 4 +++- .../edgeJobResultsDatatableController.js | 3 ++- .../{edgeJobResultsDatatable.js => index.js} | 3 ++- .../{edgeJobsDatatable.js => index.js} | 0 .../edgeStackEndpointsDatatableController.js | 5 +---- .../{edgeStackEndpointsDatatable.js => index.js} | 6 +++++- .../edge-stack-status/edgeStackStatusController.js | 7 +------ .../edge-stack-status/{edgeStackStatus.js => index.js} | 3 ++- .../{edgeStacksDatatable.js => index.js} | 2 ++ .../edit-edge-stack-form/editEdgeStackFormController.js | 7 +------ .../{editEdgeStackForm.js => index.js} | 6 +++++- app/edge/components/group-form/groupFormController.js | 6 +----- app/edge/components/group-form/{groupForm.js => index.js} | 6 +++++- .../groups-datatable/groupsDatatableController.js | 5 +---- .../groups-datatable/{groupsDatatable.js => index.js} | 4 +++- .../createEdgeGroupView}/createEdgeGroupView.html | 0 .../createEdgeGroupView}/createEdgeGroupViewController.js | 7 +------ app/edge/views/edge-groups/createEdgeGroupView/index.js | 8 ++++++++ .../edgeGroupsView}/edgeGroupsView.html | 0 .../edgeGroupsView}/edgeGroupsViewController.js | 5 +---- .../edgeGroupsView/index.js} | 4 +++- .../editEdgeGroupView}/editEdgeGroupView.html | 0 .../editEdgeGroupView}/editEdgeGroupViewController.js | 7 +------ app/edge/views/edge-groups/editEdgeGroupView/index.js | 8 ++++++++ .../{create => createEdgeJobView}/createEdgeJobView.html | 0 .../createEdgeJobViewController.js | 7 +------ .../createEdgeJobView.js => createEdgeJobView/index.js} | 2 +- app/edge/views/edge-jobs/{edit => edgeJob}/edgeJob.html | 0 .../edge-jobs/{edit => edgeJob}/edgeJobController.js | 6 +----- .../views/edge-jobs/{edit/edgeJob.js => edgeJob/index.js} | 2 +- .../views/edge-jobs/{ => edgeJobsView}/edgeJobsView.html | 0 .../{ => edgeJobsView}/edgeJobsViewController.js | 6 +----- .../edge-jobs/{edgeJobsView.js => edgeJobsView/index.js} | 2 +- app/edge/views/edge-stacks/create/createEdgeStackView.js | 4 ---- .../createEdgeStackView.html | 0 .../createEdgeStackViewController.js | 6 +----- app/edge/views/edge-stacks/createEdgeStackView/index.js | 8 ++++++++ app/edge/views/edge-stacks/edgeStacksView.js | 4 ---- .../edge-stacks/{ => edgeStacksView}/edgeStacksView.html | 0 .../{ => edgeStacksView}/edgeStacksViewController.js | 6 +----- app/edge/views/edge-stacks/edgeStacksView/index.js | 8 ++++++++ app/edge/views/edge-stacks/edit/editEdgeStackView.js | 4 ---- .../{edit => editEdgeStackView}/editEdgeStackView.html | 0 .../editEdgeStackViewController.js | 6 +----- app/edge/views/edge-stacks/editEdgeStackView/index.js | 8 ++++++++ app/edge/views/groups/create/createEdgeGroupView.js | 4 ---- app/edge/views/groups/edit/editEdgeGroupView.js | 4 ---- 52 files changed, 99 insertions(+), 115 deletions(-) rename app/edge/components/associated-endpoints-datatable/{associatedEndpointsDatatable.js => index.js} (61%) rename app/edge/components/edge-groups-selector/{edge-groups-selector.html => edgeGroupsSelector.html} (100%) rename app/edge/components/edge-groups-selector/{edge-groups-selector.js => index.js} (61%) rename app/edge/components/edge-job-form/{edgeJobForm.js => index.js} (77%) rename app/edge/components/edge-job-results-datatable/{edgeJobResultsDatatable.js => index.js} (84%) rename app/edge/components/edge-jobs-datatable/{edgeJobsDatatable.js => index.js} (100%) rename app/edge/components/edge-stack-endpoints-datatable/{edgeStackEndpointsDatatable.js => index.js} (60%) rename app/edge/components/edge-stack-status/{edgeStackStatus.js => index.js} (65%) rename app/edge/components/edge-stacks-datatable/{edgeStacksDatatable.js => index.js} (91%) rename app/edge/components/edit-edge-stack-form/{editEdgeStackForm.js => index.js} (58%) rename app/edge/components/group-form/{groupForm.js => index.js} (66%) rename app/edge/components/groups-datatable/{groupsDatatable.js => index.js} (71%) rename app/edge/views/{groups/create => edge-groups/createEdgeGroupView}/createEdgeGroupView.html (100%) rename app/edge/views/{groups/create => edge-groups/createEdgeGroupView}/createEdgeGroupViewController.js (86%) create mode 100644 app/edge/views/edge-groups/createEdgeGroupView/index.js rename app/edge/views/{groups => edge-groups/edgeGroupsView}/edgeGroupsView.html (100%) rename app/edge/views/{groups => edge-groups/edgeGroupsView}/edgeGroupsViewController.js (87%) rename app/edge/views/{groups/edgeGroupsView.js => edge-groups/edgeGroupsView/index.js} (57%) rename app/edge/views/{groups/edit => edge-groups/editEdgeGroupView}/editEdgeGroupView.html (100%) rename app/edge/views/{groups/edit => edge-groups/editEdgeGroupView}/editEdgeGroupViewController.js (88%) create mode 100644 app/edge/views/edge-groups/editEdgeGroupView/index.js rename app/edge/views/edge-jobs/{create => createEdgeJobView}/createEdgeJobView.html (100%) rename app/edge/views/edge-jobs/{create => createEdgeJobView}/createEdgeJobViewController.js (89%) rename app/edge/views/edge-jobs/{create/createEdgeJobView.js => createEdgeJobView/index.js} (70%) rename app/edge/views/edge-jobs/{edit => edgeJob}/edgeJob.html (100%) rename app/edge/views/edge-jobs/{edit => edgeJob}/edgeJobController.js (96%) rename app/edge/views/edge-jobs/{edit/edgeJob.js => edgeJob/index.js} (73%) rename app/edge/views/edge-jobs/{ => edgeJobsView}/edgeJobsView.html (100%) rename app/edge/views/edge-jobs/{ => edgeJobsView}/edgeJobsViewController.js (88%) rename app/edge/views/edge-jobs/{edgeJobsView.js => edgeJobsView/index.js} (72%) delete mode 100644 app/edge/views/edge-stacks/create/createEdgeStackView.js rename app/edge/views/edge-stacks/{create => createEdgeStackView}/createEdgeStackView.html (100%) rename app/edge/views/edge-stacks/{create => createEdgeStackView}/createEdgeStackViewController.js (95%) create mode 100644 app/edge/views/edge-stacks/createEdgeStackView/index.js delete mode 100644 app/edge/views/edge-stacks/edgeStacksView.js rename app/edge/views/edge-stacks/{ => edgeStacksView}/edgeStacksView.html (100%) rename app/edge/views/edge-stacks/{ => edgeStacksView}/edgeStacksViewController.js (85%) create mode 100644 app/edge/views/edge-stacks/edgeStacksView/index.js delete mode 100644 app/edge/views/edge-stacks/edit/editEdgeStackView.js rename app/edge/views/edge-stacks/{edit => editEdgeStackView}/editEdgeStackView.html (100%) rename app/edge/views/edge-stacks/{edit => editEdgeStackView}/editEdgeStackViewController.js (93%) create mode 100644 app/edge/views/edge-stacks/editEdgeStackView/index.js delete mode 100644 app/edge/views/groups/create/createEdgeGroupView.js delete mode 100644 app/edge/views/groups/edit/editEdgeGroupView.js diff --git a/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatableController.js b/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatableController.js index 8ba5bdc19..9b7b2a836 100644 --- a/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatableController.js +++ b/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatableController.js @@ -1,6 +1,6 @@ import angular from 'angular'; -class AssociatedEndpointsDatatableController { +export class AssociatedEndpointsDatatableController { constructor($scope, $controller, DatatableService, PaginationService) { this.extendGenericController($controller, $scope); this.DatatableService = DatatableService; @@ -98,6 +98,3 @@ class AssociatedEndpointsDatatableController { }); } } - -angular.module('portainer.edge').controller('AssociatedEndpointsDatatableController', AssociatedEndpointsDatatableController); -export default AssociatedEndpointsDatatableController; diff --git a/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatable.js b/app/edge/components/associated-endpoints-datatable/index.js similarity index 61% rename from app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatable.js rename to app/edge/components/associated-endpoints-datatable/index.js index 93d4b748f..7defaa58a 100644 --- a/app/edge/components/associated-endpoints-datatable/associatedEndpointsDatatable.js +++ b/app/edge/components/associated-endpoints-datatable/index.js @@ -1,6 +1,10 @@ +import angular from 'angular'; + +import { AssociatedEndpointsDatatableController } from './associatedEndpointsDatatableController'; + angular.module('portainer.edge').component('associatedEndpointsDatatable', { templateUrl: './associatedEndpointsDatatable.html', - controller: 'AssociatedEndpointsDatatableController', + controller: AssociatedEndpointsDatatableController, bindings: { titleText: '@', titleIcon: '@', diff --git a/app/edge/components/edge-groups-selector/edge-groups-selector.html b/app/edge/components/edge-groups-selector/edgeGroupsSelector.html similarity index 100% rename from app/edge/components/edge-groups-selector/edge-groups-selector.html rename to app/edge/components/edge-groups-selector/edgeGroupsSelector.html diff --git a/app/edge/components/edge-groups-selector/edge-groups-selector.js b/app/edge/components/edge-groups-selector/index.js similarity index 61% rename from app/edge/components/edge-groups-selector/edge-groups-selector.js rename to app/edge/components/edge-groups-selector/index.js index 66f8c429d..0a7f7cbb3 100644 --- a/app/edge/components/edge-groups-selector/edge-groups-selector.js +++ b/app/edge/components/edge-groups-selector/index.js @@ -1,5 +1,7 @@ +import angular from 'angular'; + angular.module('portainer.edge').component('edgeGroupsSelector', { - templateUrl: './edge-groups-selector.html', + templateUrl: './edgeGroupsSelector.html', bindings: { model: '=', items: '<', diff --git a/app/edge/components/edge-job-form/edgeJobFormController.js b/app/edge/components/edge-job-form/edgeJobFormController.js index cbf491187..52eabb5f6 100644 --- a/app/edge/components/edge-job-form/edgeJobFormController.js +++ b/app/edge/components/edge-job-form/edgeJobFormController.js @@ -1,8 +1,7 @@ -import angular from 'angular'; import _ from 'lodash-es'; import moment from 'moment'; -class EdgeJobFormController { +export class EdgeJobFormController { /* @ngInject */ constructor() { this.state = { @@ -102,6 +101,3 @@ function datetimeToCron(datetime) { var date = moment(datetime); return [date.minutes(), date.hours(), date.date(), date.month() + 1, '*'].join(' '); } - -angular.module('portainer.edge').controller('EdgeJobFormController', EdgeJobFormController); -export default EdgeJobFormController; diff --git a/app/edge/components/edge-job-form/edgeJobForm.js b/app/edge/components/edge-job-form/index.js similarity index 77% rename from app/edge/components/edge-job-form/edgeJobForm.js rename to app/edge/components/edge-job-form/index.js index bc987d696..58c34cefd 100644 --- a/app/edge/components/edge-job-form/edgeJobForm.js +++ b/app/edge/components/edge-job-form/index.js @@ -1,4 +1,6 @@ -import EdgeJobFormController from './edgeJobFormController'; +import angular from 'angular'; + +import { EdgeJobFormController } from './edgeJobFormController'; angular.module('portainer.edge').component('edgeJobForm', { templateUrl: './edgeJobForm.html', diff --git a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatableController.js b/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatableController.js index a66aaee35..92217e3cd 100644 --- a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatableController.js +++ b/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatableController.js @@ -1,6 +1,7 @@ +import angular from 'angular'; import _ from 'lodash-es'; -export default class EdgeJobResultsDatatableController { +export class EdgeJobResultsDatatableController { /* @ngInject */ constructor($controller, $scope, $state) { this.$state = $state; diff --git a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.js b/app/edge/components/edge-job-results-datatable/index.js similarity index 84% rename from app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.js rename to app/edge/components/edge-job-results-datatable/index.js index 96bd8094a..132690633 100644 --- a/app/edge/components/edge-job-results-datatable/edgeJobResultsDatatable.js +++ b/app/edge/components/edge-job-results-datatable/index.js @@ -1,5 +1,6 @@ import angular from 'angular'; -import EdgeJobResultsDatatableController from './edgeJobResultsDatatableController'; + +import { EdgeJobResultsDatatableController } from './edgeJobResultsDatatableController'; import './edgeJobResultsDatatable.css'; angular.module('portainer.edge').component('edgeJobResultsDatatable', { diff --git a/app/edge/components/edge-jobs-datatable/edgeJobsDatatable.js b/app/edge/components/edge-jobs-datatable/index.js similarity index 100% rename from app/edge/components/edge-jobs-datatable/edgeJobsDatatable.js rename to app/edge/components/edge-jobs-datatable/index.js diff --git a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js index dec0c08ed..59113c793 100644 --- a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js +++ b/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatableController.js @@ -1,6 +1,6 @@ import angular from 'angular'; -class EdgeStackEndpointsDatatableController { +export class EdgeStackEndpointsDatatableController { constructor($async, $scope, $controller, DatatableService, PaginationService, Notifications) { this.extendGenericController($controller, $scope); this.DatatableService = DatatableService; @@ -106,6 +106,3 @@ class EdgeStackEndpointsDatatableController { } } } - -angular.module('portainer.edge').controller('EdgeStackEndpointsDatatableController', EdgeStackEndpointsDatatableController); -export default EdgeStackEndpointsDatatableController; diff --git a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.js b/app/edge/components/edge-stack-endpoints-datatable/index.js similarity index 60% rename from app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.js rename to app/edge/components/edge-stack-endpoints-datatable/index.js index 85783034d..a0dbac847 100644 --- a/app/edge/components/edge-stack-endpoints-datatable/edgeStackEndpointsDatatable.js +++ b/app/edge/components/edge-stack-endpoints-datatable/index.js @@ -1,6 +1,10 @@ +import angular from 'angular'; + +import { EdgeStackEndpointsDatatableController } from './edgeStackEndpointsDatatableController'; + angular.module('portainer.edge').component('edgeStackEndpointsDatatable', { templateUrl: './edgeStackEndpointsDatatable.html', - controller: 'EdgeStackEndpointsDatatableController', + controller: EdgeStackEndpointsDatatableController, bindings: { titleText: '@', titleIcon: '@', diff --git a/app/edge/components/edge-stack-status/edgeStackStatusController.js b/app/edge/components/edge-stack-status/edgeStackStatusController.js index 7ce46ebb8..002af88be 100644 --- a/app/edge/components/edge-stack-status/edgeStackStatusController.js +++ b/app/edge/components/edge-stack-status/edgeStackStatusController.js @@ -1,12 +1,10 @@ -import angular from 'angular'; - const statusMap = { 1: 'ok', 2: 'error', 3: 'acknowledged', }; -class EdgeStackStatusController { +export class EdgeStackStatusController { $onChanges({ stackStatus }) { if (!stackStatus || !stackStatus.currentValue) { return; @@ -20,6 +18,3 @@ class EdgeStackStatusController { this.status = aggregateStatus; } } - -angular.module('portainer.edge').controller('EdgeStackStatusController', EdgeStackStatusController); -export default EdgeStackStatusController; diff --git a/app/edge/components/edge-stack-status/edgeStackStatus.js b/app/edge/components/edge-stack-status/index.js similarity index 65% rename from app/edge/components/edge-stack-status/edgeStackStatus.js rename to app/edge/components/edge-stack-status/index.js index a0ac9be38..11af09332 100644 --- a/app/edge/components/edge-stack-status/edgeStackStatus.js +++ b/app/edge/components/edge-stack-status/index.js @@ -1,10 +1,11 @@ import angular from 'angular'; +import { EdgeStackStatusController } from './edgeStackStatusController'; import './edgeStackStatus.css'; angular.module('portainer.edge').component('edgeStackStatus', { templateUrl: './edgeStackStatus.html', - controller: 'EdgeStackStatusController', + controller: EdgeStackStatusController, bindings: { stackStatus: '<', }, diff --git a/app/edge/components/edge-stacks-datatable/edgeStacksDatatable.js b/app/edge/components/edge-stacks-datatable/index.js similarity index 91% rename from app/edge/components/edge-stacks-datatable/edgeStacksDatatable.js rename to app/edge/components/edge-stacks-datatable/index.js index 6d05005fe..dcc6b01a6 100644 --- a/app/edge/components/edge-stacks-datatable/edgeStacksDatatable.js +++ b/app/edge/components/edge-stacks-datatable/index.js @@ -1,3 +1,5 @@ +import angular from 'angular'; + angular.module('portainer.edge').component('edgeStacksDatatable', { templateUrl: './edgeStacksDatatable.html', controller: 'GenericDatatableController', diff --git a/app/edge/components/edit-edge-stack-form/editEdgeStackFormController.js b/app/edge/components/edit-edge-stack-form/editEdgeStackFormController.js index 0c836db04..a35a7ed4e 100644 --- a/app/edge/components/edit-edge-stack-form/editEdgeStackFormController.js +++ b/app/edge/components/edit-edge-stack-form/editEdgeStackFormController.js @@ -1,6 +1,4 @@ -import angular from 'angular'; - -class EditEdgeStackFormController { +export class EditEdgeStackFormController { constructor() { this.editorUpdate = this.editorUpdate.bind(this); } @@ -9,6 +7,3 @@ class EditEdgeStackFormController { this.model.StackFileContent = cm.getValue(); } } - -angular.module('portainer.edge').controller('EditEdgeStackFormController', EditEdgeStackFormController); -export default EditEdgeStackFormController; diff --git a/app/edge/components/edit-edge-stack-form/editEdgeStackForm.js b/app/edge/components/edit-edge-stack-form/index.js similarity index 58% rename from app/edge/components/edit-edge-stack-form/editEdgeStackForm.js rename to app/edge/components/edit-edge-stack-form/index.js index 479a6876e..f0456524d 100644 --- a/app/edge/components/edit-edge-stack-form/editEdgeStackForm.js +++ b/app/edge/components/edit-edge-stack-form/index.js @@ -1,6 +1,10 @@ +import angular from 'angular'; + +import { EditEdgeStackFormController } from './editEdgeStackFormController'; + angular.module('portainer.edge').component('editEdgeStackForm', { templateUrl: './editEdgeStackForm.html', - controller: 'EditEdgeStackFormController', + controller: EditEdgeStackFormController, bindings: { model: '<', actionInProgress: '<', diff --git a/app/edge/components/group-form/groupFormController.js b/app/edge/components/group-form/groupFormController.js index 8b9c325bc..7b2283647 100644 --- a/app/edge/components/group-form/groupFormController.js +++ b/app/edge/components/group-form/groupFormController.js @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash-es'; -class EdgeGroupFormController { +export class EdgeGroupFormController { /* @ngInject */ constructor(EndpointService, $async, $scope) { this.EndpointService = EndpointService; @@ -59,6 +58,3 @@ class EdgeGroupFormController { this.endpoints.state.totalCount = totalCount; } } - -angular.module('portainer.edge').controller('EdgeGroupFormController', EdgeGroupFormController); -export default EdgeGroupFormController; diff --git a/app/edge/components/group-form/groupForm.js b/app/edge/components/group-form/index.js similarity index 66% rename from app/edge/components/group-form/groupForm.js rename to app/edge/components/group-form/index.js index 2b5f66091..98316f88e 100644 --- a/app/edge/components/group-form/groupForm.js +++ b/app/edge/components/group-form/index.js @@ -1,6 +1,10 @@ +import angular from 'angular'; + +import { EdgeGroupFormController } from './groupFormController'; + angular.module('portainer.edge').component('edgeGroupForm', { templateUrl: './groupForm.html', - controller: 'EdgeGroupFormController', + controller: EdgeGroupFormController, bindings: { model: '<', groups: '<', diff --git a/app/edge/components/groups-datatable/groupsDatatableController.js b/app/edge/components/groups-datatable/groupsDatatableController.js index afe9468d7..f3371692c 100644 --- a/app/edge/components/groups-datatable/groupsDatatableController.js +++ b/app/edge/components/groups-datatable/groupsDatatableController.js @@ -1,6 +1,6 @@ import angular from 'angular'; -class EdgeGroupsDatatableController { +export class EdgeGroupsDatatableController { constructor($scope, $controller) { const allowSelection = this.allowSelection; angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); @@ -14,6 +14,3 @@ class EdgeGroupsDatatableController { return !item.HasEdgeStack; } } - -angular.module('portainer.edge').controller('EdgeGroupsDatatableController', EdgeGroupsDatatableController); -export default EdgeGroupsDatatableController; diff --git a/app/edge/components/groups-datatable/groupsDatatable.js b/app/edge/components/groups-datatable/index.js similarity index 71% rename from app/edge/components/groups-datatable/groupsDatatable.js rename to app/edge/components/groups-datatable/index.js index 3749e8eb6..409551a94 100644 --- a/app/edge/components/groups-datatable/groupsDatatable.js +++ b/app/edge/components/groups-datatable/index.js @@ -1,8 +1,10 @@ import angular from 'angular'; +import { EdgeGroupsDatatableController } from './groupsDatatableController'; + angular.module('portainer.edge').component('edgeGroupsDatatable', { templateUrl: './groupsDatatable.html', - controller: 'EdgeGroupsDatatableController', + controller: EdgeGroupsDatatableController, bindings: { dataset: '<', titleIcon: '@', diff --git a/app/edge/views/groups/create/createEdgeGroupView.html b/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupView.html similarity index 100% rename from app/edge/views/groups/create/createEdgeGroupView.html rename to app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupView.html diff --git a/app/edge/views/groups/create/createEdgeGroupViewController.js b/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupViewController.js similarity index 86% rename from app/edge/views/groups/create/createEdgeGroupViewController.js rename to app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupViewController.js index 401f4c814..ffa6a8a2c 100644 --- a/app/edge/views/groups/create/createEdgeGroupViewController.js +++ b/app/edge/views/edge-groups/createEdgeGroupView/createEdgeGroupViewController.js @@ -1,6 +1,4 @@ -import angular from 'angular'; - -class CreateEdgeGroupController { +export class CreateEdgeGroupController { /* @ngInject */ constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async) { this.EdgeGroupService = EdgeGroupService; @@ -51,6 +49,3 @@ class CreateEdgeGroupController { } } } - -angular.module('portainer.edge').controller('CreateEdgeGroupController', CreateEdgeGroupController); -export default CreateEdgeGroupController; diff --git a/app/edge/views/edge-groups/createEdgeGroupView/index.js b/app/edge/views/edge-groups/createEdgeGroupView/index.js new file mode 100644 index 000000000..c7c22d3ef --- /dev/null +++ b/app/edge/views/edge-groups/createEdgeGroupView/index.js @@ -0,0 +1,8 @@ +import angular from 'angular'; + +import { CreateEdgeGroupController } from './createEdgeGroupViewController'; + +angular.module('portainer.edge').component('createEdgeGroupView', { + templateUrl: './createEdgeGroupView.html', + controller: CreateEdgeGroupController, +}); diff --git a/app/edge/views/groups/edgeGroupsView.html b/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsView.html similarity index 100% rename from app/edge/views/groups/edgeGroupsView.html rename to app/edge/views/edge-groups/edgeGroupsView/edgeGroupsView.html diff --git a/app/edge/views/groups/edgeGroupsViewController.js b/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsViewController.js similarity index 87% rename from app/edge/views/groups/edgeGroupsViewController.js rename to app/edge/views/edge-groups/edgeGroupsView/edgeGroupsViewController.js index 1d7f6fc32..3bc259fcf 100644 --- a/app/edge/views/groups/edgeGroupsViewController.js +++ b/app/edge/views/edge-groups/edgeGroupsView/edgeGroupsViewController.js @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash-es'; -class EdgeGroupsController { +export class EdgeGroupsController { /* @ngInject */ constructor($async, $state, EdgeGroupService, Notifications) { this.$async = $async; @@ -41,5 +40,3 @@ class EdgeGroupsController { this.$state.reload(); } } - -angular.module('portainer.edge').controller('EdgeGroupsController', EdgeGroupsController); diff --git a/app/edge/views/groups/edgeGroupsView.js b/app/edge/views/edge-groups/edgeGroupsView/index.js similarity index 57% rename from app/edge/views/groups/edgeGroupsView.js rename to app/edge/views/edge-groups/edgeGroupsView/index.js index 09f0202af..2c511421b 100644 --- a/app/edge/views/groups/edgeGroupsView.js +++ b/app/edge/views/edge-groups/edgeGroupsView/index.js @@ -1,6 +1,8 @@ import angular from 'angular'; +import { EdgeGroupsController } from './edgeGroupsViewController'; + angular.module('portainer.edge').component('edgeGroupsView', { templateUrl: './edgeGroupsView.html', - controller: 'EdgeGroupsController', + controller: EdgeGroupsController, }); diff --git a/app/edge/views/groups/edit/editEdgeGroupView.html b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupView.html similarity index 100% rename from app/edge/views/groups/edit/editEdgeGroupView.html rename to app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupView.html diff --git a/app/edge/views/groups/edit/editEdgeGroupViewController.js b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js similarity index 88% rename from app/edge/views/groups/edit/editEdgeGroupViewController.js rename to app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js index 1685fbb50..c3c6bc3f4 100644 --- a/app/edge/views/groups/edit/editEdgeGroupViewController.js +++ b/app/edge/views/edge-groups/editEdgeGroupView/editEdgeGroupViewController.js @@ -1,6 +1,4 @@ -import angular from 'angular'; - -class EditEdgeGroupController { +export class EditEdgeGroupController { /* @ngInject */ constructor(EdgeGroupService, GroupService, TagService, Notifications, $state, $async, EndpointService, EndpointHelper) { this.EdgeGroupService = EdgeGroupService; @@ -51,6 +49,3 @@ class EditEdgeGroupController { } } } - -angular.module('portainer.edge').controller('EditEdgeGroupController', EditEdgeGroupController); -export default EditEdgeGroupController; diff --git a/app/edge/views/edge-groups/editEdgeGroupView/index.js b/app/edge/views/edge-groups/editEdgeGroupView/index.js new file mode 100644 index 000000000..24c63736e --- /dev/null +++ b/app/edge/views/edge-groups/editEdgeGroupView/index.js @@ -0,0 +1,8 @@ +import angular from 'angular'; + +import { EditEdgeGroupController } from './editEdgeGroupViewController'; + +angular.module('portainer.edge').component('editEdgeGroupView', { + templateUrl: './editEdgeGroupView.html', + controller: EditEdgeGroupController, +}); diff --git a/app/edge/views/edge-jobs/create/createEdgeJobView.html b/app/edge/views/edge-jobs/createEdgeJobView/createEdgeJobView.html similarity index 100% rename from app/edge/views/edge-jobs/create/createEdgeJobView.html rename to app/edge/views/edge-jobs/createEdgeJobView/createEdgeJobView.html diff --git a/app/edge/views/edge-jobs/create/createEdgeJobViewController.js b/app/edge/views/edge-jobs/createEdgeJobView/createEdgeJobViewController.js similarity index 89% rename from app/edge/views/edge-jobs/create/createEdgeJobViewController.js rename to app/edge/views/edge-jobs/createEdgeJobView/createEdgeJobViewController.js index 155958b55..2380c1dff 100644 --- a/app/edge/views/edge-jobs/create/createEdgeJobViewController.js +++ b/app/edge/views/edge-jobs/createEdgeJobView/createEdgeJobViewController.js @@ -1,6 +1,4 @@ -import angular from 'angular'; - -class CreateEdgeJobController { +export class CreateEdgeJobViewController { constructor($async, $q, $state, EdgeJobService, GroupService, Notifications, TagService) { this.state = { actionInProgress: false, @@ -63,6 +61,3 @@ class CreateEdgeJobController { } } } - -angular.module('portainer.edge').controller('CreateEdgeJobController', CreateEdgeJobController); -export default CreateEdgeJobController; diff --git a/app/edge/views/edge-jobs/create/createEdgeJobView.js b/app/edge/views/edge-jobs/createEdgeJobView/index.js similarity index 70% rename from app/edge/views/edge-jobs/create/createEdgeJobView.js rename to app/edge/views/edge-jobs/createEdgeJobView/index.js index 4daa6dae9..ecddfb5ae 100644 --- a/app/edge/views/edge-jobs/create/createEdgeJobView.js +++ b/app/edge/views/edge-jobs/createEdgeJobView/index.js @@ -1,5 +1,5 @@ import angular from 'angular'; -import CreateEdgeJobViewController from './createEdgeJobViewController'; +import { CreateEdgeJobViewController } from './createEdgeJobViewController'; angular.module('portainer.edge').component('createEdgeJobView', { templateUrl: './createEdgeJobView.html', diff --git a/app/edge/views/edge-jobs/edit/edgeJob.html b/app/edge/views/edge-jobs/edgeJob/edgeJob.html similarity index 100% rename from app/edge/views/edge-jobs/edit/edgeJob.html rename to app/edge/views/edge-jobs/edgeJob/edgeJob.html diff --git a/app/edge/views/edge-jobs/edit/edgeJobController.js b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js similarity index 96% rename from app/edge/views/edge-jobs/edit/edgeJobController.js rename to app/edge/views/edge-jobs/edgeJob/edgeJobController.js index 41e757f10..ab5c84dab 100644 --- a/app/edge/views/edge-jobs/edit/edgeJobController.js +++ b/app/edge/views/edge-jobs/edgeJob/edgeJobController.js @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash-es'; -class EdgeJobController { +export class EdgeJobController { constructor($async, $q, $state, EdgeJobService, EndpointService, FileSaver, GroupService, HostBrowserService, Notifications, TagService) { this.state = { actionInProgress: false, @@ -154,6 +153,3 @@ class EdgeJobController { } } } - -angular.module('portainer.edge').controller('EdgeJobController', EdgeJobController); -export default EdgeJobController; diff --git a/app/edge/views/edge-jobs/edit/edgeJob.js b/app/edge/views/edge-jobs/edgeJob/index.js similarity index 73% rename from app/edge/views/edge-jobs/edit/edgeJob.js rename to app/edge/views/edge-jobs/edgeJob/index.js index f2395289f..d23dac592 100644 --- a/app/edge/views/edge-jobs/edit/edgeJob.js +++ b/app/edge/views/edge-jobs/edgeJob/index.js @@ -1,5 +1,5 @@ import angular from 'angular'; -import EdgeJobController from './edgeJobController'; +import { EdgeJobController } from './edgeJobController'; angular.module('portainer.edge').component('edgeJobView', { templateUrl: './edgeJob.html', diff --git a/app/edge/views/edge-jobs/edgeJobsView.html b/app/edge/views/edge-jobs/edgeJobsView/edgeJobsView.html similarity index 100% rename from app/edge/views/edge-jobs/edgeJobsView.html rename to app/edge/views/edge-jobs/edgeJobsView/edgeJobsView.html diff --git a/app/edge/views/edge-jobs/edgeJobsViewController.js b/app/edge/views/edge-jobs/edgeJobsView/edgeJobsViewController.js similarity index 88% rename from app/edge/views/edge-jobs/edgeJobsViewController.js rename to app/edge/views/edge-jobs/edgeJobsView/edgeJobsViewController.js index 85a4394d1..3e6fb611e 100644 --- a/app/edge/views/edge-jobs/edgeJobsViewController.js +++ b/app/edge/views/edge-jobs/edgeJobsView/edgeJobsViewController.js @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash-es'; -class EdgeJobsController { +export class EdgeJobsViewController { constructor($async, $state, EdgeJobService, ModalService, Notifications) { this.$async = $async; this.$state = $state; @@ -51,6 +50,3 @@ class EdgeJobsController { } } } - -angular.module('portainer.edge').controller('EdgeJobsController', EdgeJobsController); -export default EdgeJobsController; diff --git a/app/edge/views/edge-jobs/edgeJobsView.js b/app/edge/views/edge-jobs/edgeJobsView/index.js similarity index 72% rename from app/edge/views/edge-jobs/edgeJobsView.js rename to app/edge/views/edge-jobs/edgeJobsView/index.js index c87dd4023..124b6e666 100644 --- a/app/edge/views/edge-jobs/edgeJobsView.js +++ b/app/edge/views/edge-jobs/edgeJobsView/index.js @@ -1,5 +1,5 @@ import angular from 'angular'; -import EdgeJobsViewController from './edgeJobsViewController'; +import { EdgeJobsViewController } from './edgeJobsViewController'; angular.module('portainer.edge').component('edgeJobsView', { templateUrl: './edgeJobsView.html', diff --git a/app/edge/views/edge-stacks/create/createEdgeStackView.js b/app/edge/views/edge-stacks/create/createEdgeStackView.js deleted file mode 100644 index 5b5403337..000000000 --- a/app/edge/views/edge-stacks/create/createEdgeStackView.js +++ /dev/null @@ -1,4 +0,0 @@ -angular.module('portainer.edge').component('createEdgeStackView', { - templateUrl: './createEdgeStackView.html', - controller: 'CreateEdgeStackViewController', -}); diff --git a/app/edge/views/edge-stacks/create/createEdgeStackView.html b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html similarity index 100% rename from app/edge/views/edge-stacks/create/createEdgeStackView.html rename to app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html diff --git a/app/edge/views/edge-stacks/create/createEdgeStackViewController.js b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js similarity index 95% rename from app/edge/views/edge-stacks/create/createEdgeStackViewController.js rename to app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js index aff2e517d..e9f11dcf9 100644 --- a/app/edge/views/edge-stacks/create/createEdgeStackViewController.js +++ b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackViewController.js @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash-es'; -class CreateEdgeStackViewController { +export class CreateEdgeStackViewController { constructor($state, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async) { Object.assign(this, { $state, EdgeStackService, EdgeGroupService, EdgeTemplateService, Notifications, FormHelper, $async }); @@ -151,6 +150,3 @@ class CreateEdgeStackViewController { this.formValues.StackFileContent = cm.getValue(); } } - -angular.module('portainer.edge').controller('CreateEdgeStackViewController', CreateEdgeStackViewController); -export default CreateEdgeStackViewController; diff --git a/app/edge/views/edge-stacks/createEdgeStackView/index.js b/app/edge/views/edge-stacks/createEdgeStackView/index.js new file mode 100644 index 000000000..29206fe54 --- /dev/null +++ b/app/edge/views/edge-stacks/createEdgeStackView/index.js @@ -0,0 +1,8 @@ +import angular from 'angular'; + +import { CreateEdgeStackViewController } from './createEdgeStackViewController'; + +angular.module('portainer.edge').component('createEdgeStackView', { + templateUrl: './createEdgeStackView.html', + controller: CreateEdgeStackViewController, +}); diff --git a/app/edge/views/edge-stacks/edgeStacksView.js b/app/edge/views/edge-stacks/edgeStacksView.js deleted file mode 100644 index 27e58493c..000000000 --- a/app/edge/views/edge-stacks/edgeStacksView.js +++ /dev/null @@ -1,4 +0,0 @@ -angular.module('portainer.edge').component('edgeStacksView', { - templateUrl: './edgeStacksView.html', - controller: 'EdgeStacksViewController', -}); diff --git a/app/edge/views/edge-stacks/edgeStacksView.html b/app/edge/views/edge-stacks/edgeStacksView/edgeStacksView.html similarity index 100% rename from app/edge/views/edge-stacks/edgeStacksView.html rename to app/edge/views/edge-stacks/edgeStacksView/edgeStacksView.html diff --git a/app/edge/views/edge-stacks/edgeStacksViewController.js b/app/edge/views/edge-stacks/edgeStacksView/edgeStacksViewController.js similarity index 85% rename from app/edge/views/edge-stacks/edgeStacksViewController.js rename to app/edge/views/edge-stacks/edgeStacksView/edgeStacksViewController.js index 9aae3770d..0c68f2af1 100644 --- a/app/edge/views/edge-stacks/edgeStacksViewController.js +++ b/app/edge/views/edge-stacks/edgeStacksView/edgeStacksViewController.js @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash-es'; -class EdgeStacksViewController { +export class EdgeStacksViewController { constructor($state, Notifications, EdgeStackService, $scope, $async) { this.$state = $state; this.Notifications = Notifications; @@ -47,6 +46,3 @@ class EdgeStacksViewController { } } } - -angular.module('portainer.edge').controller('EdgeStacksViewController', EdgeStacksViewController); -export default EdgeStacksViewController; diff --git a/app/edge/views/edge-stacks/edgeStacksView/index.js b/app/edge/views/edge-stacks/edgeStacksView/index.js new file mode 100644 index 000000000..663c6ea41 --- /dev/null +++ b/app/edge/views/edge-stacks/edgeStacksView/index.js @@ -0,0 +1,8 @@ +import angular from 'angular'; + +import { EdgeStacksViewController } from './edgeStacksViewController'; + +angular.module('portainer.edge').component('edgeStacksView', { + templateUrl: './edgeStacksView.html', + controller: EdgeStacksViewController, +}); diff --git a/app/edge/views/edge-stacks/edit/editEdgeStackView.js b/app/edge/views/edge-stacks/edit/editEdgeStackView.js deleted file mode 100644 index 9f64573d5..000000000 --- a/app/edge/views/edge-stacks/edit/editEdgeStackView.js +++ /dev/null @@ -1,4 +0,0 @@ -angular.module('portainer.edge').component('editEdgeStackView', { - templateUrl: './editEdgeStackView.html', - controller: 'EditEdgeStackViewController', -}); diff --git a/app/edge/views/edge-stacks/edit/editEdgeStackView.html b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html similarity index 100% rename from app/edge/views/edge-stacks/edit/editEdgeStackView.html rename to app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackView.html diff --git a/app/edge/views/edge-stacks/edit/editEdgeStackViewController.js b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js similarity index 93% rename from app/edge/views/edge-stacks/edit/editEdgeStackViewController.js rename to app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js index 018cc1c51..bc53df983 100644 --- a/app/edge/views/edge-stacks/edit/editEdgeStackViewController.js +++ b/app/edge/views/edge-stacks/editEdgeStackView/editEdgeStackViewController.js @@ -1,7 +1,6 @@ -import angular from 'angular'; import _ from 'lodash-es'; -class EditEdgeStackViewController { +export class EditEdgeStackViewController { constructor($async, $state, EdgeGroupService, EdgeStackService, EndpointService, Notifications) { this.$async = $async; this.$state = $state; @@ -91,6 +90,3 @@ class EditEdgeStackViewController { } } } - -angular.module('portainer.edge').controller('EditEdgeStackViewController', EditEdgeStackViewController); -export default EditEdgeStackViewController; diff --git a/app/edge/views/edge-stacks/editEdgeStackView/index.js b/app/edge/views/edge-stacks/editEdgeStackView/index.js new file mode 100644 index 000000000..f81791347 --- /dev/null +++ b/app/edge/views/edge-stacks/editEdgeStackView/index.js @@ -0,0 +1,8 @@ +import angular from 'angular'; + +import { EditEdgeStackViewController } from './editEdgeStackViewController'; + +angular.module('portainer.edge').component('editEdgeStackView', { + templateUrl: './editEdgeStackView.html', + controller: EditEdgeStackViewController, +}); diff --git a/app/edge/views/groups/create/createEdgeGroupView.js b/app/edge/views/groups/create/createEdgeGroupView.js deleted file mode 100644 index e6778e728..000000000 --- a/app/edge/views/groups/create/createEdgeGroupView.js +++ /dev/null @@ -1,4 +0,0 @@ -angular.module('portainer.edge').component('createEdgeGroupView', { - templateUrl: './createEdgeGroupView.html', - controller: 'CreateEdgeGroupController', -}); diff --git a/app/edge/views/groups/edit/editEdgeGroupView.js b/app/edge/views/groups/edit/editEdgeGroupView.js deleted file mode 100644 index 7cb21ef3b..000000000 --- a/app/edge/views/groups/edit/editEdgeGroupView.js +++ /dev/null @@ -1,4 +0,0 @@ -angular.module('portainer.edge').component('editEdgeGroupView', { - templateUrl: './editEdgeGroupView.html', - controller: 'EditEdgeGroupController', -}); From 53b37ab8c80099df8801f2effc755e9e53d1ea7b Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 7 Jul 2020 02:18:39 +0300 Subject: [PATCH 044/195] feat(custom-templates): introduce custom templates (#3906) * feat(custom-templates): introduce types * feat(custom-templates): introduce data layer service * feat(custom-templates): introduce http handler * feat(custom-templates): create routes and view stubs * feat(custom-templates): add create custom template ui * feat(custom-templates): add json keys * feat(custom-templates): introduce custom templates list page * feat(custom-templates): introduce update page * feat(stack): create template from stack * feat(stacks): create stack from custom template * feat(custom-templates): disable edit/delete of templates * fix(custom-templates): fail update on non admin/owner * fix(custom-templates): add ng-inject decorator * chore(plop): revert template * feat(stacks): remove actions column * feat(stack): add button to create template from stack * feat(stacks): add empty state for templates * feat(custom-templates): show templates in a list * feat(custom-template): replace table with list * feat(custom-templates): move create template button * refactor(custom-templates): introduce more fields * feat(custom-templates): use stack type when creating template * feat(custom-templates): use same type as stack * feat(custom-templates): add edit and delete buttons to template item * feat(custom-templates): customize stack before deploy * feat(stack): show template details * feat(custom-templates): move customize * feat(custom-templates): create description required * fix(template): show platform icon * fix(custom-templates): show spinner when creating stack * feat(custom-templates): prevent user from edit templates * feat(custom-templates): use resource control for custom templates * feat(custom-templates): show created templates * feat(custom-templates): filter templates by stack type * fix(custom-templates): create swarm or standalone stack * feat(stacks): filter templates by type * feat(resource-control): disable resource control on public * feat(custom-template): apply access control on edit * feat(custom-template): add form validation * feat(stack): disable create custom template from external task * refactor(custom-templates): create template from file and type * feat(templates): introduce a file handler that returns template docker file * feat(template): introduce template duplication * feat(custom-template): enforce unique template name * fix(template): rename copy button * fix(custom-template): clear access control selection between templates * fix(custom-templates): show required fields * refactor(filesystem): use a constant for temp path --- api/bolt/customtemplate/customtemplate.go | 96 ++++++ api/bolt/datastore.go | 13 + api/filesystem/filesystem.go | 41 +++ .../customtemplates/customtemplate_create.go | 290 ++++++++++++++++++ .../customtemplates/customtemplate_delete.go | 61 ++++ .../customtemplates/customtemplate_file.go | 37 +++ .../customtemplates/customtemplate_inspect.go | 47 +++ .../customtemplates/customtemplate_list.go | 67 ++++ .../customtemplates/customtemplate_update.go | 92 ++++++ api/http/handler/customtemplates/git.go | 17 + api/http/handler/customtemplates/handler.go | 60 ++++ api/http/handler/handler.go | 6 + api/http/handler/templates/git.go | 17 + api/http/handler/templates/handler.go | 6 +- api/http/handler/templates/template_file.go | 77 +++++ api/http/server.go | 9 + api/internal/authorization/access_control.go | 33 +- api/portainer.go | 45 +++ app/constants.js | 1 + .../docker-sidebar-content.js | 2 + .../dockerSidebarContent.html | 4 + app/portainer/__module.js | 40 +++ .../porAccessControlFormController.js | 4 + .../customTemplateCommonFields.html | 71 +++++ .../customTemplateCommonFieldsController.js | 16 + .../custom-template-common-fields/index.js | 9 + .../customTemplatesList.html | 51 +++ .../components/custom-templates-list/index.js | 15 + .../forms/stack-from-template-form/index.js | 15 + .../stackFromTemplateForm.html | 73 +++++ .../template-item/template-item.css | 9 + .../template-item/template-item.js | 10 +- .../template-item/templateItem.html | 15 +- .../template-list/template-list-controller.js | 113 ++++--- .../template-list/templateList.html | 9 +- .../models/resourceControl/resourceControl.js | 2 +- .../resourceControl/resourceControlTypes.js | 2 + app/portainer/rest/customTemplate.js | 18 ++ app/portainer/rest/template.js | 3 +- app/portainer/services/api/customTemplate.js | 59 ++++ app/portainer/services/api/templateService.js | 5 + app/portainer/services/fileUpload.js | 8 + app/portainer/services/modalService.js | 6 + .../createCustomTemplateView.html | 217 +++++++++++++ .../createCustomTemplateViewController.js | 152 +++++++++ .../create-custom-template-view/index.js | 6 + .../customTemplatesView.html | 72 +++++ .../customTemplatesViewController.js | 243 +++++++++++++++ .../custom-templates-view/index.js | 6 + .../editCustomTemplateView.html | 71 +++++ .../editCustomTemplateViewController.js | 93 ++++++ .../edit-custom-template-view/index.js | 6 + app/portainer/views/sidebar/sidebar.html | 2 + .../stacks/create/createStackController.js | 61 ++-- .../views/stacks/create/createstack.html | 64 +++- app/portainer/views/stacks/edit/stack.html | 14 +- app/portainer/views/templates/templates.html | 80 +---- plop-templates/component.js.hbs | 6 +- 58 files changed, 2513 insertions(+), 154 deletions(-) create mode 100644 api/bolt/customtemplate/customtemplate.go create mode 100644 api/http/handler/customtemplates/customtemplate_create.go create mode 100644 api/http/handler/customtemplates/customtemplate_delete.go create mode 100644 api/http/handler/customtemplates/customtemplate_file.go create mode 100644 api/http/handler/customtemplates/customtemplate_inspect.go create mode 100644 api/http/handler/customtemplates/customtemplate_list.go create mode 100644 api/http/handler/customtemplates/customtemplate_update.go create mode 100644 api/http/handler/customtemplates/git.go create mode 100644 api/http/handler/customtemplates/handler.go create mode 100644 api/http/handler/templates/git.go create mode 100644 api/http/handler/templates/template_file.go create mode 100644 app/portainer/components/custom-template-common-fields/customTemplateCommonFields.html create mode 100644 app/portainer/components/custom-template-common-fields/customTemplateCommonFieldsController.js create mode 100644 app/portainer/components/custom-template-common-fields/index.js create mode 100644 app/portainer/components/custom-templates-list/customTemplatesList.html create mode 100644 app/portainer/components/custom-templates-list/index.js create mode 100644 app/portainer/components/forms/stack-from-template-form/index.js create mode 100644 app/portainer/components/forms/stack-from-template-form/stackFromTemplateForm.html create mode 100644 app/portainer/components/template-list/template-item/template-item.css create mode 100644 app/portainer/rest/customTemplate.js create mode 100644 app/portainer/services/api/customTemplate.js create mode 100644 app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html create mode 100644 app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js create mode 100644 app/portainer/views/custom-templates/create-custom-template-view/index.js create mode 100644 app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html create mode 100644 app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js create mode 100644 app/portainer/views/custom-templates/custom-templates-view/index.js create mode 100644 app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html create mode 100644 app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js create mode 100644 app/portainer/views/custom-templates/edit-custom-template-view/index.js diff --git a/api/bolt/customtemplate/customtemplate.go b/api/bolt/customtemplate/customtemplate.go new file mode 100644 index 000000000..316af170e --- /dev/null +++ b/api/bolt/customtemplate/customtemplate.go @@ -0,0 +1,96 @@ +package customtemplate + +import ( + "github.com/boltdb/bolt" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" +) + +const ( + // BucketName represents the name of the bucket where this service stores data. + BucketName = "customtemplates" +) + +// Service represents a service for managing custom template data. +type Service struct { + db *bolt.DB +} + +// NewService creates a new instance of a service. +func NewService(db *bolt.DB) (*Service, error) { + err := internal.CreateBucket(db, BucketName) + if err != nil { + return nil, err + } + + return &Service{ + db: db, + }, nil +} + +// CustomTemplates return an array containing all the custom templates. +func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) { + var customTemplates = make([]portainer.CustomTemplate, 0) + + err := service.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var customTemplate portainer.CustomTemplate + err := internal.UnmarshalObjectWithJsoniter(v, &customTemplate) + if err != nil { + return err + } + customTemplates = append(customTemplates, customTemplate) + } + + return nil + }) + + return customTemplates, err +} + +// CustomTemplate returns an custom template by ID. +func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error) { + var customTemplate portainer.CustomTemplate + identifier := internal.Itob(int(ID)) + + err := internal.GetObject(service.db, BucketName, identifier, &customTemplate) + if err != nil { + return nil, err + } + + return &customTemplate, nil +} + +// UpdateCustomTemplate updates an custom template. +func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error { + identifier := internal.Itob(int(ID)) + return internal.UpdateObject(service.db, BucketName, identifier, customTemplate) +} + +// DeleteCustomTemplate deletes an custom template. +func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error { + identifier := internal.Itob(int(ID)) + return internal.DeleteObject(service.db, BucketName, identifier) +} + +// CreateCustomTemplate assign an ID to a new custom template and saves it. +func (service *Service) CreateCustomTemplate(customTemplate *portainer.CustomTemplate) error { + return service.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + data, err := internal.MarshalObject(customTemplate) + if err != nil { + return err + } + + return bucket.Put(internal.Itob(int(customTemplate.ID)), data) + }) +} + +// GetNextIdentifier returns the next identifier for a custom template. +func (service *Service) GetNextIdentifier() int { + return internal.GetNextIdentifier(service.db, BucketName) +} diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 9ace83b9a..e7311a1bd 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -7,6 +7,7 @@ import ( "github.com/boltdb/bolt" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/customtemplate" "github.com/portainer/portainer/api/bolt/dockerhub" "github.com/portainer/portainer/api/bolt/edgegroup" "github.com/portainer/portainer/api/bolt/edgejob" @@ -43,6 +44,7 @@ type Store struct { db *bolt.DB isNew bool fileService portainer.FileService + CustomTemplateService *customtemplate.Service DockerHubService *dockerhub.Service EdgeGroupService *edgegroup.Service EdgeJobService *edgejob.Service @@ -168,6 +170,12 @@ func (store *Store) initServices() error { } store.RoleService = authorizationsetService + customTemplateService, err := customtemplate.NewService(store.db) + if err != nil { + return err + } + store.CustomTemplateService = customTemplateService + dockerhubService, err := dockerhub.NewService(store.db) if err != nil { return err @@ -291,6 +299,11 @@ func (store *Store) initServices() error { return nil } +// CustomTemplate gives access to the CustomTemplate data management layer +func (store *Store) CustomTemplate() portainer.CustomTemplateService { + return store.CustomTemplateService +} + // DockerHub gives access to the DockerHub data management layer func (store *Store) DockerHub() portainer.DockerHubService { return store.DockerHubService diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index 7d3747ae4..eb5024a93 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" + "github.com/gofrs/uuid" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/archive" @@ -43,6 +44,10 @@ const ( // ExtensionRegistryManagementStorePath represents the subfolder where files related to the // registry management extension are stored. ExtensionRegistryManagementStorePath = "extensions" + // CustomTemplateStorePath represents the subfolder where custom template files are stored in the file store folder. + CustomTemplateStorePath = "custom_templates" + // TempPath represent the subfolder where temporary files are saved + TempPath = "tmp" ) // Service represents a service for managing files and directories. @@ -393,6 +398,32 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) { return block.Bytes, nil } +// GetCustomTemplateProjectPath returns the absolute path on the FS for a custom template based +// on its identifier. +func (service *Service) GetCustomTemplateProjectPath(identifier string) string { + return path.Join(service.fileStorePath, CustomTemplateStorePath, identifier) +} + +// StoreCustomTemplateFileFromBytes creates a subfolder in the CustomTemplateStorePath and stores a new file from bytes. +// It returns the path to the folder where the file is stored. +func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error) { + customTemplateStorePath := path.Join(CustomTemplateStorePath, identifier) + err := service.createDirectoryInStore(customTemplateStorePath) + if err != nil { + return "", err + } + + templateFilePath := path.Join(customTemplateStorePath, fileName) + r := bytes.NewReader(data) + + err = service.createFileInStore(templateFilePath, r) + if err != nil { + return "", err + } + + return path.Join(service.fileStorePath, customTemplateStorePath), nil +} + // GetEdgeJobFolder returns the absolute path on the filesystem for an Edge job based // on its identifier. func (service *Service) GetEdgeJobFolder(identifier string) string { @@ -467,3 +498,13 @@ func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID strin func (service *Service) getEdgeJobTaskLogPath(edgeJobID string, taskID string) string { return fmt.Sprintf("%s/logs_%s", service.GetEdgeJobFolder(edgeJobID), taskID) } + +// GetTemporaryPath returns a temp folder +func (service *Service) GetTemporaryPath() (string, error) { + uid, err := uuid.NewV4() + if err != nil { + return "", err + } + + return path.Join(service.fileStorePath, TempPath, uid.String()), nil +} diff --git a/api/http/handler/customtemplates/customtemplate_create.go b/api/http/handler/customtemplates/customtemplate_create.go new file mode 100644 index 000000000..3bb8fef0b --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_create.go @@ -0,0 +1,290 @@ +package customtemplates + +import ( + "errors" + "net/http" + "strconv" + + "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" +) + +func (handler *Handler) customTemplateCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + method, err := request.RetrieveQueryParameter(r, "method", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err} + } + + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err} + } + + customTemplate, err := handler.createCustomTemplate(method, r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create custom template", err} + } + + customTemplate.CreatedByUserID = tokenData.ID + + customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err} + } + + for _, existingTemplate := range customTemplates { + if existingTemplate.Title == customTemplate.Title { + return &httperror.HandlerError{http.StatusInternalServerError, "Template name must be unique", errors.New("Template name must be unique")} + } + } + + err = handler.DataStore.CustomTemplate().CreateCustomTemplate(customTemplate) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create custom template", err} + } + + resourceControl := authorization.NewPrivateResourceControl(strconv.Itoa(int(customTemplate.ID)), portainer.CustomTemplateResourceControl, tokenData.ID) + + err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err} + } + + customTemplate.ResourceControl = resourceControl + + return response.JSON(w, customTemplate) +} + +func (handler *Handler) createCustomTemplate(method string, r *http.Request) (*portainer.CustomTemplate, error) { + switch method { + case "string": + return handler.createCustomTemplateFromFileContent(r) + case "repository": + return handler.createCustomTemplateFromGitRepository(r) + case "file": + return handler.createCustomTemplateFromFileUpload(r) + } + return nil, errors.New("Invalid value for query parameter: method. Value must be one of: string, repository or file") +} + +type customTemplateFromFileContentPayload struct { + Logo string + Title string + FileContent string + Description string + Note string + Platform portainer.CustomTemplatePlatform + Type portainer.StackType +} + +func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.Title) { + return portainer.Error("Invalid custom template title") + } + if govalidator.IsNull(payload.Description) { + return portainer.Error("Invalid custom template description") + } + if govalidator.IsNull(payload.FileContent) { + return portainer.Error("Invalid file content") + } + if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows { + return portainer.Error("Invalid custom template platform") + } + if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack { + return portainer.Error("Invalid custom template type") + } + return nil +} + +func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) { + var payload customTemplateFromFileContentPayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return nil, err + } + + customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier() + customTemplate := &portainer.CustomTemplate{ + ID: portainer.CustomTemplateID(customTemplateID), + Title: payload.Title, + EntryPoint: filesystem.ComposeFileDefaultName, + Description: payload.Description, + Note: payload.Note, + Platform: (payload.Platform), + Type: (payload.Type), + Logo: payload.Logo, + } + + templateFolder := strconv.Itoa(customTemplateID) + projectPath, err := handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent)) + if err != nil { + return nil, err + } + customTemplate.ProjectPath = projectPath + + return customTemplate, nil +} + +type customTemplateFromGitRepositoryPayload struct { + Logo string + Title string + Description string + Note string + Platform portainer.CustomTemplatePlatform + Type portainer.StackType + RepositoryURL string + RepositoryReferenceName string + RepositoryAuthentication bool + RepositoryUsername string + RepositoryPassword string + ComposeFilePathInRepository string +} + +func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.Title) { + return portainer.Error("Invalid custom template title") + } + if govalidator.IsNull(payload.Description) { + return portainer.Error("Invalid custom template description") + } + if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) { + return portainer.Error("Invalid repository URL. Must correspond to a valid URL format") + } + if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { + return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled") + } + if govalidator.IsNull(payload.ComposeFilePathInRepository) { + payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName + } + if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows { + return portainer.Error("Invalid custom template platform") + } + if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack { + return portainer.Error("Invalid custom template type") + } + return nil +} + +func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (*portainer.CustomTemplate, error) { + var payload customTemplateFromGitRepositoryPayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return nil, err + } + + customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier() + customTemplate := &portainer.CustomTemplate{ + ID: portainer.CustomTemplateID(customTemplateID), + Title: payload.Title, + EntryPoint: payload.ComposeFilePathInRepository, + Description: payload.Description, + Note: payload.Note, + Platform: payload.Platform, + Type: payload.Type, + Logo: payload.Logo, + } + + projectPath := handler.FileService.GetCustomTemplateProjectPath(strconv.Itoa(customTemplateID)) + customTemplate.ProjectPath = projectPath + + gitCloneParams := &cloneRepositoryParameters{ + url: payload.RepositoryURL, + referenceName: payload.RepositoryReferenceName, + path: projectPath, + authentication: payload.RepositoryAuthentication, + username: payload.RepositoryUsername, + password: payload.RepositoryPassword, + } + + err = handler.cloneGitRepository(gitCloneParams) + if err != nil { + return nil, err + } + + return customTemplate, nil +} + +type customTemplateFromFileUploadPayload struct { + Logo string + Title string + Description string + Note string + Platform portainer.CustomTemplatePlatform + Type portainer.StackType + FileContent []byte +} + +func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) error { + title, err := request.RetrieveMultiPartFormValue(r, "Title", false) + if err != nil { + return portainer.Error("Invalid custom template title") + } + payload.Title = title + + description, err := request.RetrieveMultiPartFormValue(r, "Description", false) + if err != nil { + return portainer.Error("Invalid custom template description") + } + + payload.Description = description + + note, _ := request.RetrieveMultiPartFormValue(r, "Note", true) + payload.Note = note + + platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true) + templatePlatform := portainer.CustomTemplatePlatform(platform) + if templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows { + return portainer.Error("Invalid custom template platform") + } + payload.Platform = templatePlatform + + typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true) + templateType := portainer.StackType(typeNumeral) + if templateType != portainer.DockerComposeStack && templateType != portainer.DockerSwarmStack { + return portainer.Error("Invalid custom template type") + } + payload.Type = templateType + + composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") + if err != nil { + return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly") + } + payload.FileContent = composeFileContent + + return nil +} + +func (handler *Handler) createCustomTemplateFromFileUpload(r *http.Request) (*portainer.CustomTemplate, error) { + payload := &customTemplateFromFileUploadPayload{} + err := payload.Validate(r) + if err != nil { + return nil, err + } + + customTemplateID := handler.DataStore.CustomTemplate().GetNextIdentifier() + customTemplate := &portainer.CustomTemplate{ + ID: portainer.CustomTemplateID(customTemplateID), + Title: payload.Title, + Description: payload.Description, + Note: payload.Note, + Platform: payload.Platform, + Type: payload.Type, + Logo: payload.Logo, + EntryPoint: filesystem.ComposeFileDefaultName, + } + + templateFolder := strconv.Itoa(customTemplateID) + projectPath, err := handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent)) + if err != nil { + return nil, err + } + customTemplate.ProjectPath = projectPath + + return customTemplate, nil +} diff --git a/api/http/handler/customtemplates/customtemplate_delete.go b/api/http/handler/customtemplates/customtemplate_delete.go new file mode 100644 index 000000000..f81c41992 --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_delete.go @@ -0,0 +1,61 @@ +package customtemplates + +import ( + "net/http" + "strconv" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" +) + +func (handler *Handler) customTemplateDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template identifier route variable", err} + } + + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + } + + customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err} + } + + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(strconv.Itoa(customTemplateID), portainer.CustomTemplateResourceControl) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the custom template", err} + } + + access := userCanEditTemplate(customTemplate, securityContext) + if !access { + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + } + + err = handler.DataStore.CustomTemplate().DeleteCustomTemplate(portainer.CustomTemplateID(customTemplateID)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the custom template from the database", err} + } + + err = handler.FileService.RemoveDirectory(customTemplate.ProjectPath) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove custom template files from disk", err} + } + + if resourceControl != nil { + err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err} + } + } + + return response.Empty(w) + +} diff --git a/api/http/handler/customtemplates/customtemplate_file.go b/api/http/handler/customtemplates/customtemplate_file.go new file mode 100644 index 000000000..5b3689fa1 --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_file.go @@ -0,0 +1,37 @@ +package customtemplates + +import ( + "net/http" + "path" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" +) + +type fileResponse struct { + FileContent string +} + +// GET request on /api/custom_templates/:id/file +func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid custom template identifier route variable", err} + } + + customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err} + } + + fileContent, err := handler.FileService.GetFileContent(path.Join(customTemplate.ProjectPath, customTemplate.EntryPoint)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom template file from disk", err} + } + + return response.JSON(w, &fileResponse{FileContent: string(fileContent)}) +} diff --git a/api/http/handler/customtemplates/customtemplate_inspect.go b/api/http/handler/customtemplates/customtemplate_inspect.go new file mode 100644 index 000000000..fd77be2b0 --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_inspect.go @@ -0,0 +1,47 @@ +package customtemplates + +import ( + "net/http" + "strconv" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" +) + +func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template identifier route variable", err} + } + + customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err} + } + + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err} + } + + resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(strconv.Itoa(customTemplateID), portainer.CustomTemplateResourceControl) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the custom template", err} + } + + access := userCanEditTemplate(customTemplate, securityContext) + if !access { + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + } + + if resourceControl != nil { + customTemplate.ResourceControl = resourceControl + } + + return response.JSON(w, customTemplate) +} diff --git a/api/http/handler/customtemplates/customtemplate_list.go b/api/http/handler/customtemplates/customtemplate_list.go new file mode 100644 index 000000000..b33cb8297 --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_list.go @@ -0,0 +1,67 @@ +package customtemplates + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" +) + +func (handler *Handler) customTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err} + } + + stackType, _ := request.RetrieveNumericQueryParameter(r, "type", true) + + resourceControls, err := handler.DataStore.ResourceControl().ResourceControls() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err} + } + + customTemplates = authorization.DecorateCustomTemplates(customTemplates, resourceControls) + + customTemplates = filterTemplatesByEngineType(customTemplates, portainer.StackType(stackType)) + + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + } + + if !securityContext.IsAdmin { + user, err := handler.DataStore.User().User(securityContext.UserID) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err} + } + + userTeamIDs := make([]portainer.TeamID, 0) + for _, membership := range securityContext.UserMemberships { + userTeamIDs = append(userTeamIDs, membership.TeamID) + } + + customTemplates = authorization.FilterAuthorizedCustomTemplates(customTemplates, user, userTeamIDs) + } + + return response.JSON(w, customTemplates) +} + +func filterTemplatesByEngineType(templates []portainer.CustomTemplate, stackType portainer.StackType) []portainer.CustomTemplate { + if stackType == 0 { + return templates + } + + filteredTemplates := []portainer.CustomTemplate{} + + for _, template := range templates { + if template.Type == stackType { + filteredTemplates = append(filteredTemplates, template) + } + } + + return filteredTemplates +} diff --git a/api/http/handler/customtemplates/customtemplate_update.go b/api/http/handler/customtemplates/customtemplate_update.go new file mode 100644 index 000000000..b969b4fe2 --- /dev/null +++ b/api/http/handler/customtemplates/customtemplate_update.go @@ -0,0 +1,92 @@ +package customtemplates + +import ( + "net/http" + "strconv" + + "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" +) + +type customTemplateUpdatePayload struct { + Logo string + Title string + Description string + Note string + Platform portainer.CustomTemplatePlatform + Type portainer.StackType + FileContent string +} + +func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.Title) { + return portainer.Error("Invalid custom template title") + } + if govalidator.IsNull(payload.FileContent) { + return portainer.Error("Invalid file content") + } + if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows { + return portainer.Error("Invalid custom template platform") + } + if payload.Type != portainer.DockerComposeStack && payload.Type != portainer.DockerSwarmStack { + return portainer.Error("Invalid custom template type") + } + if govalidator.IsNull(payload.Description) { + return portainer.Error("Invalid custom template description") + } + return nil +} + +func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + customTemplateID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid Custom template identifier route variable", err} + } + + var payload customTemplateUpdatePayload + err = request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err} + } + + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + } + + access := userCanEditTemplate(customTemplate, securityContext) + if !access { + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + } + + templateFolder := strconv.Itoa(customTemplateID) + _, err = handler.FileService.StoreCustomTemplateFileFromBytes(templateFolder, customTemplate.EntryPoint, []byte(payload.FileContent)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated custom template file on disk", err} + } + + customTemplate.Title = payload.Title + customTemplate.Logo = payload.Logo + customTemplate.Description = payload.Description + customTemplate.Note = payload.Note + customTemplate.Platform = payload.Platform + customTemplate.Type = payload.Type + + err = handler.DataStore.CustomTemplate().UpdateCustomTemplate(customTemplate.ID, customTemplate) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist custom template changes inside the database", err} + } + + return response.JSON(w, customTemplate) +} diff --git a/api/http/handler/customtemplates/git.go b/api/http/handler/customtemplates/git.go new file mode 100644 index 000000000..b4f3e3211 --- /dev/null +++ b/api/http/handler/customtemplates/git.go @@ -0,0 +1,17 @@ +package customtemplates + +type cloneRepositoryParameters struct { + url string + referenceName string + path string + authentication bool + username string + password string +} + +func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error { + if parameters.authentication { + return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password) + } + return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path) +} diff --git a/api/http/handler/customtemplates/handler.go b/api/http/handler/customtemplates/handler.go new file mode 100644 index 000000000..ff8fce4a5 --- /dev/null +++ b/api/http/handler/customtemplates/handler.go @@ -0,0 +1,60 @@ +package customtemplates + +import ( + "net/http" + + "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" +) + +// Handler is the HTTP handler used to handle endpoint group operations. +type Handler struct { + *mux.Router + DataStore portainer.DataStore + FileService portainer.FileService + GitService portainer.GitService +} + +// NewHandler creates a handler to manage endpoint group operations. +func NewHandler(bouncer *security.RequestBouncer) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + } + h.Handle("/custom_templates", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateCreate))).Methods(http.MethodPost) + h.Handle("/custom_templates", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateList))).Methods(http.MethodGet) + h.Handle("/custom_templates/{id}", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateInspect))).Methods(http.MethodGet) + h.Handle("/custom_templates/{id}/file", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateFile))).Methods(http.MethodGet) + h.Handle("/custom_templates/{id}", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateUpdate))).Methods(http.MethodPut) + h.Handle("/custom_templates/{id}", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.customTemplateDelete))).Methods(http.MethodDelete) + return h +} + +func userCanEditTemplate(customTemplate *portainer.CustomTemplate, securityContext *security.RestrictedRequestContext) bool { + return securityContext.IsAdmin || customTemplate.CreatedByUserID == securityContext.UserID +} + +func userCanAccessTemplate(customTemplate portainer.CustomTemplate, securityContext *security.RestrictedRequestContext, resourceControl *portainer.ResourceControl) bool { + if securityContext.IsAdmin || customTemplate.CreatedByUserID == securityContext.UserID { + return true + } + + userTeamIDs := make([]portainer.TeamID, 0) + for _, membership := range securityContext.UserMemberships { + userTeamIDs = append(userTeamIDs, membership.TeamID) + } + + if resourceControl != nil && authorization.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) { + return true + } + + return false +} diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 113fa60c4..4d7dbf760 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/portainer/portainer/api/http/handler/auth" + "github.com/portainer/portainer/api/http/handler/customtemplates" "github.com/portainer/portainer/api/http/handler/dockerhub" "github.com/portainer/portainer/api/http/handler/edgegroups" "github.com/portainer/portainer/api/http/handler/edgejobs" @@ -37,6 +38,7 @@ import ( // Handler is a collection of all the service handlers. type Handler struct { AuthHandler *auth.Handler + CustomTemplatesHandler *customtemplates.Handler DockerHubHandler *dockerhub.Handler EdgeGroupsHandler *edgegroups.Handler EdgeJobsHandler *edgejobs.Handler @@ -73,6 +75,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/dockerhub"): http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/custom_templates"): + http.StripPrefix("/api", h.CustomTemplatesHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"): + http.StripPrefix("/api", h.EdgeStacksHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/edge_groups"): http.StripPrefix("/api", h.EdgeGroupsHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/edge_jobs"): diff --git a/api/http/handler/templates/git.go b/api/http/handler/templates/git.go new file mode 100644 index 000000000..cc94668cd --- /dev/null +++ b/api/http/handler/templates/git.go @@ -0,0 +1,17 @@ +package templates + +type cloneRepositoryParameters struct { + url string + referenceName string + path string + authentication bool + username string + password string +} + +func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error { + if parameters.authentication { + return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password) + } + return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path) +} diff --git a/api/http/handler/templates/handler.go b/api/http/handler/templates/handler.go index 7360d5252..5c89f350f 100644 --- a/api/http/handler/templates/handler.go +++ b/api/http/handler/templates/handler.go @@ -12,7 +12,9 @@ import ( // Handler represents an HTTP API handler for managing templates. type Handler struct { *mux.Router - DataStore portainer.DataStore + DataStore portainer.DataStore + GitService portainer.GitService + FileService portainer.FileService } // NewHandler returns a new instance of Handler. @@ -23,5 +25,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { h.Handle("/templates", bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) + h.Handle("/templates/file", + bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateFile))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/templates/template_file.go b/api/http/handler/templates/template_file.go new file mode 100644 index 000000000..614e3d170 --- /dev/null +++ b/api/http/handler/templates/template_file.go @@ -0,0 +1,77 @@ +package templates + +import ( + "errors" + "log" + "net/http" + "path" + + "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" +) + +type filePayload struct { + RepositoryURL string + ComposeFilePathInRepository string +} + +type fileResponse struct { + FileContent string +} + +func (payload *filePayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.RepositoryURL) { + return errors.New("Invalid repository url") + } + + if govalidator.IsNull(payload.ComposeFilePathInRepository) { + return errors.New("Invalid file path") + } + + return nil +} + +func (handler *Handler) templateFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload filePayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + projectPath, err := handler.FileService.GetTemporaryPath() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create temporary folder", err} + } + + defer handler.cleanUp(projectPath) + + gitCloneParams := &cloneRepositoryParameters{ + url: payload.RepositoryURL, + path: projectPath, + } + + err = handler.cloneGitRepository(gitCloneParams) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clone git repository", err} + } + + composeFilePath := path.Join(projectPath, payload.ComposeFilePathInRepository) + + fileContent, err := handler.FileService.GetFileContent(composeFilePath) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Failed loading file content", err} + } + + return response.JSON(w, fileResponse{FileContent: string(fileContent)}) + +} + +func (handler *Handler) cleanUp(projectPath string) error { + err := handler.FileService.RemoveDirectory(projectPath) + if err != nil { + log.Printf("http error: Unable to cleanup stack creation (err=%s)\n", err) + } + return nil +} diff --git a/api/http/server.go b/api/http/server.go index 4693c5334..1b1e24b76 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/handler" "github.com/portainer/portainer/api/http/handler/auth" + "github.com/portainer/portainer/api/http/handler/customtemplates" "github.com/portainer/portainer/api/http/handler/dockerhub" "github.com/portainer/portainer/api/http/handler/edgegroups" "github.com/portainer/portainer/api/http/handler/edgejobs" @@ -92,6 +93,11 @@ func (server *Server) Start() error { var roleHandler = roles.NewHandler(requestBouncer) roleHandler.DataStore = server.DataStore + var customTemplatesHandler = customtemplates.NewHandler(requestBouncer) + customTemplatesHandler.DataStore = server.DataStore + customTemplatesHandler.FileService = server.FileService + customTemplatesHandler.GitService = server.GitService + var dockerHubHandler = dockerhub.NewHandler(requestBouncer) dockerHubHandler.DataStore = server.DataStore @@ -184,6 +190,8 @@ func (server *Server) Start() error { var templatesHandler = templates.NewHandler(requestBouncer) templatesHandler.DataStore = server.DataStore + templatesHandler.FileService = server.FileService + templatesHandler.GitService = server.GitService var uploadHandler = upload.NewHandler(requestBouncer) uploadHandler.FileService = server.FileService @@ -206,6 +214,7 @@ func (server *Server) Start() error { server.Handler = &handler.Handler{ RoleHandler: roleHandler, AuthHandler: authHandler, + CustomTemplatesHandler: customTemplatesHandler, DockerHubHandler: dockerHubHandler, EdgeGroupsHandler: edgeGroupsHandler, EdgeJobsHandler: edgeJobsHandler, diff --git a/api/internal/authorization/access_control.go b/api/internal/authorization/access_control.go index 9879799ac..cb263b76a 100644 --- a/api/internal/authorization/access_control.go +++ b/api/internal/authorization/access_control.go @@ -1,6 +1,10 @@ package authorization -import "github.com/portainer/portainer/api" +import ( + "strconv" + + "github.com/portainer/portainer/api" +) // NewPrivateResourceControl will create a new private resource control associated to the resource specified by the // identifier and type parameters. It automatically assigns it to the user specified by the userID parameter. @@ -100,6 +104,20 @@ func DecorateStacks(stacks []portainer.Stack, resourceControls []portainer.Resou return stacks } +// DecorateCustomTemplates will iterate through a list of custom templates, check for an associated resource control for each +// template and decorate the template element if a resource control is found. +func DecorateCustomTemplates(templates []portainer.CustomTemplate, resourceControls []portainer.ResourceControl) []portainer.CustomTemplate { + for idx, template := range templates { + + resourceControl := GetResourceControlByResourceIDAndType(strconv.Itoa(int(template.ID)), portainer.CustomTemplateResourceControl, resourceControls) + if resourceControl != nil { + templates[idx].ResourceControl = resourceControl + } + } + + return templates +} + // FilterAuthorizedStacks returns a list of decorated stacks filtered through resource control access checks. func FilterAuthorizedStacks(stacks []portainer.Stack, user *portainer.User, userTeamIDs []portainer.TeamID, rbacEnabled bool) []portainer.Stack { authorizedStacks := make([]portainer.Stack, 0) @@ -119,6 +137,19 @@ func FilterAuthorizedStacks(stacks []portainer.Stack, user *portainer.User, user return authorizedStacks } +// FilterAuthorizedCustomTemplates returns a list of decorated custom templates filtered through resource control access checks. +func FilterAuthorizedCustomTemplates(customTemplates []portainer.CustomTemplate, user *portainer.User, userTeamIDs []portainer.TeamID) []portainer.CustomTemplate { + authorizedTemplates := make([]portainer.CustomTemplate, 0) + + for _, customTemplate := range customTemplates { + if customTemplate.CreatedByUserID == user.ID || (customTemplate.ResourceControl != nil && UserCanAccessResource(user.ID, userTeamIDs, customTemplate.ResourceControl)) { + authorizedTemplates = append(authorizedTemplates, customTemplate) + } + } + + return authorizedTemplates +} + // UserCanAccessResource will valide that a user has permissions defined in the specified resource control // based on its identifier and the team(s) he is part of. func UserCanAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool { diff --git a/api/portainer.go b/api/portainer.go index 582e7cf84..8e94b7694 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -61,6 +61,27 @@ type ( SnapshotInterval *string } + // CustomTemplate represents a custom template + CustomTemplate struct { + ID CustomTemplateID `json:"Id"` + Title string `json:"Title"` + Description string `json:"Description"` + ProjectPath string `json:"ProjectPath"` + EntryPoint string `json:"EntryPoint"` + CreatedByUserID UserID `json:"CreatedByUserId"` + Note string `json:"Note"` + Platform CustomTemplatePlatform `json:"Platform"` + Logo string `json:"Logo"` + Type StackType `json:"Type"` + ResourceControl *ResourceControl `json:"ResourceControl"` + } + + // CustomTemplateID represents a custom template identifier + CustomTemplateID int + + // CustomTemplatePlatform represents a custom template platform + CustomTemplatePlatform int + // DockerHub represents all the required information to connect and use the // Docker Hub DockerHub struct { @@ -746,6 +767,16 @@ type ( CompareHashAndData(hash string, data string) error } + // CustomTemplateService represents a service to manage custom templates + CustomTemplateService interface { + GetNextIdentifier() int + CustomTemplates() ([]CustomTemplate, error) + CustomTemplate(ID CustomTemplateID) (*CustomTemplate, error) + CreateCustomTemplate(customTemplate *CustomTemplate) error + UpdateCustomTemplate(ID CustomTemplateID, customTemplate *CustomTemplate) error + DeleteCustomTemplate(ID CustomTemplateID) error + } + // DataStore defines the interface to manage the data DataStore interface { Open() error @@ -755,6 +786,7 @@ type ( MigrateData() error DockerHub() DockerHubService + CustomTemplate() CustomTemplateService EdgeGroup() EdgeGroupService EdgeJob() EdgeJobService EdgeStack() EdgeStackService @@ -897,6 +929,9 @@ type ( StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error ExtractExtensionArchive(data []byte) error GetBinaryFolder() string + StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error) + GetCustomTemplateProjectPath(identifier string) string + GetTemporaryPath() (string, error) } // GitService represents a service for managing Git @@ -1140,6 +1175,14 @@ const ( EdgeJobLogsStatusCollected ) +const ( + _ CustomTemplatePlatform = iota + // CustomTemplatePlatformLinux represents a custom template for linux + CustomTemplatePlatformLinux + // CustomTemplatePlatformWindows represents a custom template for windows + CustomTemplatePlatformWindows +) + const ( _ EdgeStackStatusType = iota //StatusOk represents a successfully deployed edge stack @@ -1240,6 +1283,8 @@ const ( StackResourceControl // ConfigResourceControl represents a resource control associated to a Docker config ConfigResourceControl + // CustomTemplateResourceControl represents a resource control associated to a custom template + CustomTemplateResourceControl ) const ( diff --git a/app/constants.js b/app/constants.js index a727766bb..f27f26557 100644 --- a/app/constants.js +++ b/app/constants.js @@ -2,6 +2,7 @@ angular .module('portainer') .constant('API_ENDPOINT_AUTH', 'api/auth') .constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub') + .constant('API_ENDPOINT_CUSTOM_TEMPLATES', 'api/custom_templates') .constant('API_ENDPOINT_EDGE_GROUPS', 'api/edge_groups') .constant('API_ENDPOINT_EDGE_JOBS', 'api/edge_jobs') .constant('API_ENDPOINT_EDGE_STACKS', 'api/edge_stacks') diff --git a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js index b57e4cdcb..400293ee5 100644 --- a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js +++ b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js @@ -6,5 +6,7 @@ angular.module('portainer.docker').component('dockerSidebarContent', { standaloneManagement: '<', adminAccess: '<', offlineMode: '<', + toggle: '<', + currentRouteName: '<', }, }); diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index 50815d41d..118b63fbe 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -3,6 +3,10 @@
+ + +
+
+ +
+ + + No custom template are available. Head over the custom template view to create one. + +
+
+ +
+
+ Information +
+
+
+
+
+
+
+ + +
+
+ Web editor +
+
+
+ +
+
+
+
+
@@ -213,7 +276,6 @@
-
diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html index 17fe4869c..a8eca1b52 100644 --- a/app/portainer/views/stacks/edit/stack.html +++ b/app/portainer/views/stacks/edit/stack.html @@ -38,9 +38,19 @@
{{ stackName }} - + Create template from stack + + +
diff --git a/app/portainer/views/templates/templates.html b/app/portainer/views/templates/templates.html index e1b98dbd1..3617c82d8 100644 --- a/app/portainer/views/templates/templates.html +++ b/app/portainer/views/templates/templates.html @@ -9,77 +9,15 @@
-
- - - -
- -
-
- Information -
-
-
-
-
-
-
- -
- Configuration -
- -
- -
- -
-
- - -
- -
- - -
-
- - - - - -
- Actions -
-
-
- - - {{ state.formValidationError }} -
-
- -
-
-
-
+ +
diff --git a/plop-templates/component.js.hbs b/plop-templates/component.js.hbs index 45237ce14..a29c3e518 100644 --- a/plop-templates/component.js.hbs +++ b/plop-templates/component.js.hbs @@ -1,6 +1,6 @@ -import {{properCase name}}Controller from './{{camelCase name}}Controller.js' +import {{properCase name}}Controller from './{{dashCase name}}/{{camelCase name}}Controller.js' angular.module('portainer.{{module}}').component('{{camelCase name}}', { -templateUrl: './{{camelCase name}}.html', -controller: {{properCase name}}Controller, + templateUrl: './{{camelCase name}}.html', + controller: {{properCase name}}Controller, }); \ No newline at end of file From 91981c815cb5640bea434fb7e9ed343afb912841 Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Tue, 7 Jul 2020 02:01:18 +0200 Subject: [PATCH 045/195] feat(volumes): Ensure a unique identifier for volumes (#3879) * feat(volumes): Ensure a unique identifier for volumes * feat(volumes): change few things --- api/http/proxy/factory/docker/transport.go | 9 +--- api/http/proxy/factory/docker/volumes.go | 60 ++++++++++++++++++++-- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 09027d0c4..cf9b64a86 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -264,14 +264,7 @@ func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Res default: // assume /volumes/{name} - volumeID := path.Base(requestPath) - - if request.Method == http.MethodGet { - return transport.rewriteOperation(request, transport.volumeInspectOperation) - } else if request.Method == http.MethodDelete { - return transport.executeGenericResourceDeletionOperation(request, volumeID, portainer.VolumeResourceControl) - } - return transport.restrictedResourceOperation(request, volumeID, portainer.VolumeResourceControl, false) + return transport.restrictedVolumeOperation(requestPath, request) } } diff --git a/api/http/proxy/factory/docker/volumes.go b/api/http/proxy/factory/docker/volumes.go index a4b5002ef..61a38c72a 100644 --- a/api/http/proxy/factory/docker/volumes.go +++ b/api/http/proxy/factory/docker/volumes.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "path" "github.com/docker/docker/client" @@ -14,7 +15,7 @@ import ( ) const ( - volumeObjectIdentifier = "Name" + volumeObjectIdentifier = "ID" ) func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { @@ -45,6 +46,14 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo if responseObject["Volumes"] != nil { volumeData := responseObject["Volumes"].([]interface{}) + for _, volumeObject := range volumeData { + volume := volumeObject.(map[string]interface{}) + if volume["Name"] == nil || volume["CreatedAt"] == nil { + return errors.New("missing identifier in Docker resource list response") + } + volume[volumeObjectIdentifier] = volume["Name"].(string) + volume["CreatedAt"].(string) + } + resourceOperationParameters := &resourceOperationParameters{ resourceIdentifierAttribute: volumeObjectIdentifier, resourceType: portainer.VolumeResourceControl, @@ -55,7 +64,6 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo if err != nil { return err } - // Overwrite the original volume list responseObject["Volumes"] = volumeData } @@ -73,6 +81,11 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec return err } + if responseObject["Name"] == nil || responseObject["CreatedAt"] == nil { + return errors.New("missing identifier in Docker resource detail response") + } + responseObject[volumeObjectIdentifier] = responseObject["Name"].(string) + responseObject["CreatedAt"].(string) + resourceOperationParameters := &resourceOperationParameters{ resourceIdentifierAttribute: volumeObjectIdentifier, resourceType: portainer.VolumeResourceControl, @@ -123,7 +136,48 @@ func (transport *Transport) decorateVolumeResourceCreationOperation(request *htt } if response.StatusCode == http.StatusCreated { - err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID) + err = transport.decorateVolumeCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID) } return response, err } + +func (transport *Transport) decorateVolumeCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error { + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + if responseObject["Name"] == nil || responseObject["CreatedAt"] == nil { + return errors.New("missing identifier in Docker resource creation response") + } + resourceID := responseObject["Name"].(string) + responseObject["CreatedAt"].(string) + + resourceControl, err := transport.createPrivateResourceControl(resourceID, resourceType, userID) + if err != nil { + return err + } + + responseObject = decorateObject(responseObject, resourceControl) + + return responseutils.RewriteResponse(response, responseObject, http.StatusOK) +} + +func (transport *Transport) restrictedVolumeOperation(requestPath string, request *http.Request) (*http.Response, error) { + + if request.Method == http.MethodGet { + return transport.rewriteOperation(request, transport.volumeInspectOperation) + } + + cli := transport.dockerClient + volume, err := cli.VolumeInspect(context.Background(), path.Base(requestPath)) + if err != nil { + return nil, err + } + + volumeID := volume.Name + volume.CreatedAt + + if request.Method == http.MethodDelete { + return transport.executeGenericResourceDeletionOperation(request, volumeID, portainer.VolumeResourceControl) + } + return transport.restrictedResourceOperation(request, volumeID, portainer.VolumeResourceControl, false) +} From d4456f81ec772dfbd0255f2b0a9ec774cae61117 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 7 Jul 2020 16:55:02 +1200 Subject: [PATCH 046/195] feat(endpoint-init): remove Azure ACI and remote Docker options (#4015) --- app/portainer/models/endpoint/formValues.js | 2 - .../views/init/endpoint/includes/azure.html | 63 --------- .../views/init/endpoint/includes/remote.html | 120 ------------------ .../views/init/endpoint/initEndpoint.html | 6 - .../init/endpoint/initEndpointController.js | 82 +----------- 5 files changed, 2 insertions(+), 271 deletions(-) delete mode 100644 app/portainer/views/init/endpoint/includes/azure.html delete mode 100644 app/portainer/views/init/endpoint/includes/remote.html diff --git a/app/portainer/models/endpoint/formValues.js b/app/portainer/models/endpoint/formValues.js index f7268ffb9..05e9d180c 100644 --- a/app/portainer/models/endpoint/formValues.js +++ b/app/portainer/models/endpoint/formValues.js @@ -35,7 +35,5 @@ export const PortainerEndpointInitFormValueEndpointSections = Object.freeze([ 'fas fa-dharmachakra', 'Manage the local Kubernetes environment' ), - new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.REMOTE, 'Remote', 'fab fa-docker', 'Manage a remote Docker environment'), new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.AGENT, 'Agent', 'fa fa-bolt', 'Connect to a Portainer agent'), - new PortainerEndpointInitFormValueEndpointSection(PortainerEndpointConnectionTypes.AZURE, 'Azure', 'fab fa-microsoft', 'Connect to Microsoft Azure ACI'), ]); diff --git a/app/portainer/views/init/endpoint/includes/azure.html b/app/portainer/views/init/endpoint/includes/azure.html deleted file mode 100644 index dca7e4953..000000000 --- a/app/portainer/views/init/endpoint/includes/azure.html +++ /dev/null @@ -1,63 +0,0 @@ -
- Information -
-
-
- -

This feature is experimental.

-

- Connect to Microsoft Azure to manage Azure Container Instances (ACI). -

-

- - Have a look at - the Azure documentation - to retrieve the credentials required below. -

-
-
-
-
- Environment -
- -
- -
- -
-
- -
- Azure credentials -
- -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- diff --git a/app/portainer/views/init/endpoint/includes/remote.html b/app/portainer/views/init/endpoint/includes/remote.html deleted file mode 100644 index a6e23ab15..000000000 --- a/app/portainer/views/init/endpoint/includes/remote.html +++ /dev/null @@ -1,120 +0,0 @@ -
- Information -
-
-
- -

- Connect Portainer to a remote Docker environment using the Docker API over TCP. -

-

- - The Docker API must be exposed over TCP. You can find more information about how to expose the Docker API over TCP - in the Docker documentation. -

-
-
-
-
- Environment -
- -
- -
- -
-
- - -
- -
- -
-
- - -
-
- - -
-
- - -
- -
-
- - -
-
- - -
-
- - -
-
- -
- Required TLS files -
- -
- -
- - - {{ ctrl.formValues.TLSCACert.name }} - - - -
-
- -
- -
- -
- - - {{ ctrl.formValues.TLSCert.name }} - - - -
-
- - -
- -
- - - {{ ctrl.formValues.TLSKey.name }} - - - -
-
- -
-
- diff --git a/app/portainer/views/init/endpoint/initEndpoint.html b/app/portainer/views/init/endpoint/initEndpoint.html index 3f3c50f99..df9b01966 100644 --- a/app/portainer/views/init/endpoint/initEndpoint.html +++ b/app/portainer/views/init/endpoint/initEndpoint.html @@ -45,12 +45,6 @@
-
- -
-
- -
diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index ca2d94ca3..e643a3679 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -1,12 +1,10 @@ import _ from 'lodash-es'; import angular from 'angular'; -import { PortainerEndpointInitFormValues, PortainerEndpointInitFormValueEndpointSections } from 'Portainer/models/endpoint/formValues'; -import { PortainerEndpointTypes, PortainerEndpointConnectionTypes } from 'Portainer/models/endpoint/models'; +import { PortainerEndpointInitFormValueEndpointSections, PortainerEndpointInitFormValues } from 'Portainer/models/endpoint/formValues'; +import { PortainerEndpointConnectionTypes, PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; require('./includes/localDocker.html'); require('./includes/localKubernetes.html'); -require('./includes/remote.html'); -require('./includes/azure.html'); require('./includes/agent.html'); class InitEndpointController { @@ -23,8 +21,6 @@ class InitEndpointController { this.createLocalEndpointAsync = this.createLocalEndpointAsync.bind(this); this.createLocalKubernetesEndpointAsync = this.createLocalKubernetesEndpointAsync.bind(this); this.createAgentEndpointAsync = this.createAgentEndpointAsync.bind(this); - this.createAzureEndpointAsync = this.createAzureEndpointAsync.bind(this); - this.createRemoteEndpointAsync = this.createRemoteEndpointAsync.bind(this); } $onInit() { @@ -43,30 +39,12 @@ class InitEndpointController { this.PortainerEndpointConnectionTypes = PortainerEndpointConnectionTypes; } - isRemoteConnectButtonDisabled() { - return ( - this.state.actionInProgress || - !this.formValues.Name || - !this.formValues.URL || - (this.formValues.TLS && - ((this.formValues.TLSVerify && !this.formValues.TLSCACert) || (!this.formValues.TLSSKipClientVerify && (!this.formValues.TLSCert || !this.formValues.TLSKey)))) - ); - } - - isAzureConnectButtonDisabled() { - return this.state.actionInProgress || !this.formValues.Name || !this.formValues.AzureApplicationId || !this.formValues.AzureTenantId || !this.formValues.AzureAuthenticationKey; - } - isConnectButtonDisabled() { switch (this.formValues.ConnectionType) { case PortainerEndpointConnectionTypes.DOCKER_LOCAL: return this.state.actionInProgress; case PortainerEndpointConnectionTypes.KUBERNETES_LOCAL: return this.state.actionInProgress; - case PortainerEndpointConnectionTypes.REMOTE: - return this.isRemoteConnectButtonDisabled(); - case PortainerEndpointConnectionTypes.AZURE: - return this.isAzureConnectButtonDisabled(); case PortainerEndpointConnectionTypes.AGENT: return this.state.actionInProgress || !this.formValues.Name || !this.formValues.URL; default: @@ -80,10 +58,6 @@ class InitEndpointController { return this.createLocalEndpoint(); case PortainerEndpointConnectionTypes.KUBERNETES_LOCAL: return this.createLocalKubernetesEndpoint(); - case PortainerEndpointConnectionTypes.REMOTE: - return this.createRemoteEndpoint(); - case PortainerEndpointConnectionTypes.AZURE: - return this.createAzureEndpoint(); case PortainerEndpointConnectionTypes.AGENT: return this.createAgentEndpoint(); default: @@ -165,58 +139,6 @@ class InitEndpointController { createAgentEndpoint() { return this.$async(this.createAgentEndpointAsync); } - - /** - * DOCKER REMOTE (1) - */ - async createRemoteEndpointAsync() { - try { - this.state.actionInProgress = true; - const name = this.formValues.Name; - const type = PortainerEndpointTypes.DockerEnvironment; - const URL = this.formValues.URL; - const PublicURL = URL.split(':')[0]; - const TLS = this.formValues.TLS; - const TLSSkipVerify = TLS && this.formValues.TLSSkipVerify; - const TLSSKipClientVerify = TLS && this.formValues.TLSSKipClientVerify; - const TLSCAFile = TLSSkipVerify ? null : this.formValues.TLSCACert; - const TLSCertFile = TLSSKipClientVerify ? null : this.formValues.TLSCert; - const TLSKeyFile = TLSSKipClientVerify ? null : this.formValues.TLSKey; - await this.EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); - this.$state.go('portainer.home'); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); - } finally { - this.state.actionInProgress = false; - } - } - - createRemoteEndpoint() { - return this.$async(this.createAgentEndpointAsync); - } - - /** - * AZURE (4) - */ - async createAzureEndpointAsync() { - try { - this.state.actionInProgress = true; - var name = this.formValues.Name; - var applicationId = this.formValues.AzureApplicationId; - var tenantId = this.formValues.AzureTenantId; - var authenticationKey = this.formValues.AzureAuthenticationKey; - await this.EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, []); - this.$state.go('portainer.home'); - } catch (err) { - this.Notifications.error('Failure', err, 'Unable to connect to the Azure environment'); - } finally { - this.state.actionInProgress = false; - } - } - - createAzureEndpoint() { - return this.$async(this.createAgentEndpointAsync); - } } export default InitEndpointController; From e82833a363d0f2a81686106fbc3a14d54184d145 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 8 Jul 2020 09:53:10 +1200 Subject: [PATCH 047/195] chore(README): update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d5fe4230..825476455 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/) From db4a5292becbc4c739fa886cf9dfa170fc69b366 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 8 Jul 2020 00:57:52 +0300 Subject: [PATCH 048/195] refactor(errors): reorganize errors (#3938) * refactor(bolt): move ErrObjectNotFound to bolt * refactor(http): move ErrUnauthorized to http package * refactor(http): move ErrResourceAccessDenied to http errors * refactor(http): move security errors to package * refactor(users): move user errors to users package * refactor(errors): move single errors to their package * refactor(schedules): move schedule error to package * refactor(http): move endpoint error to http package * refactor(docker): move docker errors to package * refactor(filesystem): move filesystem errors to package * refactor(errors): remove portainer.Error * style(chisel): reorder imports * fix(stacks): remove portainer.Error --- api/bolt/datastore.go | 3 +- api/bolt/errors/errors.go | 7 ++ api/bolt/init.go | 5 +- api/bolt/internal/db.go | 4 +- api/bolt/migrator/migrate_dbversion0.go | 3 +- api/bolt/stack/stack.go | 3 +- api/bolt/team/team.go | 3 +- api/bolt/user/user.go | 3 +- api/bolt/version/version.go | 4 +- api/bolt/webhook/webhook.go | 5 +- api/chisel/service.go | 9 +- api/cli/cli.go | 11 +- api/docker/client.go | 6 +- api/docker/errors.go | 8 ++ api/errors.go | 117 ------------------ api/exec/swarm_stack.go | 3 +- api/filesystem/filesystem.go | 10 +- api/http/client/client.go | 8 +- api/http/errors/errors.go | 12 ++ api/http/handler/auth/authenticate.go | 17 +-- api/http/handler/auth/authenticate_oauth.go | 19 +-- .../handler/dockerhub/dockerhub_update.go | 3 +- .../handler/edgegroups/edgegroup_create.go | 9 +- .../handler/edgegroups/edgegroup_delete.go | 6 +- .../handler/edgegroups/edgegroup_inspect.go | 3 +- .../handler/edgegroups/edgegroup_update.go | 12 +- api/http/handler/edgejobs/edgejob_create.go | 10 +- api/http/handler/edgejobs/edgejob_delete.go | 3 +- api/http/handler/edgejobs/edgejob_file.go | 3 +- api/http/handler/edgejobs/edgejob_inspect.go | 8 +- .../edgejobs/edgejob_tasklogs_clear.go | 3 +- .../edgejobs/edgejob_tasklogs_collect.go | 3 +- .../handler/edgejobs/edgejob_tasks_list.go | 12 +- api/http/handler/edgejobs/edgejob_update.go | 12 +- .../handler/edgestacks/edgestack_create.go | 22 ++-- .../handler/edgestacks/edgestack_delete.go | 3 +- api/http/handler/edgestacks/edgestack_file.go | 3 +- .../handler/edgestacks/edgestack_inspect.go | 3 +- .../edgestacks/edgestack_status_update.go | 12 +- .../handler/edgestacks/edgestack_update.go | 8 +- .../endpointedge/endpoint_edgejob_logs.go | 7 +- .../endpoint_edgestack_inspect.go | 5 +- .../endpointgroups/endpointgroup_create.go | 3 +- .../endpointgroups/endpointgroup_delete.go | 6 +- .../endpointgroup_endpoint_add.go | 5 +- .../endpointgroup_endpoint_delete.go | 5 +- .../endpointgroups/endpointgroup_inspect.go | 3 +- .../endpointgroups/endpointgroup_update.go | 3 +- api/http/handler/endpointproxy/proxy_azure.go | 3 +- .../handler/endpointproxy/proxy_docker.go | 3 +- .../handler/endpointproxy/proxy_kubernetes.go | 3 +- .../handler/endpointproxy/proxy_storidge.go | 6 +- api/http/handler/endpoints/endpoint_create.go | 20 +-- api/http/handler/endpoints/endpoint_delete.go | 3 +- .../endpoints/endpoint_extension_add.go | 8 +- .../endpoints/endpoint_extension_remove.go | 3 +- .../handler/endpoints/endpoint_inspect.go | 3 +- .../handler/endpoints/endpoint_snapshot.go | 3 +- .../endpoints/endpoint_status_inspect.go | 5 +- api/http/handler/endpoints/endpoint_update.go | 3 +- .../handler/extensions/extension_create.go | 5 +- .../handler/extensions/extension_delete.go | 3 +- .../handler/extensions/extension_inspect.go | 6 +- .../handler/extensions/extension_update.go | 6 +- .../handler/extensions/extension_upload.go | 5 +- api/http/handler/registries/proxy.go | 8 +- .../registries/proxy_management_gitlab.go | 8 +- .../handler/registries/registry_configure.go | 12 +- .../handler/registries/registry_create.go | 9 +- .../handler/registries/registry_delete.go | 3 +- .../handler/registries/registry_inspect.go | 7 +- .../handler/registries/registry_update.go | 6 +- .../resourcecontrol_create.go | 9 +- .../resourcecontrol_delete.go | 3 +- .../resourcecontrol_update.go | 8 +- api/http/handler/settings/settings_update.go | 12 +- .../handler/stacks/create_compose_stack.go | 22 ++-- .../handler/stacks/create_kubernetes_stack.go | 5 +- api/http/handler/stacks/create_swarm_stack.go | 28 ++--- api/http/handler/stacks/handler.go | 13 +- api/http/handler/stacks/stack_create.go | 6 +- api/http/handler/stacks/stack_delete.go | 24 ++-- api/http/handler/stacks/stack_file.go | 8 +- api/http/handler/stacks/stack_inspect.go | 8 +- api/http/handler/stacks/stack_list.go | 5 +- api/http/handler/stacks/stack_migrate.go | 13 +- api/http/handler/stacks/stack_update.go | 16 +-- api/http/handler/tags/tag_create.go | 5 +- api/http/handler/tags/tag_delete.go | 3 +- .../teammemberships/teammembership_create.go | 12 +- .../teammemberships/teammembership_delete.go | 6 +- .../teammemberships/teammembership_list.go | 4 +- .../teammemberships/teammembership_update.go | 15 ++- api/http/handler/teams/team_create.go | 8 +- api/http/handler/teams/team_delete.go | 3 +- api/http/handler/teams/team_inspect.go | 6 +- api/http/handler/teams/team_memberships.go | 3 +- api/http/handler/teams/team_update.go | 3 +- api/http/handler/upload/upload_tls.go | 3 +- api/http/handler/users/admin_check.go | 3 +- api/http/handler/users/admin_init.go | 9 +- api/http/handler/users/handler.go | 10 ++ api/http/handler/users/user_create.go | 17 +-- api/http/handler/users/user_delete.go | 7 +- api/http/handler/users/user_inspect.go | 9 +- api/http/handler/users/user_memberships.go | 3 +- api/http/handler/users/user_update.go | 19 +-- .../handler/users/user_update_password.go | 15 ++- api/http/handler/webhooks/webhook_create.go | 12 +- api/http/handler/webhooks/webhook_execute.go | 8 +- api/http/handler/websocket/attach.go | 3 +- api/http/handler/websocket/exec.go | 3 +- api/http/handler/websocket/pod.go | 3 +- api/http/proxy/factory/docker/transport.go | 3 +- api/http/security/bouncer.go | 35 +++--- api/http/security/context.go | 5 +- api/http/security/errors.go | 7 ++ api/http/security/rate_limiter.go | 4 +- api/http/security/rbac.go | 2 +- api/jwt/jwt.go | 11 +- api/ldap/ldap.go | 12 +- 121 files changed, 550 insertions(+), 477 deletions(-) create mode 100644 api/bolt/errors/errors.go create mode 100644 api/docker/errors.go delete mode 100644 api/errors.go create mode 100644 api/http/errors/errors.go create mode 100644 api/http/security/errors.go diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index e7311a1bd..068670b0e 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -15,6 +15,7 @@ import ( "github.com/portainer/portainer/api/bolt/endpoint" "github.com/portainer/portainer/api/bolt/endpointgroup" "github.com/portainer/portainer/api/bolt/endpointrelation" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/extension" "github.com/portainer/portainer/api/bolt/migrator" "github.com/portainer/portainer/api/bolt/registry" @@ -123,7 +124,7 @@ func (store *Store) MigrateData() error { } version, err := store.VersionService.DBVersion() - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { version = 0 } else if err != nil { return err diff --git a/api/bolt/errors/errors.go b/api/bolt/errors/errors.go new file mode 100644 index 000000000..c9a142189 --- /dev/null +++ b/api/bolt/errors/errors.go @@ -0,0 +1,7 @@ +package errors + +import "errors" + +var ( + ErrObjectNotFound = errors.New("Object not found inside the database") +) diff --git a/api/bolt/init.go b/api/bolt/init.go index df6277ed8..244f4e961 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -2,13 +2,14 @@ package bolt import ( portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/authorization" ) // Init creates the default data set. func (store *Store) Init() error { _, err := store.SettingsService.Settings() - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { defaultSettings := &portainer.Settings{ AuthenticationMethod: portainer.AuthenticationInternal, BlackListedLabels: make([]portainer.Pair, 0), @@ -42,7 +43,7 @@ func (store *Store) Init() error { } _, err = store.DockerHubService.DockerHub() - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { defaultDockerHub := &portainer.DockerHub{ Authentication: false, Username: "", diff --git a/api/bolt/internal/db.go b/api/bolt/internal/db.go index 9689fa691..101e1ff00 100644 --- a/api/bolt/internal/db.go +++ b/api/bolt/internal/db.go @@ -4,7 +4,7 @@ import ( "encoding/binary" "github.com/boltdb/bolt" - "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // Itob returns an 8-byte big endian representation of v. @@ -36,7 +36,7 @@ func GetObject(db *bolt.DB, bucketName string, key []byte, object interface{}) e value := bucket.Get(key) if value == nil { - return portainer.ErrObjectNotFound + return errors.ErrObjectNotFound } data = make([]byte, len(value)) diff --git a/api/bolt/migrator/migrate_dbversion0.go b/api/bolt/migrator/migrate_dbversion0.go index 04d1a93b5..1ed54c41d 100644 --- a/api/bolt/migrator/migrate_dbversion0.go +++ b/api/bolt/migrator/migrate_dbversion0.go @@ -3,6 +3,7 @@ package migrator import ( "github.com/boltdb/bolt" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/user" ) @@ -22,7 +23,7 @@ func (m *Migrator) updateAdminUserToDBVersion1() error { if err != nil { return err } - } else if err != nil && err != portainer.ErrObjectNotFound { + } else if err != nil && err != errors.ErrObjectNotFound { return err } return nil diff --git a/api/bolt/stack/stack.go b/api/bolt/stack/stack.go index 54d5facd1..a5145ba35 100644 --- a/api/bolt/stack/stack.go +++ b/api/bolt/stack/stack.go @@ -2,6 +2,7 @@ package stack import ( "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" @@ -64,7 +65,7 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) { } if stack == nil { - return portainer.ErrObjectNotFound + return errors.ErrObjectNotFound } return nil diff --git a/api/bolt/team/team.go b/api/bolt/team/team.go index d77aed90e..e88e6a3f8 100644 --- a/api/bolt/team/team.go +++ b/api/bolt/team/team.go @@ -2,6 +2,7 @@ package team import ( "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" @@ -64,7 +65,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) { } if team == nil { - return portainer.ErrObjectNotFound + return errors.ErrObjectNotFound } return nil diff --git a/api/bolt/user/user.go b/api/bolt/user/user.go index 63e3dfdc9..fa0c5c151 100644 --- a/api/bolt/user/user.go +++ b/api/bolt/user/user.go @@ -2,6 +2,7 @@ package user import ( "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" @@ -64,7 +65,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error) } if user == nil { - return portainer.ErrObjectNotFound + return errors.ErrObjectNotFound } return nil }) diff --git a/api/bolt/version/version.go b/api/bolt/version/version.go index 18e9ab6d5..992224eea 100644 --- a/api/bolt/version/version.go +++ b/api/bolt/version/version.go @@ -4,7 +4,7 @@ import ( "strconv" "github.com/boltdb/bolt" - "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/internal" ) @@ -40,7 +40,7 @@ func (service *Service) DBVersion() (int, error) { value := bucket.Get([]byte(versionKey)) if value == nil { - return portainer.ErrObjectNotFound + return errors.ErrObjectNotFound } data = make([]byte, len(value)) diff --git a/api/bolt/webhook/webhook.go b/api/bolt/webhook/webhook.go index 38377cdb4..d18900de9 100644 --- a/api/bolt/webhook/webhook.go +++ b/api/bolt/webhook/webhook.go @@ -2,6 +2,7 @@ package webhook import ( "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" @@ -87,7 +88,7 @@ func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, erro } if webhook == nil { - return portainer.ErrObjectNotFound + return errors.ErrObjectNotFound } return nil @@ -118,7 +119,7 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) } if webhook == nil { - return portainer.ErrObjectNotFound + return errors.ErrObjectNotFound } return nil diff --git a/api/chisel/service.go b/api/chisel/service.go index 12ea9ef31..e66983222 100644 --- a/api/chisel/service.go +++ b/api/chisel/service.go @@ -7,11 +7,10 @@ import ( "time" "github.com/dchest/uniuri" - - cmap "github.com/orcaman/concurrent-map" - chserver "github.com/jpillora/chisel/server" - portainer "github.com/portainer/portainer/api" + cmap "github.com/orcaman/concurrent-map" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) const ( @@ -88,7 +87,7 @@ func (service *Service) retrievePrivateKeySeed() (string, error) { var serverInfo *portainer.TunnelServerInfo serverInfo, err := service.dataStore.TunnelServer().Info() - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { keySeed := uniuri.NewLen(16) serverInfo = &portainer.TunnelServerInfo{ diff --git a/api/cli/cli.go b/api/cli/cli.go index fed9b46d0..4bcec03fd 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "time" "github.com/portainer/portainer/api" @@ -15,11 +16,11 @@ import ( // Service implements the CLIService interface type Service struct{} -const ( - errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://") - errSocketOrNamedPipeNotFound = portainer.Error("Unable to locate Unix socket or named pipe") - errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval") - errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file") +var ( + errInvalidEndpointProtocol = errors.New("Invalid endpoint protocol: Portainer only supports unix://, npipe:// or tcp://") + errSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe") + errInvalidSnapshotInterval = errors.New("Invalid snapshot interval") + errAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file") ) // ParseFlags parse the CLI flags and return a portainer.Flags struct diff --git a/api/docker/client.go b/api/docker/client.go index 6ccb5ad7c..dace4800d 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -1,6 +1,7 @@ package docker import ( + "errors" "fmt" "net/http" "strings" @@ -11,8 +12,9 @@ import ( "github.com/portainer/portainer/api/crypto" ) +var errUnsupportedEnvironmentType = errors.New("Environment not supported") + const ( - unsupportedEnvironmentType = portainer.Error("Environment not supported") defaultDockerRequestTimeout = 60 dockerClientVersion = "1.37" ) @@ -36,7 +38,7 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers // with an agent enabled endpoint to target a specific node in an agent cluster. func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) { if endpoint.Type == portainer.AzureEnvironment { - return nil, unsupportedEnvironmentType + return nil, errUnsupportedEnvironmentType } else if endpoint.Type == portainer.AgentOnDockerEnvironment { return createAgentClient(endpoint, factory.signatureService, nodeName) } else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment { diff --git a/api/docker/errors.go b/api/docker/errors.go new file mode 100644 index 000000000..80611c8b4 --- /dev/null +++ b/api/docker/errors.go @@ -0,0 +1,8 @@ +package docker + +import "errors" + +// Docker errors +var ( + ErrUnableToPingEndpoint = errors.New("Unable to communicate with the endpoint") +) diff --git a/api/errors.go b/api/errors.go deleted file mode 100644 index 8e09838a1..000000000 --- a/api/errors.go +++ /dev/null @@ -1,117 +0,0 @@ -package portainer - -// General errors. -const ( - ErrUnauthorized = Error("Unauthorized") - ErrResourceAccessDenied = Error("Access denied to resource") - ErrAuthorizationRequired = Error("Authorization required for this operation") - ErrObjectNotFound = Error("Object not found inside the database") - ErrMissingSecurityContext = Error("Unable to find security details in request context") -) - -// User errors. -const ( - ErrUserAlreadyExists = Error("User already exists") - ErrInvalidUsername = Error("Invalid username. White spaces are not allowed") - ErrAdminAlreadyInitialized = Error("An administrator user already exists") - ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator") - ErrCannotRemoveLastLocalAdmin = Error("Cannot remove the last local administrator account") -) - -// Team errors. -const ( - ErrTeamAlreadyExists = Error("Team already exists") -) - -// TeamMembership errors. -const ( - ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team") -) - -// ResourceControl errors. -const ( - ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource") - ErrInvalidResourceControlType = Error("Unsupported resource control type") -) - -// Endpoint errors. -const ( - ErrEndpointAccessDenied = Error("Access denied to endpoint") -) - -// Azure environment errors -const ( - ErrAzureInvalidCredentials = Error("Invalid Azure credentials") -) - -// Endpoint group errors. -const ( - ErrCannotRemoveDefaultGroup = Error("Cannot remove the default endpoint group") -) - -// Registry errors. -const ( - ErrRegistryAlreadyExists = Error("A registry is already defined for this URL") -) - -// Stack errors -const ( - ErrStackAlreadyExists = Error("A stack already exists with this name") - ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository") - ErrStackNotExternal = Error("Not an external stack") -) - -// Tag errors -const ( - ErrTagAlreadyExists = Error("A tag already exists with this name") -) - -// Endpoint extensions error -const ( - ErrEndpointExtensionNotSupported = Error("This extension is not supported") - ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint") -) - -// Crypto errors. -const ( - ErrCryptoHashFailure = Error("Unable to hash data") -) - -// JWT errors. -const ( - ErrSecretGeneration = Error("Unable to generate secret key") - ErrInvalidJWTToken = Error("Invalid JWT token") - ErrMissingContextData = Error("Unable to find JWT data in request context") -) - -// File errors. -const ( - ErrUndefinedTLSFileType = Error("Undefined TLS file type") -) - -// Extension errors. -const ( - ErrExtensionAlreadyEnabled = Error("This extension is already enabled") -) - -// Docker errors. -const ( - ErrUnableToPingEndpoint = Error("Unable to communicate with the endpoint") -) - -// Schedule errors. -const ( - ErrHostManagementFeaturesDisabled = Error("Host management features are disabled") -) - -// Error represents an application error. -type Error string - -// Error returns the error message. -func (e Error) Error() string { return string(e) } - -// Webhook errors -const ( - ErrWebhookAlreadyExists = Error("A webhook for this resource already exists") - ErrUnsupportedWebhookType = Error("Webhooks for this resource are not currently supported") -) diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index 94d9e0904..31fb48836 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -3,6 +3,7 @@ package exec import ( "bytes" "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -103,7 +104,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor err := cmd.Run() if err != nil { - return portainer.Error(stderr.String()) + return errors.New(stderr.String()) } return nil diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index eb5024a93..ff99bd32c 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "encoding/pem" + "errors" "fmt" "io/ioutil" @@ -50,6 +51,9 @@ const ( TempPath = "tmp" ) +// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type +var ErrUndefinedTLSFileType = errors.New("Undefined TLS file type") + // Service represents a service for managing files and directories. type Service struct { dataStorePath string @@ -194,7 +198,7 @@ func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer. case portainer.TLSFileKey: fileName = TLSKeyFile default: - return "", portainer.ErrUndefinedTLSFileType + return "", ErrUndefinedTLSFileType } tlsFilePath := path.Join(storePath, fileName) @@ -217,7 +221,7 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF case portainer.TLSFileKey: fileName = TLSKeyFile default: - return "", portainer.ErrUndefinedTLSFileType + return "", ErrUndefinedTLSFileType } return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil } @@ -243,7 +247,7 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT case portainer.TLSFileKey: fileName = TLSKeyFile default: - return portainer.ErrUndefinedTLSFileType + return ErrUndefinedTLSFileType } filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName) diff --git a/api/http/client/client.go b/api/http/client/client.go index fb690105f..ba185950a 100644 --- a/api/http/client/client.go +++ b/api/http/client/client.go @@ -3,6 +3,7 @@ package client import ( "crypto/tls" "encoding/json" + "errors" "fmt" "io/ioutil" "log" @@ -14,9 +15,10 @@ import ( "github.com/portainer/portainer/api" ) +var errInvalidResponseStatus = errors.New("Invalid response status (expecting 200)") + const ( - errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)") - defaultHTTPTimeout = 5 + defaultHTTPTimeout = 5 ) // HTTPClient represents a client to send HTTP requests. @@ -56,7 +58,7 @@ func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portain } if response.StatusCode != http.StatusOK { - return nil, portainer.ErrAzureInvalidCredentials + return nil, errors.New("Invalid Azure credentials") } var token AzureAuthenticationResponse diff --git a/api/http/errors/errors.go b/api/http/errors/errors.go new file mode 100644 index 000000000..2e6aeceb5 --- /dev/null +++ b/api/http/errors/errors.go @@ -0,0 +1,12 @@ +package errors + +import "errors" + +var ( + // ErrEndpointAccessDenied Access denied to endpoint error + ErrEndpointAccessDenied = errors.New("Access denied to endpoint") + // ErrUnauthorized Unauthorized error + ErrUnauthorized = errors.New("Unauthorized") + // ErrResourceAccessDenied Access denied to resource error + ErrResourceAccessDenied = errors.New("Access denied to resource") +) diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index eb9592566..52c1982b9 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -1,6 +1,7 @@ package auth import ( + "errors" "log" "net/http" "strings" @@ -10,6 +11,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/internal/authorization" ) @@ -24,10 +27,10 @@ type authenticateResponse struct { func (payload *authenticatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Username) { - return portainer.Error("Invalid username") + return errors.New("Invalid username") } if govalidator.IsNull(payload.Password) { - return portainer.Error("Invalid password") + return errors.New("Invalid password") } return nil } @@ -45,19 +48,19 @@ func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *ht } u, err := handler.DataStore.User().UserByUsername(payload.Username) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err} } - if err == portainer.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) { - return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized} + if err == bolterrors.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) { + return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized} } if settings.AuthenticationMethod == portainer.AuthenticationLDAP { if u == nil && settings.LDAPSettings.AutoCreateUsers { return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings) } else if u == nil && !settings.LDAPSettings.AutoCreateUsers { - return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized} } return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings) } @@ -87,7 +90,7 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer. func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError { err := handler.CryptoService.CompareHashAndData(user.Password, password) if err != nil { - return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized} } return handler.writeToken(w, user) diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index 68f998495..f0d6d59a2 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -2,7 +2,7 @@ package auth import ( "encoding/json" - "github.com/portainer/portainer/api/internal/authorization" + "errors" "io/ioutil" "log" "net/http" @@ -11,6 +11,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" + "github.com/portainer/portainer/api/internal/authorization" ) type oauthPayload struct { @@ -19,7 +22,7 @@ type oauthPayload struct { func (payload *oauthPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Code) { - return portainer.Error("Invalid OAuth authorization code") + return errors.New("Invalid OAuth authorization code") } return nil } @@ -66,7 +69,7 @@ func (handler *Handler) authenticateThroughExtension(code, licenseKey string, se } if resp.StatusCode != http.StatusOK { - return "", portainer.Error(extResp.Err + ":" + extResp.Details) + return "", errors.New(extResp.Err + ":" + extResp.Details) } return extResp.Username, nil @@ -85,11 +88,11 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h } if settings.AuthenticationMethod != 3 { - return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", portainer.Error("OAuth authentication is not enabled")} + return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")} } extension, err := handler.DataStore.Extension().Extension(portainer.OAuthAuthenticationExtension) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} @@ -98,16 +101,16 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings) if err != nil { log.Printf("[DEBUG] - OAuth authentication error: %s", err) - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized} } user, err := handler.DataStore.User().UserByUsername(username) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err} } if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers { - return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", httperrors.ErrUnauthorized} } if user == nil { diff --git a/api/http/handler/dockerhub/dockerhub_update.go b/api/http/handler/dockerhub/dockerhub_update.go index 78787566e..e12d3ad24 100644 --- a/api/http/handler/dockerhub/dockerhub_update.go +++ b/api/http/handler/dockerhub/dockerhub_update.go @@ -1,6 +1,7 @@ package dockerhub import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -18,7 +19,7 @@ type dockerhubUpdatePayload struct { func (payload *dockerhubUpdatePayload) Validate(r *http.Request) error { if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) { - return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled") + return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled") } return nil } diff --git a/api/http/handler/edgegroups/edgegroup_create.go b/api/http/handler/edgegroups/edgegroup_create.go index 90c733073..3e767891e 100644 --- a/api/http/handler/edgegroups/edgegroup_create.go +++ b/api/http/handler/edgegroups/edgegroup_create.go @@ -1,6 +1,7 @@ package edgegroups import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -20,13 +21,13 @@ type edgeGroupCreatePayload struct { func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid Edge group name") + return errors.New("Invalid Edge group name") } if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) { - return portainer.Error("TagIDs is mandatory for a dynamic Edge group") + return errors.New("TagIDs is mandatory for a dynamic Edge group") } if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) { - return portainer.Error("Endpoints is mandatory for a static Edge group") + return errors.New("Endpoints is mandatory for a static Edge group") } return nil } @@ -45,7 +46,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) for _, edgeGroup := range edgeGroups { if edgeGroup.Name == payload.Name { - return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")} + return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", errors.New("Edge group name must be unique")} } } diff --git a/api/http/handler/edgegroups/edgegroup_delete.go b/api/http/handler/edgegroups/edgegroup_delete.go index 806888617..a45486bfc 100644 --- a/api/http/handler/edgegroups/edgegroup_delete.go +++ b/api/http/handler/edgegroups/edgegroup_delete.go @@ -1,12 +1,14 @@ package edgegroups import ( + "errors" "net/http" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { @@ -16,7 +18,7 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) } _, err = handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err} @@ -30,7 +32,7 @@ func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) for _, edgeStack := range edgeStacks { for _, groupID := range edgeStack.EdgeGroups { if groupID == portainer.EdgeGroupID(edgeGroupID) { - return &httperror.HandlerError{http.StatusForbidden, "Edge group is used by an Edge stack", portainer.Error("Edge group is used by an Edge stack")} + return &httperror.HandlerError{http.StatusForbidden, "Edge group is used by an Edge stack", errors.New("Edge group is used by an Edge stack")} } } } diff --git a/api/http/handler/edgegroups/edgegroup_inspect.go b/api/http/handler/edgegroups/edgegroup_inspect.go index db88789c1..9f7f31173 100644 --- a/api/http/handler/edgegroups/edgegroup_inspect.go +++ b/api/http/handler/edgegroups/edgegroup_inspect.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { @@ -16,7 +17,7 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) } edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err} diff --git a/api/http/handler/edgegroups/edgegroup_update.go b/api/http/handler/edgegroups/edgegroup_update.go index 2be4fb346..e71f48457 100644 --- a/api/http/handler/edgegroups/edgegroup_update.go +++ b/api/http/handler/edgegroups/edgegroup_update.go @@ -1,6 +1,7 @@ package edgegroups import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -8,6 +9,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/edge" ) @@ -21,13 +23,13 @@ type edgeGroupUpdatePayload struct { func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid Edge group name") + return errors.New("Invalid Edge group name") } if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) { - return portainer.Error("TagIDs is mandatory for a dynamic Edge group") + return errors.New("TagIDs is mandatory for a dynamic Edge group") } if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) { - return portainer.Error("Endpoints is mandatory for a static Edge group") + return errors.New("Endpoints is mandatory for a static Edge group") } return nil } @@ -45,7 +47,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) } edgeGroup, err := handler.DataStore.EdgeGroup().EdgeGroup(portainer.EdgeGroupID(edgeGroupID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err} @@ -58,7 +60,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) } for _, edgeGroup := range edgeGroups { if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) { - return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")} + return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", errors.New("Edge group name must be unique")} } } diff --git a/api/http/handler/edgejobs/edgejob_create.go b/api/http/handler/edgejobs/edgejob_create.go index 33f91493a..e329316a7 100644 --- a/api/http/handler/edgejobs/edgejob_create.go +++ b/api/http/handler/edgejobs/edgejob_create.go @@ -41,7 +41,7 @@ type edgeJobCreateFromFileContentPayload struct { func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid Edge job name") + return errors.New("Invalid Edge job name") } if !govalidator.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) { @@ -49,15 +49,15 @@ func (payload *edgeJobCreateFromFileContentPayload) Validate(r *http.Request) er } if govalidator.IsNull(payload.CronExpression) { - return portainer.Error("Invalid cron expression") + return errors.New("Invalid cron expression") } if payload.Endpoints == nil || len(payload.Endpoints) == 0 { - return portainer.Error("Invalid endpoints payload") + return errors.New("Invalid endpoints payload") } if govalidator.IsNull(payload.FileContent) { - return portainer.Error("Invalid script file content") + return errors.New("Invalid script file content") } return nil @@ -114,7 +114,7 @@ func (payload *edgeJobCreateFromFilePayload) Validate(r *http.Request) error { file, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { - return portainer.Error("Invalid script file. Ensure that the file is uploaded correctly") + return errors.New("Invalid script file. Ensure that the file is uploaded correctly") } payload.File = file diff --git a/api/http/handler/edgejobs/edgejob_delete.go b/api/http/handler/edgejobs/edgejob_delete.go index da3e39ac6..66efdd55e 100644 --- a/api/http/handler/edgejobs/edgejob_delete.go +++ b/api/http/handler/edgejobs/edgejob_delete.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { @@ -17,7 +18,7 @@ func (handler *Handler) edgeJobDelete(w http.ResponseWriter, r *http.Request) *h } edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err} diff --git a/api/http/handler/edgejobs/edgejob_file.go b/api/http/handler/edgejobs/edgejob_file.go index cd02f0919..25e71366a 100644 --- a/api/http/handler/edgejobs/edgejob_file.go +++ b/api/http/handler/edgejobs/edgejob_file.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type edgeJobFileResponse struct { @@ -21,7 +22,7 @@ func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *htt } edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err} diff --git a/api/http/handler/edgejobs/edgejob_inspect.go b/api/http/handler/edgejobs/edgejob_inspect.go index 49857bb39..0625987cf 100644 --- a/api/http/handler/edgejobs/edgejob_inspect.go +++ b/api/http/handler/edgejobs/edgejob_inspect.go @@ -3,11 +3,11 @@ package edgejobs import ( "net/http" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" - httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type edgeJobInspectResponse struct { @@ -22,7 +22,7 @@ func (handler *Handler) edgeJobInspect(w http.ResponseWriter, r *http.Request) * } edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err} diff --git a/api/http/handler/edgejobs/edgejob_tasklogs_clear.go b/api/http/handler/edgejobs/edgejob_tasklogs_clear.go index c100260b3..e49f3c978 100644 --- a/api/http/handler/edgejobs/edgejob_tasklogs_clear.go +++ b/api/http/handler/edgejobs/edgejob_tasklogs_clear.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/edge_jobs/:id/tasks/:taskID/logs @@ -23,7 +24,7 @@ func (handler *Handler) edgeJobTasksClear(w http.ResponseWriter, r *http.Request } edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err} diff --git a/api/http/handler/edgejobs/edgejob_tasklogs_collect.go b/api/http/handler/edgejobs/edgejob_tasklogs_collect.go index e19af696c..cb09aa2db 100644 --- a/api/http/handler/edgejobs/edgejob_tasklogs_collect.go +++ b/api/http/handler/edgejobs/edgejob_tasklogs_collect.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) // POST request on /api/edge_jobs/:id/tasks/:taskID/logs @@ -22,7 +23,7 @@ func (handler *Handler) edgeJobTasksCollect(w http.ResponseWriter, r *http.Reque } edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err} diff --git a/api/http/handler/edgejobs/edgejob_tasks_list.go b/api/http/handler/edgejobs/edgejob_tasks_list.go index 57bbf3784..6b021255c 100644 --- a/api/http/handler/edgejobs/edgejob_tasks_list.go +++ b/api/http/handler/edgejobs/edgejob_tasks_list.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type taskContainer struct { @@ -18,22 +19,13 @@ type taskContainer struct { // GET request on /api/edge_jobs/:id/tasks func (handler *Handler) edgeJobTasksList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.DataStore.Settings().Settings() - if err != nil { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} - } - - if !settings.EnableEdgeComputeFeatures { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Edge compute features are disabled", portainer.ErrHostManagementFeaturesDisabled} - } - edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err} } edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err} diff --git a/api/http/handler/edgejobs/edgejob_update.go b/api/http/handler/edgejobs/edgejob_update.go index 559ba26f2..26d756320 100644 --- a/api/http/handler/edgejobs/edgejob_update.go +++ b/api/http/handler/edgejobs/edgejob_update.go @@ -10,6 +10,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type edgeJobUpdatePayload struct { @@ -28,15 +29,6 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error { } func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - settings, err := handler.DataStore.Settings().Settings() - if err != nil { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err} - } - - if !settings.EnableEdgeComputeFeatures { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Edge compute features are disabled", portainer.ErrHostManagementFeaturesDisabled} - } - edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge job identifier route variable", err} @@ -49,7 +41,7 @@ func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *h } edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge job with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err} diff --git a/api/http/handler/edgestacks/edgestack_create.go b/api/http/handler/edgestacks/edgestack_create.go index e6e96f02d..0f3224e95 100644 --- a/api/http/handler/edgestacks/edgestack_create.go +++ b/api/http/handler/edgestacks/edgestack_create.go @@ -82,13 +82,13 @@ type swarmStackFromFileContentPayload struct { func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } if govalidator.IsNull(payload.StackFileContent) { - return portainer.Error("Invalid stack file content") + return errors.New("Invalid stack file content") } if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 { - return portainer.Error("Edge Groups are mandatory for an Edge stack") + return errors.New("Edge Groups are mandatory for an Edge stack") } return nil } @@ -144,19 +144,19 @@ type swarmStackFromGitRepositoryPayload struct { func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) { - return portainer.Error("Invalid repository URL. Must correspond to a valid URL format") + return errors.New("Invalid repository URL. Must correspond to a valid URL format") } if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { - return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled") + return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled") } if govalidator.IsNull(payload.ComposeFilePathInRepository) { payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName } if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 { - return portainer.Error("Edge Groups are mandatory for an Edge stack") + return errors.New("Edge Groups are mandatory for an Edge stack") } return nil } @@ -218,20 +218,20 @@ type swarmStackFromFileUploadPayload struct { func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error { name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } payload.Name = name composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { - return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly") + return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly") } payload.StackFileContent = composeFileContent var edgeGroups []portainer.EdgeGroupID err = request.RetrieveMultiPartFormJSONValue(r, "EdgeGroups", &edgeGroups, false) if err != nil || len(edgeGroups) == 0 { - return portainer.Error("Edge Groups are mandatory for an Edge stack") + return errors.New("Edge Groups are mandatory for an Edge stack") } payload.EdgeGroups = edgeGroups return nil @@ -283,7 +283,7 @@ func (handler *Handler) validateUniqueName(name string) error { for _, stack := range edgeStacks { if strings.EqualFold(stack.Name, name) { - return portainer.Error("Edge stack name must be unique") + return errors.New("Edge stack name must be unique") } } return nil diff --git a/api/http/handler/edgestacks/edgestack_delete.go b/api/http/handler/edgestacks/edgestack_delete.go index 641d2d66e..ee01443f5 100644 --- a/api/http/handler/edgestacks/edgestack_delete.go +++ b/api/http/handler/edgestacks/edgestack_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/edge" ) @@ -17,7 +18,7 @@ func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) } edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err} diff --git a/api/http/handler/edgestacks/edgestack_file.go b/api/http/handler/edgestacks/edgestack_file.go index bcb20f626..d45dc433c 100644 --- a/api/http/handler/edgestacks/edgestack_file.go +++ b/api/http/handler/edgestacks/edgestack_file.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) type stackFileResponse struct { @@ -22,7 +23,7 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h } stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err} diff --git a/api/http/handler/edgestacks/edgestack_inspect.go b/api/http/handler/edgestacks/edgestack_inspect.go index ae417e603..50a960817 100644 --- a/api/http/handler/edgestacks/edgestack_inspect.go +++ b/api/http/handler/edgestacks/edgestack_inspect.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { @@ -16,7 +17,7 @@ func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) } edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err} diff --git a/api/http/handler/edgestacks/edgestack_status_update.go b/api/http/handler/edgestacks/edgestack_status_update.go index 5d5c3ebda..f1d870d51 100644 --- a/api/http/handler/edgestacks/edgestack_status_update.go +++ b/api/http/handler/edgestacks/edgestack_status_update.go @@ -1,6 +1,7 @@ package edgestacks import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -8,6 +9,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type updateStatusPayload struct { @@ -18,13 +20,13 @@ type updateStatusPayload struct { func (payload *updateStatusPayload) Validate(r *http.Request) error { if payload.Status == nil { - return portainer.Error("Invalid status") + return errors.New("Invalid status") } if payload.EndpointID == nil { - return portainer.Error("Invalid EndpointID") + return errors.New("Invalid EndpointID") } if *payload.Status == portainer.StatusError && govalidator.IsNull(payload.Error) { - return portainer.Error("Error message is mandatory when status is error") + return errors.New("Error message is mandatory when status is error") } return nil } @@ -36,7 +38,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req } stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} @@ -49,7 +51,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(*payload.EndpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/edgestacks/edgestack_update.go b/api/http/handler/edgestacks/edgestack_update.go index 39edc92e1..ccc65bf44 100644 --- a/api/http/handler/edgestacks/edgestack_update.go +++ b/api/http/handler/edgestacks/edgestack_update.go @@ -1,6 +1,7 @@ package edgestacks import ( + "errors" "net/http" "strconv" @@ -9,6 +10,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/edge" ) @@ -21,10 +23,10 @@ type updateEdgeStackPayload struct { func (payload *updateEdgeStackPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.StackFileContent) { - return portainer.Error("Invalid stack file content") + return errors.New("Invalid stack file content") } if payload.EdgeGroups != nil && len(payload.EdgeGroups) == 0 { - return portainer.Error("Edge Groups are mandatory for an Edge stack") + return errors.New("Edge Groups are mandatory for an Edge stack") } return nil } @@ -36,7 +38,7 @@ func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) } stack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(stackID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointedge/endpoint_edgejob_logs.go b/api/http/handler/endpointedge/endpoint_edgejob_logs.go index 8596ceeb5..d433f5789 100644 --- a/api/http/handler/endpointedge/endpoint_edgejob_logs.go +++ b/api/http/handler/endpointedge/endpoint_edgejob_logs.go @@ -7,7 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type logsPayload struct { @@ -26,7 +27,7 @@ func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Requ } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -49,7 +50,7 @@ func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Requ } edgeJob, err := handler.DataStore.EdgeJob().EdgeJob(portainer.EdgeJobID(edgeJobID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge job with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge job with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go index 54af3f3ef..9f57e115f 100644 --- a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go +++ b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) type configResponse struct { @@ -24,7 +25,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http. } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -41,7 +42,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http. } edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index d77464744..f50bf9736 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -1,6 +1,7 @@ package endpointgroups import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -19,7 +20,7 @@ type endpointGroupCreatePayload struct { func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid endpoint group name") + return errors.New("Invalid endpoint group name") } if payload.TagIDs == nil { payload.TagIDs = []portainer.TagID{} diff --git a/api/http/handler/endpointgroups/endpointgroup_delete.go b/api/http/handler/endpointgroups/endpointgroup_delete.go index 0dba168eb..39ab4ba1e 100644 --- a/api/http/handler/endpointgroups/endpointgroup_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_delete.go @@ -1,12 +1,14 @@ package endpointgroups import ( + "errors" "net/http" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/endpoint_groups/:id @@ -17,11 +19,11 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque } if endpointGroupID == 1 { - return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", portainer.ErrCannotRemoveDefaultGroup} + return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", errors.New("Cannot remove the default endpoint group")} } endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go index 710aa7df5..cce2e737a 100644 --- a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // PUT request on /api/endpoint_groups/:id/endpoints/:endpointId @@ -22,14 +23,14 @@ func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http. } endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go index f235b3ab9..595be6df9 100644 --- a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/endpoint_groups/:id/endpoints/:endpointId @@ -22,14 +23,14 @@ func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *ht } _, err = handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointgroups/endpointgroup_inspect.go b/api/http/handler/endpointgroups/endpointgroup_inspect.go index b931ba82b..fdf26a976 100644 --- a/api/http/handler/endpointgroups/endpointgroup_inspect.go +++ b/api/http/handler/endpointgroups/endpointgroup_inspect.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // GET request on /api/endpoint_groups/:id @@ -17,7 +18,7 @@ func (handler *Handler) endpointGroupInspect(w http.ResponseWriter, r *http.Requ } endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index c03215518..72cf3b3f6 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/tag" ) @@ -37,7 +38,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque } endpointGroup, err := handler.DataStore.EndpointGroup().EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go index a9e66b66b..9984b763f 100644 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -6,6 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "net/http" ) @@ -17,7 +18,7 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index 041a6f178..5b082e419 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -8,6 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "net/http" ) @@ -19,7 +20,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointproxy/proxy_kubernetes.go b/api/http/handler/endpointproxy/proxy_kubernetes.go index 744ab4340..be6400265 100644 --- a/api/http/handler/endpointproxy/proxy_kubernetes.go +++ b/api/http/handler/endpointproxy/proxy_kubernetes.go @@ -8,6 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "net/http" ) @@ -19,7 +20,7 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpointproxy/proxy_storidge.go b/api/http/handler/endpointproxy/proxy_storidge.go index 70a019a83..e44a74abc 100644 --- a/api/http/handler/endpointproxy/proxy_storidge.go +++ b/api/http/handler/endpointproxy/proxy_storidge.go @@ -3,11 +3,13 @@ package endpointproxy // TODO: legacy extension management import ( + "errors" "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "net/http" ) @@ -19,7 +21,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -38,7 +40,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt } if storidgeExtension == nil { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", portainer.ErrEndpointExtensionNotSupported} + return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", errors.New("This extension is not supported")} } proxyExtensionKey := strconv.Itoa(endpointID) + "_" + strconv.Itoa(int(portainer.StoridgeEndpointExtension)) + "_" + storidgeExtension.URL diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 9b8692903..210941fbc 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -39,13 +39,13 @@ type endpointCreatePayload struct { func (payload *endpointCreatePayload) Validate(r *http.Request) error { name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { - return portainer.Error("Invalid endpoint name") + return errors.New("Invalid endpoint name") } payload.Name = name endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false) if err != nil || endpointType == 0 { - return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)") + return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)") } payload.EndpointType = endpointType @@ -58,7 +58,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { var tagIDs []portainer.TagID err = request.RetrieveMultiPartFormJSONValue(r, "TagIds", &tagIDs, true) if err != nil { - return portainer.Error("Invalid TagIds parameter") + return errors.New("Invalid TagIds parameter") } payload.TagIDs = tagIDs if payload.TagIDs == nil { @@ -77,7 +77,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { if !payload.TLSSkipVerify { caCert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile") if err != nil { - return portainer.Error("Invalid CA certificate file. Ensure that the file is uploaded correctly") + return errors.New("Invalid CA certificate file. Ensure that the file is uploaded correctly") } payload.TLSCACertFile = caCert } @@ -85,13 +85,13 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { if !payload.TLSSkipClientVerify { cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile") if err != nil { - return portainer.Error("Invalid certificate file. Ensure that the file is uploaded correctly") + return errors.New("Invalid certificate file. Ensure that the file is uploaded correctly") } payload.TLSCertFile = cert key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile") if err != nil { - return portainer.Error("Invalid key file. Ensure that the file is uploaded correctly") + return errors.New("Invalid key file. Ensure that the file is uploaded correctly") } payload.TLSKeyFile = key } @@ -101,25 +101,25 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { case portainer.AzureEnvironment: azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false) if err != nil { - return portainer.Error("Invalid Azure application ID") + return errors.New("Invalid Azure application ID") } payload.AzureApplicationID = azureApplicationID azureTenantID, err := request.RetrieveMultiPartFormValue(r, "AzureTenantID", false) if err != nil { - return portainer.Error("Invalid Azure tenant ID") + return errors.New("Invalid Azure tenant ID") } payload.AzureTenantID = azureTenantID azureAuthenticationKey, err := request.RetrieveMultiPartFormValue(r, "AzureAuthenticationKey", false) if err != nil { - return portainer.Error("Invalid Azure authentication key") + return errors.New("Invalid Azure authentication key") } payload.AzureAuthenticationKey = azureAuthenticationKey default: endpointURL, err := request.RetrieveMultiPartFormValue(r, "URL", true) if err != nil { - return portainer.Error("Invalid endpoint URL") + return errors.New("Invalid endpoint URL") } payload.URL = endpointURL diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 7a280d703..b35fc8cf2 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/endpoints/:id @@ -18,7 +19,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) * } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpoints/endpoint_extension_add.go b/api/http/handler/endpoints/endpoint_extension_add.go index 4472913dc..e99e10caa 100644 --- a/api/http/handler/endpoints/endpoint_extension_add.go +++ b/api/http/handler/endpoints/endpoint_extension_add.go @@ -3,6 +3,7 @@ package endpoints // TODO: legacy extension management import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -10,6 +11,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type endpointExtensionAddPayload struct { @@ -19,10 +21,10 @@ type endpointExtensionAddPayload struct { func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error { if payload.Type != 1 { - return portainer.Error("Invalid type value. Value must be one of: 1 (Storidge)") + return errors.New("Invalid type value. Value must be one of: 1 (Storidge)") } if payload.Type == 1 && govalidator.IsNull(payload.URL) { - return portainer.Error("Invalid extension URL") + return errors.New("Invalid extension URL") } return nil } @@ -35,7 +37,7 @@ func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Requ } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpoints/endpoint_extension_remove.go b/api/http/handler/endpoints/endpoint_extension_remove.go index 6d81da363..99edf1bc8 100644 --- a/api/http/handler/endpoints/endpoint_extension_remove.go +++ b/api/http/handler/endpoints/endpoint_extension_remove.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/endpoints/:id/extensions/:extensionType @@ -19,7 +20,7 @@ func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.R } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index 01abe2c3d..1ce758eb9 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // GET request on /api/endpoints/:id @@ -17,7 +18,7 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 03816b642..e834fea5f 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/snapshot" ) @@ -18,7 +19,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index b29a1da5b..8c1dacd62 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -7,7 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) type stackStatusResponse struct { @@ -40,7 +41,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 8979571b0..305a57e54 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/internal/edge" "github.com/portainer/portainer/api/internal/tag" @@ -51,7 +52,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/extensions/extension_create.go b/api/http/handler/extensions/extension_create.go index dd602f3b0..8d7add8c4 100644 --- a/api/http/handler/extensions/extension_create.go +++ b/api/http/handler/extensions/extension_create.go @@ -1,6 +1,7 @@ package extensions import ( + "errors" "net/http" "strconv" @@ -17,7 +18,7 @@ type extensionCreatePayload struct { func (payload *extensionCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.License) { - return portainer.Error("Invalid license") + return errors.New("Invalid license") } return nil @@ -43,7 +44,7 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) for _, existingExtension := range extensions { if existingExtension.ID == extensionID && existingExtension.Enabled { - return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled} + return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", errors.New("This extension is already enabled")} } } diff --git a/api/http/handler/extensions/extension_delete.go b/api/http/handler/extensions/extension_delete.go index 789fa84fd..351de2406 100644 --- a/api/http/handler/extensions/extension_delete.go +++ b/api/http/handler/extensions/extension_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/extensions/:id @@ -18,7 +19,7 @@ func (handler *Handler) extensionDelete(w http.ResponseWriter, r *http.Request) extensionID := portainer.ExtensionID(extensionIdentifier) extension, err := handler.DataStore.Extension().Extension(extensionID) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} diff --git a/api/http/handler/extensions/extension_inspect.go b/api/http/handler/extensions/extension_inspect.go index 94c8292fe..4081a3cb7 100644 --- a/api/http/handler/extensions/extension_inspect.go +++ b/api/http/handler/extensions/extension_inspect.go @@ -3,12 +3,12 @@ package extensions import ( "net/http" - "github.com/portainer/portainer/api/http/client" - httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/client" ) // GET request on /api/extensions/:id @@ -26,7 +26,7 @@ func (handler *Handler) extensionInspect(w http.ResponseWriter, r *http.Request) } localExtension, err := handler.DataStore.Extension().Extension(extensionID) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension information from the database", err} } diff --git a/api/http/handler/extensions/extension_update.go b/api/http/handler/extensions/extension_update.go index 38487a2f7..6ff469e48 100644 --- a/api/http/handler/extensions/extension_update.go +++ b/api/http/handler/extensions/extension_update.go @@ -1,6 +1,7 @@ package extensions import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -8,6 +9,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type extensionUpdatePayload struct { @@ -16,7 +18,7 @@ type extensionUpdatePayload struct { func (payload *extensionUpdatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Version) { - return portainer.Error("Invalid extension version") + return errors.New("Invalid extension version") } return nil @@ -36,7 +38,7 @@ func (handler *Handler) extensionUpdate(w http.ResponseWriter, r *http.Request) } extension, err := handler.DataStore.Extension().Extension(extensionID) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} diff --git a/api/http/handler/extensions/extension_upload.go b/api/http/handler/extensions/extension_upload.go index 9469726be..236d445ee 100644 --- a/api/http/handler/extensions/extension_upload.go +++ b/api/http/handler/extensions/extension_upload.go @@ -1,6 +1,7 @@ package extensions import ( + "errors" "net/http" "strconv" @@ -19,13 +20,13 @@ type extensionUploadPayload struct { func (payload *extensionUploadPayload) Validate(r *http.Request) error { license, err := request.RetrieveMultiPartFormValue(r, "License", false) if err != nil { - return portainer.Error("Invalid license") + return errors.New("Invalid license") } payload.License = license fileData, fileName, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { - return portainer.Error("Invalid extension archive file. Ensure that the file is uploaded correctly") + return errors.New("Invalid extension archive file. Ensure that the file is uploaded correctly") } payload.ExtensionArchive = fileData payload.ArchiveFileName = fileName diff --git a/api/http/handler/registries/proxy.go b/api/http/handler/registries/proxy.go index 0bc996d95..d452e8e66 100644 --- a/api/http/handler/registries/proxy.go +++ b/api/http/handler/registries/proxy.go @@ -8,6 +8,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/errors" ) // request on /api/registries/:id/v2 @@ -18,7 +20,7 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt } registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} @@ -26,11 +28,11 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt err = handler.requestBouncer.RegistryAccess(r, registry) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied} } extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} diff --git a/api/http/handler/registries/proxy_management_gitlab.go b/api/http/handler/registries/proxy_management_gitlab.go index c9dcbfac0..6a8656c38 100644 --- a/api/http/handler/registries/proxy_management_gitlab.go +++ b/api/http/handler/registries/proxy_management_gitlab.go @@ -8,6 +8,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/errors" ) // request on /api/registries/{id}/proxies/gitlab @@ -18,7 +20,7 @@ func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWrit } registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} @@ -26,11 +28,11 @@ func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWrit err = handler.requestBouncer.RegistryAccess(r, registry) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied} } extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} diff --git a/api/http/handler/registries/registry_configure.go b/api/http/handler/registries/registry_configure.go index 21d2f92f8..16bc4a30f 100644 --- a/api/http/handler/registries/registry_configure.go +++ b/api/http/handler/registries/registry_configure.go @@ -1,6 +1,7 @@ package registries import ( + "errors" "net/http" "strconv" @@ -8,6 +9,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type registryConfigurePayload struct { @@ -28,7 +30,7 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error { if useAuthentication { username, err := request.RetrieveMultiPartFormValue(r, "Username", false) if err != nil { - return portainer.Error("Invalid username") + return errors.New("Invalid username") } payload.Username = username @@ -45,19 +47,19 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error { if useTLS && !skipTLSVerify { cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile") if err != nil { - return portainer.Error("Invalid certificate file. Ensure that the file is uploaded correctly") + return errors.New("Invalid certificate file. Ensure that the file is uploaded correctly") } payload.TLSCertFile = cert key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile") if err != nil { - return portainer.Error("Invalid key file. Ensure that the file is uploaded correctly") + return errors.New("Invalid key file. Ensure that the file is uploaded correctly") } payload.TLSKeyFile = key ca, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile") if err != nil { - return portainer.Error("Invalid CA certificate file. Ensure that the file is uploaded correctly") + return errors.New("Invalid CA certificate file. Ensure that the file is uploaded correctly") } payload.TLSCACertFile = ca } @@ -79,7 +81,7 @@ func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request } registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} diff --git a/api/http/handler/registries/registry_create.go b/api/http/handler/registries/registry_create.go index aac899138..fc0444e42 100644 --- a/api/http/handler/registries/registry_create.go +++ b/api/http/handler/registries/registry_create.go @@ -1,6 +1,7 @@ package registries import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -22,16 +23,16 @@ type registryCreatePayload struct { func (payload *registryCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid registry name") + return errors.New("Invalid registry name") } if govalidator.IsNull(payload.URL) { - return portainer.Error("Invalid registry URL") + return errors.New("Invalid registry URL") } if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) { - return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled") + return errors.New("Invalid credentials. Username and password must be specified when authentication is enabled") } if payload.Type != portainer.QuayRegistry && payload.Type != portainer.AzureRegistry && payload.Type != portainer.CustomRegistry && payload.Type != portainer.GitlabRegistry { - return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)") + return errors.New("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)") } return nil } diff --git a/api/http/handler/registries/registry_delete.go b/api/http/handler/registries/registry_delete.go index 62e0c663f..877ca4a39 100644 --- a/api/http/handler/registries/registry_delete.go +++ b/api/http/handler/registries/registry_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/registries/:id @@ -17,7 +18,7 @@ func (handler *Handler) registryDelete(w http.ResponseWriter, r *http.Request) * } _, err = handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} diff --git a/api/http/handler/registries/registry_inspect.go b/api/http/handler/registries/registry_inspect.go index 586f198db..715e7dcc0 100644 --- a/api/http/handler/registries/registry_inspect.go +++ b/api/http/handler/registries/registry_inspect.go @@ -3,6 +3,9 @@ package registries import ( "net/http" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/errors" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" @@ -17,7 +20,7 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) } registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} @@ -25,7 +28,7 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request) err = handler.requestBouncer.RegistryAccess(r, registry) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied} } hideFields(registry) diff --git a/api/http/handler/registries/registry_update.go b/api/http/handler/registries/registry_update.go index 1ba467199..e77dfb765 100644 --- a/api/http/handler/registries/registry_update.go +++ b/api/http/handler/registries/registry_update.go @@ -1,12 +1,14 @@ package registries import ( + "errors" "net/http" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type registryUpdatePayload struct { @@ -37,7 +39,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) * } registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} @@ -54,7 +56,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) * } for _, r := range registries { if r.ID != registry.ID && hasSameURL(&r, registry) { - return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", portainer.ErrRegistryAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "Another registry with the same URL already exists", errors.New("A registry is already defined for this URL")} } } diff --git a/api/http/handler/resourcecontrols/resourcecontrol_create.go b/api/http/handler/resourcecontrols/resourcecontrol_create.go index 5f56daa19..0371c6f8c 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_create.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_create.go @@ -21,6 +21,11 @@ type resourceControlCreatePayload struct { SubResourceIDs []string } +var ( + errResourceControlAlreadyExists = errors.New("A resource control is already applied on this resource") //http/resourceControl + errInvalidResourceControlType = errors.New("Unsupported resource control type") //http/resourceControl +) + func (payload *resourceControlCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.ResourceID) { return errors.New("invalid payload: invalid resource identifier") @@ -65,7 +70,7 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req case "config": resourceControlType = portainer.ConfigResourceControl default: - return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", portainer.ErrInvalidResourceControlType} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", errInvalidResourceControlType} } rc, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType) @@ -73,7 +78,7 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err} } if rc != nil { - return &httperror.HandlerError{http.StatusConflict, "A resource control is already associated to this resource", portainer.ErrResourceControlAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A resource control is already associated to this resource", errResourceControlAlreadyExists} } var userAccesses = make([]portainer.UserResourceAccess, 0) diff --git a/api/http/handler/resourcecontrols/resourcecontrol_delete.go b/api/http/handler/resourcecontrols/resourcecontrol_delete.go index 076e86519..394974923 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_delete.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/resource_controls/:id @@ -17,7 +18,7 @@ func (handler *Handler) resourceControlDelete(w http.ResponseWriter, r *http.Req } _, err = handler.DataStore.ResourceControl().ResourceControl(portainer.ResourceControlID(resourceControlID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err} diff --git a/api/http/handler/resourcecontrols/resourcecontrol_update.go b/api/http/handler/resourcecontrols/resourcecontrol_update.go index 9e5f52752..b200a290d 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_update.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_update.go @@ -8,6 +8,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -43,7 +45,7 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req } resourceControl, err := handler.DataStore.ResourceControl().ResourceControl(portainer.ResourceControlID(resourceControlID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err} @@ -55,7 +57,7 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req } if !security.AuthorizedResourceControlAccess(resourceControl, securityContext) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access the resource control", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access the resource control", httperrors.ErrResourceAccessDenied} } resourceControl.Public = payload.Public @@ -82,7 +84,7 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req resourceControl.TeamAccesses = teamAccesses if !security.AuthorizedResourceControlUpdate(resourceControl, securityContext) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", httperrors.ErrResourceAccessDenied} } err = handler.DataStore.ResourceControl().UpdateResourceControl(resourceControl.ID, resourceControl) diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 79047e146..d0a7f896a 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -1,6 +1,7 @@ package settings import ( + "errors" "net/http" "time" @@ -9,6 +10,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/filesystem" ) @@ -31,18 +33,18 @@ type settingsUpdatePayload struct { func (payload *settingsUpdatePayload) Validate(r *http.Request) error { if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 { - return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)") + return errors.New("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)") } if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) { - return portainer.Error("Invalid logo URL. Must correspond to a valid URL format") + return errors.New("Invalid logo URL. Must correspond to a valid URL format") } if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) { - return portainer.Error("Invalid external templates URL. Must correspond to a valid URL format") + return errors.New("Invalid external templates URL. Must correspond to a valid URL format") } if payload.UserSessionTimeout != nil { _, err := time.ParseDuration(*payload.UserSessionTimeout) if err != nil { - return portainer.Error("Invalid user session timeout") + return errors.New("Invalid user session timeout") } } @@ -169,7 +171,7 @@ func (handler *Handler) updateVolumeBrowserSetting(settings *portainer.Settings) } extension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return err } diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 051999980..b021190b8 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -31,11 +31,11 @@ type composeStackFromFileContentPayload struct { func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } payload.Name = normalizeStackName(payload.Name) if govalidator.IsNull(payload.StackFileContent) { - return portainer.Error("Invalid stack file content") + return errors.New("Invalid stack file content") } return nil } @@ -54,7 +54,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, for _, stack := range stacks { if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} } } @@ -110,14 +110,14 @@ type composeStackFromGitRepositoryPayload struct { func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } payload.Name = normalizeStackName(payload.Name) if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) { - return portainer.Error("Invalid repository URL. Must correspond to a valid URL format") + return errors.New("Invalid repository URL. Must correspond to a valid URL format") } if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { - return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled") + return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled") } if govalidator.IsNull(payload.ComposeFilePathInRepository) { payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName @@ -139,7 +139,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite for _, stack := range stacks { if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} } } @@ -201,20 +201,20 @@ type composeStackFromFileUploadPayload struct { func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) error { name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } payload.Name = normalizeStackName(name) composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { - return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly") + return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly") } payload.StackFileContent = composeFileContent var env []portainer.Pair err = request.RetrieveMultiPartFormJSONValue(r, "Env", &env, true) if err != nil { - return portainer.Error("Invalid Env parameter") + return errors.New("Invalid Env parameter") } payload.Env = env return nil @@ -234,7 +234,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, for _, stack := range stacks { if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} } } diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index cbf8eb1fe..61ca665a5 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -1,6 +1,7 @@ package stacks import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -19,10 +20,10 @@ type kubernetesStackPayload struct { func (payload *kubernetesStackPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.StackFileContent) { - return portainer.Error("Invalid stack file content") + return errors.New("Invalid stack file content") } if govalidator.IsNull(payload.Namespace) { - return portainer.Error("Invalid namespace") + return errors.New("Invalid namespace") } return nil } diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 09e743e44..e28261bed 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -24,13 +24,13 @@ type swarmStackFromFileContentPayload struct { func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } if govalidator.IsNull(payload.SwarmID) { - return portainer.Error("Invalid Swarm ID") + return errors.New("Invalid Swarm ID") } if govalidator.IsNull(payload.StackFileContent) { - return portainer.Error("Invalid stack file content") + return errors.New("Invalid stack file content") } return nil } @@ -49,7 +49,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r for _, stack := range stacks { if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} } } @@ -107,16 +107,16 @@ type swarmStackFromGitRepositoryPayload struct { func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } if govalidator.IsNull(payload.SwarmID) { - return portainer.Error("Invalid Swarm ID") + return errors.New("Invalid Swarm ID") } if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) { - return portainer.Error("Invalid repository URL. Must correspond to a valid URL format") + return errors.New("Invalid repository URL. Must correspond to a valid URL format") } if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { - return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled") + return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled") } if govalidator.IsNull(payload.ComposeFilePathInRepository) { payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName @@ -138,7 +138,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, for _, stack := range stacks { if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} } } @@ -202,26 +202,26 @@ type swarmStackFromFileUploadPayload struct { func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error { name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { - return portainer.Error("Invalid stack name") + return errors.New("Invalid stack name") } payload.Name = name swarmID, err := request.RetrieveMultiPartFormValue(r, "SwarmID", false) if err != nil { - return portainer.Error("Invalid Swarm ID") + return errors.New("Invalid Swarm ID") } payload.SwarmID = swarmID composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { - return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly") + return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly") } payload.StackFileContent = composeFileContent var env []portainer.Pair err = request.RetrieveMultiPartFormJSONValue(r, "Env", &env, true) if err != nil { - return portainer.Error("Invalid Env parameter") + return errors.New("Invalid Env parameter") } payload.Env = env return nil @@ -241,7 +241,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r for _, stack := range stacks { if strings.EqualFold(stack.Name, payload.Name) { - return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", portainer.ErrStackAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A stack with this name already exists", errStackAlreadyExists} } } diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index 5271cffb1..e560392c3 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -1,16 +1,23 @@ package stacks import ( + "errors" "net/http" "sync" "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) +var ( + errStackAlreadyExists = errors.New("A stack already exists with this name") + errStackNotExternal = errors.New("Not an external stack") +) + // Handler is the HTTP handler used to handle stack operations. type Handler struct { stackCreationMutex *sync.Mutex @@ -65,9 +72,9 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR } _, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return false, nil - } else if err != nil && err != portainer.ErrObjectNotFound { + } else if err != nil && err != bolterrors.ErrObjectNotFound { return false, err } diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index b4a3a3992..87f358abd 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -11,6 +11,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -45,7 +47,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -68,7 +70,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return handler.createComposeStack(w, r, method, endpoint, tokenData.ID) case portainer.KubernetesStack: if tokenData.Role != portainer.AdministratorRole { - return &httperror.HandlerError{http.StatusForbidden, "Access denied", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized} } return handler.createKubernetesStack(w, r, endpoint) diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index cac9b21fe..89c7ab14b 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -1,15 +1,17 @@ package stacks import ( + "errors" "net/http" "strconv" - "github.com/portainer/portainer/api/http/security" - httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" + "github.com/portainer/portainer/api/http/security" ) // DELETE request on /api/stacks/:id?external=&endpointId= @@ -37,7 +39,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt } stack, err := handler.DataStore.Stack().Stack(portainer.StackID(id)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} @@ -57,7 +59,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt } endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointIdentifier) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} @@ -78,7 +80,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err} } if !access { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied} } err = handler.deleteStack(stack, endpoint) @@ -118,7 +120,7 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit } rbacExtension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify if RBAC extension is loaded", err} } @@ -130,24 +132,24 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit if rbacExtension != nil { if !securityContext.IsAdmin && !endpointResourceAccess { - return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized} } } else { if !securityContext.IsAdmin { - return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized} } } stack, err := handler.DataStore.Stack().StackByName(stackName) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err} } if stack != nil { - return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", portainer.ErrStackNotExternal} + return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", errors.New("A tag already exists with this name")} } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index 7c601a263..e3a111668 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -8,6 +8,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -23,14 +25,14 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe } stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -56,7 +58,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err} } if !access { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied} } stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint)) diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index eb41dc793..3e78f66ec 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -7,6 +7,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -18,14 +20,14 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht } stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -51,7 +53,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err} } if !access { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied} } if resourceControl != nil { diff --git a/api/http/handler/stacks/stack_list.go b/api/http/handler/stacks/stack_list.go index 047740397..aff22b9a0 100644 --- a/api/http/handler/stacks/stack_list.go +++ b/api/http/handler/stacks/stack_list.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -45,9 +46,9 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe if !securityContext.IsAdmin { rbacExtensionEnabled := true _, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { rbacExtensionEnabled = false - } else if err != nil && err != portainer.ErrObjectNotFound { + } else if err != nil && err != errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check if RBAC extension is enabled", err} } diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index 1c0f98497..4e52c0e77 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -1,12 +1,15 @@ package stacks import ( + "errors" "net/http" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -18,7 +21,7 @@ type stackMigratePayload struct { func (payload *stackMigratePayload) Validate(r *http.Request) error { if payload.EndpointID == 0 { - return portainer.Error("Invalid endpoint identifier. Must be a positive number") + return errors.New("Invalid endpoint identifier. Must be a positive number") } return nil } @@ -37,14 +40,14 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht } stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -70,7 +73,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err} } if !access { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied} } // TODO: this is a work-around for stacks created with Portainer version >= 1.17.1 @@ -85,7 +88,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht } targetEndpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(payload.EndpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 3781f9a78..86a1e65e5 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -1,16 +1,18 @@ package stacks import ( + "errors" "net/http" "strconv" - "github.com/portainer/portainer/api/http/security" - "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" + "github.com/portainer/portainer/api/http/security" ) type updateComposeStackPayload struct { @@ -20,7 +22,7 @@ type updateComposeStackPayload struct { func (payload *updateComposeStackPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.StackFileContent) { - return portainer.Error("Invalid stack file content") + return errors.New("Invalid stack file content") } return nil } @@ -33,7 +35,7 @@ type updateSwarmStackPayload struct { func (payload *updateSwarmStackPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.StackFileContent) { - return portainer.Error("Invalid stack file content") + return errors.New("Invalid stack file content") } return nil } @@ -46,7 +48,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt } stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} @@ -64,7 +66,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt } endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} @@ -90,7 +92,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err} } if !access { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied} } updateError := handler.updateAndDeployStack(r, stack, endpoint) diff --git a/api/http/handler/tags/tag_create.go b/api/http/handler/tags/tag_create.go index d1857bcf5..5a2d1e400 100644 --- a/api/http/handler/tags/tag_create.go +++ b/api/http/handler/tags/tag_create.go @@ -1,6 +1,7 @@ package tags import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -16,7 +17,7 @@ type tagCreatePayload struct { func (payload *tagCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid tag name") + return errors.New("Invalid tag name") } return nil } @@ -36,7 +37,7 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe for _, tag := range tags { if tag.Name == payload.Name { - return &httperror.HandlerError{http.StatusConflict, "This name is already associated to a tag", portainer.ErrTagAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "This name is already associated to a tag", errors.New("A tag already exists with this name")} } } diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index 5df4f2e2b..287af24bd 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/internal/edge" ) @@ -19,7 +20,7 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe tagID := portainer.TagID(id) tag, err := handler.DataStore.Tag().Tag(tagID) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a tag with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag with the specified identifier inside the database", err} diff --git a/api/http/handler/teammemberships/teammembership_create.go b/api/http/handler/teammemberships/teammembership_create.go index 21f5cd74d..506608522 100644 --- a/api/http/handler/teammemberships/teammembership_create.go +++ b/api/http/handler/teammemberships/teammembership_create.go @@ -1,12 +1,14 @@ package teammemberships import ( + "errors" "net/http" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -18,13 +20,13 @@ type teamMembershipCreatePayload struct { func (payload *teamMembershipCreatePayload) Validate(r *http.Request) error { if payload.UserID == 0 { - return portainer.Error("Invalid UserID") + return errors.New("Invalid UserID") } if payload.TeamID == 0 { - return portainer.Error("Invalid TeamID") + return errors.New("Invalid TeamID") } if payload.Role != 1 && payload.Role != 2 { - return portainer.Error("Invalid role value. Value must be one of: 1 (leader) or 2 (member)") + return errors.New("Invalid role value. Value must be one of: 1 (leader) or 2 (member)") } return nil } @@ -43,7 +45,7 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ } if !security.AuthorizedTeamManagement(portainer.TeamID(payload.TeamID), securityContext) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to manage team memberships", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to manage team memberships", httperrors.ErrResourceAccessDenied} } memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(portainer.UserID(payload.UserID)) @@ -54,7 +56,7 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ if len(memberships) > 0 { for _, membership := range memberships { if membership.UserID == portainer.UserID(payload.UserID) && membership.TeamID == portainer.TeamID(payload.TeamID) { - return &httperror.HandlerError{http.StatusConflict, "Team membership already registered", portainer.ErrTeamMembershipAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "Team membership already registered", errors.New("Team membership already exists for this user and team")} } } } diff --git a/api/http/handler/teammemberships/teammembership_delete.go b/api/http/handler/teammemberships/teammembership_delete.go index 775ba4d1a..c12892325 100644 --- a/api/http/handler/teammemberships/teammembership_delete.go +++ b/api/http/handler/teammemberships/teammembership_delete.go @@ -7,6 +7,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -18,7 +20,7 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ } membership, err := handler.DataStore.TeamMembership().TeamMembership(portainer.TeamMembershipID(membershipID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team membership with the specified identifier inside the database", err} @@ -30,7 +32,7 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ } if !security.AuthorizedTeamManagement(membership.TeamID, securityContext) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the membership", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the membership", errors.ErrResourceAccessDenied} } err = handler.DataStore.TeamMembership().DeleteTeamMembership(portainer.TeamMembershipID(membershipID)) diff --git a/api/http/handler/teammemberships/teammembership_list.go b/api/http/handler/teammemberships/teammembership_list.go index 20e42b599..77a113776 100644 --- a/api/http/handler/teammemberships/teammembership_list.go +++ b/api/http/handler/teammemberships/teammembership_list.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -17,7 +17,7 @@ func (handler *Handler) teamMembershipList(w http.ResponseWriter, r *http.Reques } if !securityContext.IsAdmin && !securityContext.IsTeamLeader { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to list team memberships", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to list team memberships", errors.ErrResourceAccessDenied} } memberships, err := handler.DataStore.TeamMembership().TeamMemberships() diff --git a/api/http/handler/teammemberships/teammembership_update.go b/api/http/handler/teammemberships/teammembership_update.go index d98929a75..cf801a65d 100644 --- a/api/http/handler/teammemberships/teammembership_update.go +++ b/api/http/handler/teammemberships/teammembership_update.go @@ -1,12 +1,15 @@ package teammemberships import ( + "errors" "net/http" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -18,13 +21,13 @@ type teamMembershipUpdatePayload struct { func (payload *teamMembershipUpdatePayload) Validate(r *http.Request) error { if payload.UserID == 0 { - return portainer.Error("Invalid UserID") + return errors.New("Invalid UserID") } if payload.TeamID == 0 { - return portainer.Error("Invalid TeamID") + return errors.New("Invalid TeamID") } if payload.Role != 1 && payload.Role != 2 { - return portainer.Error("Invalid role value. Value must be one of: 1 (leader) or 2 (member)") + return errors.New("Invalid role value. Value must be one of: 1 (leader) or 2 (member)") } return nil } @@ -48,18 +51,18 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ } if !security.AuthorizedTeamManagement(portainer.TeamID(payload.TeamID), securityContext) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the membership", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the membership", httperrors.ErrResourceAccessDenied} } membership, err := handler.DataStore.TeamMembership().TeamMembership(portainer.TeamMembershipID(membershipID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team membership with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team membership with the specified identifier inside the database", err} } if securityContext.IsTeamLeader && membership.Role != portainer.MembershipRole(payload.Role) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the role of membership", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the role of membership", httperrors.ErrResourceAccessDenied} } membership.UserID = portainer.UserID(payload.UserID) diff --git a/api/http/handler/teams/team_create.go b/api/http/handler/teams/team_create.go index 9012b0b88..583087733 100644 --- a/api/http/handler/teams/team_create.go +++ b/api/http/handler/teams/team_create.go @@ -1,6 +1,7 @@ package teams import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -8,6 +9,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type teamCreatePayload struct { @@ -16,7 +18,7 @@ type teamCreatePayload struct { func (payload *teamCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Name) { - return portainer.Error("Invalid team name") + return errors.New("Invalid team name") } return nil } @@ -29,11 +31,11 @@ func (handler *Handler) teamCreate(w http.ResponseWriter, r *http.Request) *http } team, err := handler.DataStore.Team().TeamByName(payload.Name) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve teams from the database", err} } if team != nil { - return &httperror.HandlerError{http.StatusConflict, "A team with the same name already exists", portainer.ErrTeamAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A team with the same name already exists", errors.New("Team already exists")} } team = &portainer.Team{ diff --git a/api/http/handler/teams/team_delete.go b/api/http/handler/teams/team_delete.go index d1629347e..6d3ac534c 100644 --- a/api/http/handler/teams/team_delete.go +++ b/api/http/handler/teams/team_delete.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // DELETE request on /api/teams/:id @@ -17,7 +18,7 @@ func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *http } _, err = handler.DataStore.Team().Team(portainer.TeamID(teamID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team with the specified identifier inside the database", err} diff --git a/api/http/handler/teams/team_inspect.go b/api/http/handler/teams/team_inspect.go index f543a8f37..81d739824 100644 --- a/api/http/handler/teams/team_inspect.go +++ b/api/http/handler/teams/team_inspect.go @@ -7,6 +7,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -23,11 +25,11 @@ func (handler *Handler) teamInspect(w http.ResponseWriter, r *http.Request) *htt } if !security.AuthorizedTeamManagement(portainer.TeamID(teamID), securityContext) { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to team", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to team", errors.ErrResourceAccessDenied} } team, err := handler.DataStore.Team().Team(portainer.TeamID(teamID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team with the specified identifier inside the database", err} diff --git a/api/http/handler/teams/team_memberships.go b/api/http/handler/teams/team_memberships.go index 1d61e1a01..75c6f1389 100644 --- a/api/http/handler/teams/team_memberships.go +++ b/api/http/handler/teams/team_memberships.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -23,7 +24,7 @@ func (handler *Handler) teamMemberships(w http.ResponseWriter, r *http.Request) } if !security.AuthorizedTeamManagement(portainer.TeamID(teamID), securityContext) { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to team", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to team", errors.ErrResourceAccessDenied} } memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByTeamID(portainer.TeamID(teamID)) diff --git a/api/http/handler/teams/team_update.go b/api/http/handler/teams/team_update.go index 06e1ef9d0..eba25b11d 100644 --- a/api/http/handler/teams/team_update.go +++ b/api/http/handler/teams/team_update.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) type teamUpdatePayload struct { @@ -31,7 +32,7 @@ func (handler *Handler) teamUpdate(w http.ResponseWriter, r *http.Request) *http } team, err := handler.DataStore.Team().Team(portainer.TeamID(teamID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a team with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a team with the specified identifier inside the database", err} diff --git a/api/http/handler/upload/upload_tls.go b/api/http/handler/upload/upload_tls.go index 383f21fac..aa16544fb 100644 --- a/api/http/handler/upload/upload_tls.go +++ b/api/http/handler/upload/upload_tls.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/filesystem" ) // POST request on /api/upload/tls/{certificate:(?:ca|cert|key)}?folder= @@ -35,7 +36,7 @@ func (handler *Handler) uploadTLS(w http.ResponseWriter, r *http.Request) *httpe case "key": fileType = portainer.TLSFileKey default: - return &httperror.HandlerError{http.StatusBadRequest, "Invalid certificate route value. Value must be one of: ca, cert or key", portainer.ErrUndefinedTLSFileType} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid certificate route value. Value must be one of: ca, cert or key", filesystem.ErrUndefinedTLSFileType} } _, err = handler.FileService.StoreTLSFileFromBytes(folder, fileType, file) diff --git a/api/http/handler/users/admin_check.go b/api/http/handler/users/admin_check.go index 7c1a54f33..0bf503f7c 100644 --- a/api/http/handler/users/admin_check.go +++ b/api/http/handler/users/admin_check.go @@ -6,6 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // GET request on /api/users/admin/check @@ -16,7 +17,7 @@ func (handler *Handler) adminCheck(w http.ResponseWriter, r *http.Request) *http } if len(users) == 0 { - return &httperror.HandlerError{http.StatusNotFound, "No administrator account found inside the database", portainer.ErrObjectNotFound} + return &httperror.HandlerError{http.StatusNotFound, "No administrator account found inside the database", errors.ErrObjectNotFound} } return response.Empty(w) diff --git a/api/http/handler/users/admin_init.go b/api/http/handler/users/admin_init.go index b76299199..d15ed7d7a 100644 --- a/api/http/handler/users/admin_init.go +++ b/api/http/handler/users/admin_init.go @@ -1,6 +1,7 @@ package users import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -18,10 +19,10 @@ type adminInitPayload struct { func (payload *adminInitPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Username) || govalidator.Contains(payload.Username, " ") { - return portainer.Error("Invalid username. Must not contain any whitespace") + return errors.New("Invalid username. Must not contain any whitespace") } if govalidator.IsNull(payload.Password) { - return portainer.Error("Invalid password") + return errors.New("Invalid password") } return nil } @@ -40,7 +41,7 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe } if len(users) != 0 { - return &httperror.HandlerError{http.StatusConflict, "Unable to create administrator user", portainer.ErrAdminAlreadyInitialized} + return &httperror.HandlerError{http.StatusConflict, "Unable to create administrator user", errAdminAlreadyInitialized} } user := &portainer.User{ @@ -51,7 +52,7 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe user.Password, err = handler.CryptoService.Hash(payload.Password) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", errCryptoHashFailure} } err = handler.DataStore.User().CreateUser(user) diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index 3413d4bb9..8c4da1c4a 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -1,6 +1,8 @@ package users import ( + "errors" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" @@ -11,6 +13,14 @@ import ( "github.com/gorilla/mux" ) +var ( + errUserAlreadyExists = errors.New("User already exists") + errAdminAlreadyInitialized = errors.New("An administrator user already exists") + errAdminCannotRemoveSelf = errors.New("Cannot remove your own user account. Contact another administrator") + errCannotRemoveLastLocalAdmin = errors.New("Cannot remove the last local administrator account") + errCryptoHashFailure = errors.New("Unable to hash data") +) + func hideFields(user *portainer.User) { user.Password = "" } diff --git a/api/http/handler/users/user_create.go b/api/http/handler/users/user_create.go index d39d1a9a7..36f9d9039 100644 --- a/api/http/handler/users/user_create.go +++ b/api/http/handler/users/user_create.go @@ -1,6 +1,7 @@ package users import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -8,6 +9,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -20,11 +23,11 @@ type userCreatePayload struct { func (payload *userCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Username) || govalidator.Contains(payload.Username, " ") { - return portainer.Error("Invalid username. Must not contain any whitespace") + return errors.New("Invalid username. Must not contain any whitespace") } if payload.Role != 1 && payload.Role != 2 { - return portainer.Error("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)") + return errors.New("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)") } return nil } @@ -43,19 +46,19 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http } if !securityContext.IsAdmin && !securityContext.IsTeamLeader { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create user", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create user", httperrors.ErrResourceAccessDenied} } if securityContext.IsTeamLeader && payload.Role == 1 { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create administrator user", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create administrator user", httperrors.ErrResourceAccessDenied} } user, err := handler.DataStore.User().UserByUsername(payload.Username) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err} } if user != nil { - return &httperror.HandlerError{http.StatusConflict, "Another user with the same username already exists", portainer.ErrUserAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "Another user with the same username already exists", errUserAlreadyExists} } user = &portainer.User{ @@ -72,7 +75,7 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http if settings.AuthenticationMethod == portainer.AuthenticationInternal { user.Password, err = handler.CryptoService.Hash(payload.Password) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", errCryptoHashFailure} } } diff --git a/api/http/handler/users/user_delete.go b/api/http/handler/users/user_delete.go index e192cd055..12a1be2c0 100644 --- a/api/http/handler/users/user_delete.go +++ b/api/http/handler/users/user_delete.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/security" ) @@ -28,11 +29,11 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http } if tokenData.ID == portainer.UserID(userID) { - return &httperror.HandlerError{http.StatusForbidden, "Cannot remove your own user account. Contact another administrator", portainer.ErrAdminCannotRemoveSelf} + return &httperror.HandlerError{http.StatusForbidden, "Cannot remove your own user account. Contact another administrator", errAdminCannotRemoveSelf} } user, err := handler.DataStore.User().User(portainer.UserID(userID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err} @@ -63,7 +64,7 @@ func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.U } if localAdminCount < 2 { - return &httperror.HandlerError{http.StatusInternalServerError, "Cannot remove local administrator user", portainer.ErrCannotRemoveLastLocalAdmin} + return &httperror.HandlerError{http.StatusInternalServerError, "Cannot remove local administrator user", errCannotRemoveLastLocalAdmin} } return handler.deleteUser(w, user) diff --git a/api/http/handler/users/user_inspect.go b/api/http/handler/users/user_inspect.go index b35d67d3a..cfd87efd0 100644 --- a/api/http/handler/users/user_inspect.go +++ b/api/http/handler/users/user_inspect.go @@ -3,12 +3,13 @@ package users import ( "net/http" - "github.com/portainer/portainer/api/http/security" - httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/errors" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/users/:id @@ -24,11 +25,11 @@ func (handler *Handler) userInspect(w http.ResponseWriter, r *http.Request) *htt } if !securityContext.IsAdmin && securityContext.UserID != portainer.UserID(userID) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied inspect user", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied inspect user", errors.ErrResourceAccessDenied} } user, err := handler.DataStore.User().User(portainer.UserID(userID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err} diff --git a/api/http/handler/users/user_memberships.go b/api/http/handler/users/user_memberships.go index 3d4655bbb..283b6ee25 100644 --- a/api/http/handler/users/user_memberships.go +++ b/api/http/handler/users/user_memberships.go @@ -7,6 +7,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -23,7 +24,7 @@ func (handler *Handler) userMemberships(w http.ResponseWriter, r *http.Request) } if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user memberships", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user memberships", errors.ErrUnauthorized} } memberships, err := handler.DataStore.TeamMembership().TeamMembershipsByUserID(portainer.UserID(userID)) diff --git a/api/http/handler/users/user_update.go b/api/http/handler/users/user_update.go index 55e2f4e59..dd1f57519 100644 --- a/api/http/handler/users/user_update.go +++ b/api/http/handler/users/user_update.go @@ -1,6 +1,7 @@ package users import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -8,6 +9,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -19,11 +22,11 @@ type userUpdatePayload struct { func (payload *userUpdatePayload) Validate(r *http.Request) error { if govalidator.Contains(payload.Username, " ") { - return portainer.Error("Invalid username. Must not contain any whitespace") + return errors.New("Invalid username. Must not contain any whitespace") } if payload.Role != 0 && payload.Role != 1 && payload.Role != 2 { - return portainer.Error("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)") + return errors.New("Invalid role value. Value must be one of: 1 (administrator) or 2 (regular user)") } return nil } @@ -41,7 +44,7 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http } if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user", httperrors.ErrUnauthorized} } var payload userUpdatePayload @@ -51,11 +54,11 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http } if tokenData.Role != portainer.AdministratorRole && payload.Role != 0 { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user to administrator role", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user to administrator role", httperrors.ErrResourceAccessDenied} } user, err := handler.DataStore.User().User(portainer.UserID(userID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err} @@ -63,11 +66,11 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http if payload.Username != "" && payload.Username != user.Username { sameNameUser, err := handler.DataStore.User().UserByUsername(payload.Username) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err} } if sameNameUser != nil && sameNameUser.ID != portainer.UserID(userID) { - return &httperror.HandlerError{http.StatusConflict, "Another user with the same username already exists", portainer.ErrUserAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "Another user with the same username already exists", errUserAlreadyExists} } user.Username = payload.Username @@ -76,7 +79,7 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http if payload.Password != "" { user.Password, err = handler.CryptoService.Hash(payload.Password) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", errCryptoHashFailure} } } diff --git a/api/http/handler/users/user_update_password.go b/api/http/handler/users/user_update_password.go index 8905a0462..c0556dfd1 100644 --- a/api/http/handler/users/user_update_password.go +++ b/api/http/handler/users/user_update_password.go @@ -1,6 +1,7 @@ package users import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -8,6 +9,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -18,10 +21,10 @@ type userUpdatePasswordPayload struct { func (payload *userUpdatePasswordPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Password) { - return portainer.Error("Invalid current password") + return errors.New("Invalid current password") } if govalidator.IsNull(payload.NewPassword) { - return portainer.Error("Invalid new password") + return errors.New("Invalid new password") } return nil } @@ -39,7 +42,7 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques } if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user", httperrors.ErrUnauthorized} } var payload userUpdatePasswordPayload @@ -49,7 +52,7 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques } user, err := handler.DataStore.User().User(portainer.UserID(userID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a user with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err} @@ -57,12 +60,12 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Specified password do not match actual password", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusForbidden, "Specified password do not match actual password", httperrors.ErrUnauthorized} } user.Password, err = handler.CryptoService.Hash(payload.NewPassword) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", errCryptoHashFailure} } err = handler.DataStore.User().UpdateUser(user.ID, user) diff --git a/api/http/handler/webhooks/webhook_create.go b/api/http/handler/webhooks/webhook_create.go index 5b960ec2d..dc00dcd38 100644 --- a/api/http/handler/webhooks/webhook_create.go +++ b/api/http/handler/webhooks/webhook_create.go @@ -1,6 +1,7 @@ package webhooks import ( + "errors" "net/http" "github.com/asaskevich/govalidator" @@ -9,6 +10,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type webhookCreatePayload struct { @@ -19,13 +21,13 @@ type webhookCreatePayload struct { func (payload *webhookCreatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.ResourceID) { - return portainer.Error("Invalid ResourceID") + return errors.New("Invalid ResourceID") } if payload.EndpointID == 0 { - return portainer.Error("Invalid EndpointID") + return errors.New("Invalid EndpointID") } if payload.WebhookType != 1 { - return portainer.Error("Invalid WebhookType") + return errors.New("Invalid WebhookType") } return nil } @@ -38,11 +40,11 @@ func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *h } webhook, err := handler.DataStore.Webhook().WebhookByResourceID(payload.ResourceID) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "An error occurred retrieving webhooks from the database", err} } if webhook != nil { - return &httperror.HandlerError{http.StatusConflict, "A webhook for this resource already exists", portainer.ErrWebhookAlreadyExists} + return &httperror.HandlerError{http.StatusConflict, "A webhook for this resource already exists", errors.New("A webhook for this resource already exists")} } token, err := uuid.NewV4() diff --git a/api/http/handler/webhooks/webhook_execute.go b/api/http/handler/webhooks/webhook_execute.go index 11e62fd99..e07ae8b2e 100644 --- a/api/http/handler/webhooks/webhook_execute.go +++ b/api/http/handler/webhooks/webhook_execute.go @@ -2,6 +2,7 @@ package webhooks import ( "context" + "errors" "net/http" "strings" @@ -10,6 +11,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) // Acts on a passed in token UUID to restart the docker service @@ -23,7 +25,7 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) * webhook, err := handler.DataStore.Webhook().WebhookByToken(webhookToken) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a webhook with this token", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve webhook from the database", err} @@ -34,7 +36,7 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) * webhookType := webhook.WebhookType endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -46,7 +48,7 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) * case portainer.ServiceWebhook: return handler.executeServiceWebhook(w, endpoint, resourceID, imageTag) default: - return &httperror.HandlerError{http.StatusInternalServerError, "Unsupported webhook type", portainer.ErrUnsupportedWebhookType} + return &httperror.HandlerError{http.StatusInternalServerError, "Unsupported webhook type", errors.New("Webhooks for this resource are not currently supported")} } } diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index dee223853..6da16659a 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -11,6 +11,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/errors" ) // websocketAttach handles GET requests on /websocket/attach?id=&endpointId=&nodeName=&token= @@ -33,7 +34,7 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request) } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index eb59956ff..0abbcb263 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -3,6 +3,7 @@ package websocket import ( "bytes" "encoding/json" + "github.com/portainer/portainer/api/bolt/errors" "net" "net/http" "net/http/httputil" @@ -40,7 +41,7 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == errors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} diff --git a/api/http/handler/websocket/pod.go b/api/http/handler/websocket/pod.go index 46f7f1dfd..103b2b60f 100644 --- a/api/http/handler/websocket/pod.go +++ b/api/http/handler/websocket/pod.go @@ -10,6 +10,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" portainer "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) // websocketPodExec handles GET requests on /websocket/pod?token=&endpointId=&namespace=&podName=&containerName=&command= @@ -49,7 +50,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request) } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index cf9b64a86..5be06eb0b 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/client" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" @@ -402,7 +403,7 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r if tokenData.Role != portainer.AdministratorRole { rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension) - if err != nil && err != portainer.ErrObjectNotFound { + if err != nil && err != bolterrors.ErrObjectNotFound { return nil, err } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 97abab731..ca8c165cb 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -2,12 +2,13 @@ package security import ( "errors" + "net/http" + "strings" httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer/api" - - "net/http" - "strings" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" ) type ( @@ -110,13 +111,13 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp } if !authorizedEndpointAccess(endpoint, group, tokenData.ID, memberships) { - return portainer.ErrEndpointAccessDenied + return httperrors.ErrEndpointAccessDenied } if authorizationCheck { err = bouncer.checkEndpointOperationAuthorization(r, endpoint) if err != nil { - return portainer.ErrAuthorizationRequired + return ErrAuthorizationRequired } } @@ -152,7 +153,7 @@ func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Reque } extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return nil } else if err != nil { return err @@ -192,7 +193,7 @@ func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portain } if !AuthorizedRegistryAccess(registry, tokenData.ID, memberships) { - return portainer.ErrEndpointAccessDenied + return httperrors.ErrEndpointAccessDenied } return nil @@ -213,7 +214,7 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenData, err := RetrieveTokenData(r) if err != nil { - httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrUnauthorized) + httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized) return } @@ -223,9 +224,9 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, } extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { if administratorOnly { - httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrUnauthorized) + httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized) return } @@ -237,8 +238,8 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, } user, err := bouncer.dataStore.User().User(tokenData.ID) - if err != nil && err == portainer.ErrObjectNotFound { - httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) + if err != nil && err == bolterrors.ErrObjectNotFound { + httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized) return } else if err != nil { httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user details from the database", err) @@ -254,7 +255,7 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey) err = bouncer.rbacExtensionClient.checkAuthorization(apiOperation) if err != nil { - httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrAuthorizationRequired) + httperror.WriteError(w, http.StatusForbidden, "Access denied", ErrAuthorizationRequired) return } @@ -268,7 +269,7 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenData, err := RetrieveTokenData(r) if err != nil { - httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied) + httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrResourceAccessDenied) return } @@ -301,7 +302,7 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han } if token == "" { - httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) + httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized) return } @@ -313,8 +314,8 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han } _, err = bouncer.dataStore.User().User(tokenData.ID) - if err != nil && err == portainer.ErrObjectNotFound { - httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", portainer.ErrUnauthorized) + if err != nil && err == bolterrors.ErrObjectNotFound { + httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized) return } else if err != nil { httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user details from the database", err) diff --git a/api/http/security/context.go b/api/http/security/context.go index 58daaa9e3..8350fa56f 100644 --- a/api/http/security/context.go +++ b/api/http/security/context.go @@ -2,6 +2,7 @@ package security import ( "context" + "errors" "net/http" "github.com/portainer/portainer/api" @@ -25,7 +26,7 @@ func storeTokenData(request *http.Request, tokenData *portainer.TokenData) conte func RetrieveTokenData(request *http.Request) (*portainer.TokenData, error) { contextData := request.Context().Value(contextAuthenticationKey) if contextData == nil { - return nil, portainer.ErrMissingContextData + return nil, errors.New("Unable to find JWT data in request context") } tokenData := contextData.(*portainer.TokenData) @@ -42,7 +43,7 @@ func storeRestrictedRequestContext(request *http.Request, requestContext *Restri func RetrieveRestrictedRequestContext(request *http.Request) (*RestrictedRequestContext, error) { contextData := request.Context().Value(contextRestrictedRequest) if contextData == nil { - return nil, portainer.ErrMissingSecurityContext + return nil, errors.New("Unable to find security details in request context") } requestContext := contextData.(*RestrictedRequestContext) diff --git a/api/http/security/errors.go b/api/http/security/errors.go new file mode 100644 index 000000000..40193b38c --- /dev/null +++ b/api/http/security/errors.go @@ -0,0 +1,7 @@ +package security + +import "errors" + +var ( + ErrAuthorizationRequired = errors.New("Authorization required for this operation") +) diff --git a/api/http/security/rate_limiter.go b/api/http/security/rate_limiter.go index e8cc7ae5c..65eea5d4a 100644 --- a/api/http/security/rate_limiter.go +++ b/api/http/security/rate_limiter.go @@ -7,7 +7,7 @@ import ( "github.com/g07cha/defender" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/errors" ) // RateLimiter represents an entity that manages request rate limiting @@ -30,7 +30,7 @@ func (limiter *RateLimiter) LimitAccess(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := StripAddrPort(r.RemoteAddr) if banned := limiter.Inc(ip); banned == true { - httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied) + httperror.WriteError(w, http.StatusForbidden, "Access denied", errors.ErrResourceAccessDenied) return } next.ServeHTTP(w, r) diff --git a/api/http/security/rbac.go b/api/http/security/rbac.go index 08366cb72..f4281331b 100644 --- a/api/http/security/rbac.go +++ b/api/http/security/rbac.go @@ -52,7 +52,7 @@ func (client *rbacExtensionClient) checkAuthorization(authRequest *portainer.API defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { - return portainer.ErrAuthorizationRequired + return ErrAuthorizationRequired } return nil diff --git a/api/jwt/jwt.go b/api/jwt/jwt.go index aedb264de..4bd9f8fec 100644 --- a/api/jwt/jwt.go +++ b/api/jwt/jwt.go @@ -1,6 +1,8 @@ package jwt import ( + "errors" + "github.com/portainer/portainer/api" "fmt" @@ -23,6 +25,11 @@ type claims struct { jwt.StandardClaims } +var ( + errSecretGeneration = errors.New("Unable to generate secret key") + errInvalidJWTToken = errors.New("Invalid JWT token") +) + // NewService initializes a new service. It will generate a random key that will be used to sign JWT tokens. func NewService(userSessionDuration string) (*Service, error) { userSessionTimeout, err := time.ParseDuration(userSessionDuration) @@ -32,7 +39,7 @@ func NewService(userSessionDuration string) (*Service, error) { secret := securecookie.GenerateRandomKey(32) if secret == nil { - return nil, portainer.ErrSecretGeneration + return nil, errSecretGeneration } service := &Service{ @@ -83,7 +90,7 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, } } - return nil, portainer.ErrInvalidJWTToken + return nil, errInvalidJWTToken } // SetUserSessionDuration sets the user session duration diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go index c35f3a422..89c08ec61 100644 --- a/api/ldap/ldap.go +++ b/api/ldap/ldap.go @@ -1,18 +1,20 @@ package ldap import ( + "errors" "fmt" "strings" ldap "github.com/go-ldap/ldap/v3" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/crypto" + httperrors "github.com/portainer/portainer/api/http/errors" ) -const ( - // ErrUserNotFound defines an error raised when the user is not found via LDAP search +var ( + // errUserNotFound defines an error raised when the user is not found via LDAP search // or that too many entries (> 1) are returned. - ErrUserNotFound = portainer.Error("User not found or too many entries returned") + errUserNotFound = errors.New("User not found or too many entries returned") ) // Service represents a service used to authenticate users against a LDAP/AD. @@ -47,7 +49,7 @@ func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearc } if !found { - return "", ErrUserNotFound + return "", errUserNotFound } return userDN, nil @@ -105,7 +107,7 @@ func (*Service) AuthenticateUser(username, password string, settings *portainer. err = connection.Bind(userDN, password) if err != nil { - return portainer.ErrUnauthorized + return httperrors.ErrUnauthorized } return nil From 08095913a68f0915e5ef2f147d417c7496a58a41 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 8 Jul 2020 12:25:37 +1200 Subject: [PATCH 049/195] fix(api): fix issues with old error declaration --- .../customtemplates/customtemplate_create.go | 32 +++++++++---------- .../customtemplates/customtemplate_delete.go | 6 ++-- .../customtemplates/customtemplate_file.go | 3 +- .../customtemplates/customtemplate_inspect.go | 6 ++-- .../customtemplates/customtemplate_update.go | 18 +++++++---- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/api/http/handler/customtemplates/customtemplate_create.go b/api/http/handler/customtemplates/customtemplate_create.go index 3bb8fef0b..af1a81e48 100644 --- a/api/http/handler/customtemplates/customtemplate_create.go +++ b/api/http/handler/customtemplates/customtemplate_create.go @@ -85,19 +85,19 @@ type customTemplateFromFileContentPayload struct { func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Title) { - return portainer.Error("Invalid custom template title") + return errors.New("Invalid custom template title") } if govalidator.IsNull(payload.Description) { - return portainer.Error("Invalid custom template description") + return errors.New("Invalid custom template description") } if govalidator.IsNull(payload.FileContent) { - return portainer.Error("Invalid file content") + return errors.New("Invalid file content") } if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows { - return portainer.Error("Invalid custom template platform") + return errors.New("Invalid custom template platform") } if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack { - return portainer.Error("Invalid custom template type") + return errors.New("Invalid custom template type") } return nil } @@ -148,25 +148,25 @@ type customTemplateFromGitRepositoryPayload struct { func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Title) { - return portainer.Error("Invalid custom template title") + return errors.New("Invalid custom template title") } if govalidator.IsNull(payload.Description) { - return portainer.Error("Invalid custom template description") + return errors.New("Invalid custom template description") } if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) { - return portainer.Error("Invalid repository URL. Must correspond to a valid URL format") + return errors.New("Invalid repository URL. Must correspond to a valid URL format") } if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) { - return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled") + return errors.New("Invalid repository credentials. Username and password must be specified when authentication is enabled") } if govalidator.IsNull(payload.ComposeFilePathInRepository) { payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName } if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows { - return portainer.Error("Invalid custom template platform") + return errors.New("Invalid custom template platform") } if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack { - return portainer.Error("Invalid custom template type") + return errors.New("Invalid custom template type") } return nil } @@ -223,13 +223,13 @@ type customTemplateFromFileUploadPayload struct { func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) error { title, err := request.RetrieveMultiPartFormValue(r, "Title", false) if err != nil { - return portainer.Error("Invalid custom template title") + return errors.New("Invalid custom template title") } payload.Title = title description, err := request.RetrieveMultiPartFormValue(r, "Description", false) if err != nil { - return portainer.Error("Invalid custom template description") + return errors.New("Invalid custom template description") } payload.Description = description @@ -240,20 +240,20 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er platform, _ := request.RetrieveNumericMultiPartFormValue(r, "Platform", true) templatePlatform := portainer.CustomTemplatePlatform(platform) if templatePlatform != portainer.CustomTemplatePlatformLinux && templatePlatform != portainer.CustomTemplatePlatformWindows { - return portainer.Error("Invalid custom template platform") + return errors.New("Invalid custom template platform") } payload.Platform = templatePlatform typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true) templateType := portainer.StackType(typeNumeral) if templateType != portainer.DockerComposeStack && templateType != portainer.DockerSwarmStack { - return portainer.Error("Invalid custom template type") + return errors.New("Invalid custom template type") } payload.Type = templateType composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { - return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly") + return errors.New("Invalid Compose file. Ensure that the Compose file is uploaded correctly") } payload.FileContent = composeFileContent diff --git a/api/http/handler/customtemplates/customtemplate_delete.go b/api/http/handler/customtemplates/customtemplate_delete.go index f81c41992..24500894a 100644 --- a/api/http/handler/customtemplates/customtemplate_delete.go +++ b/api/http/handler/customtemplates/customtemplate_delete.go @@ -8,6 +8,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -23,7 +25,7 @@ func (handler *Handler) customTemplateDelete(w http.ResponseWriter, r *http.Requ } customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err} @@ -36,7 +38,7 @@ func (handler *Handler) customTemplateDelete(w http.ResponseWriter, r *http.Requ access := userCanEditTemplate(customTemplate, securityContext) if !access { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied} } err = handler.DataStore.CustomTemplate().DeleteCustomTemplate(portainer.CustomTemplateID(customTemplateID)) diff --git a/api/http/handler/customtemplates/customtemplate_file.go b/api/http/handler/customtemplates/customtemplate_file.go index 5b3689fa1..5fa74d813 100644 --- a/api/http/handler/customtemplates/customtemplate_file.go +++ b/api/http/handler/customtemplates/customtemplate_file.go @@ -8,6 +8,7 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type fileResponse struct { @@ -22,7 +23,7 @@ func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Reques } customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err} diff --git a/api/http/handler/customtemplates/customtemplate_inspect.go b/api/http/handler/customtemplates/customtemplate_inspect.go index fd77be2b0..6259e3eb7 100644 --- a/api/http/handler/customtemplates/customtemplate_inspect.go +++ b/api/http/handler/customtemplates/customtemplate_inspect.go @@ -8,6 +8,8 @@ import ( "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -18,7 +20,7 @@ func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Req } customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err} @@ -36,7 +38,7 @@ func (handler *Handler) customTemplateInspect(w http.ResponseWriter, r *http.Req access := userCanEditTemplate(customTemplate, securityContext) if !access { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied} } if resourceControl != nil { diff --git a/api/http/handler/customtemplates/customtemplate_update.go b/api/http/handler/customtemplates/customtemplate_update.go index b969b4fe2..51939833f 100644 --- a/api/http/handler/customtemplates/customtemplate_update.go +++ b/api/http/handler/customtemplates/customtemplate_update.go @@ -1,14 +1,18 @@ package customtemplates import ( + "errors" "net/http" "strconv" + bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" + httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" ) @@ -24,19 +28,19 @@ type customTemplateUpdatePayload struct { func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.Title) { - return portainer.Error("Invalid custom template title") + return errors.New("Invalid custom template title") } if govalidator.IsNull(payload.FileContent) { - return portainer.Error("Invalid file content") + return errors.New("Invalid file content") } if payload.Platform != portainer.CustomTemplatePlatformLinux && payload.Platform != portainer.CustomTemplatePlatformWindows { - return portainer.Error("Invalid custom template platform") + return errors.New("Invalid custom template platform") } if payload.Type != portainer.DockerComposeStack && payload.Type != portainer.DockerSwarmStack { - return portainer.Error("Invalid custom template type") + return errors.New("Invalid custom template type") } if govalidator.IsNull(payload.Description) { - return portainer.Error("Invalid custom template description") + return errors.New("Invalid custom template description") } return nil } @@ -54,7 +58,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ } customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID)) - if err == portainer.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err} @@ -67,7 +71,7 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ access := userCanEditTemplate(customTemplate, securityContext) if !access { - return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied} } templateFolder := strconv.Itoa(customTemplateID) From c778ef64049d75153bb0518a47a89288ddff7cb3 Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Wed, 8 Jul 2020 03:35:52 +0200 Subject: [PATCH 050/195] feat(networks): Support multiple excluded IPs for MACVLAN networks (#3962) * feat(networks): Support multiple excluded IPs for MACVLAN networks * feat(networks): add a generated name * feat(networks): prevent create macvlan network where exclude ip is the same as gateway * feat(networks): remove auxaddresses validation on submit * feat(networks): check exclude ip validation on change * feat(networks): check form validation on change * feat(networks): clean checkAuxiliaryAddress function --- .../create/createNetworkController.js | 67 +++++++++++++++++-- .../views/networks/create/createnetwork.html | 50 ++++++++++++-- app/docker/views/networks/edit/network.html | 8 ++- 3 files changed, 112 insertions(+), 13 deletions(-) diff --git a/app/docker/views/networks/create/createNetworkController.js b/app/docker/views/networks/create/createNetworkController.js index 8fe56ede9..134d4fff9 100644 --- a/app/docker/views/networks/create/createNetworkController.js +++ b/app/docker/views/networks/create/createNetworkController.js @@ -21,13 +21,13 @@ angular.module('portainer.docker').controller('CreateNetworkController', [ Subnet: '', Gateway: '', IPRange: '', - AuxAddress: '', + AuxiliaryAddresses: [], }, IPV6: { Subnet: '', Gateway: '', IPRange: '', - AuxAddress: '', + AuxiliaryAddresses: [], }, Labels: [], AccessControlData: new AccessControlFormData(), @@ -79,6 +79,59 @@ angular.module('portainer.docker').controller('CreateNetworkController', [ $scope.formValues.Labels.splice(index, 1); }; + $scope.addIPV4AuxAddress = function () { + $scope.formValues.IPV4.AuxiliaryAddresses.push(''); + }; + + $scope.addIPV6AuxAddress = function () { + $scope.formValues.IPV6.AuxiliaryAddresses.push(''); + }; + + $scope.removeIPV4AuxAddress = function (index) { + $scope.formValues.IPV4.AuxiliaryAddresses.splice(index, 1); + $scope.state.IPV4AuxiliaryAddressesError.splice(index, 1); + }; + + $scope.removeIPV6AuxAddress = function (index) { + $scope.formValues.IPV6.AuxiliaryAddresses.splice(index, 1); + $scope.state.IPV6AuxiliaryAddressesError.splice(index, 1); + }; + + function checkAuxiliaryAddress(excludedIP, gateway) { + const split = _.split(excludedIP, '='); + + if (split.length === 2) { + return split[1] === gateway; + } + return excludedIP === gateway; + } + + $scope.checkIPV4AuxiliaryAddress = function (index) { + $scope.state.IPV4AuxiliaryAddressesError[index] = checkAuxiliaryAddress($scope.formValues.IPV4.AuxiliaryAddresses[index], $scope.formValues.IPV4.Gateway); + }; + + $scope.checkIPV6AuxiliaryAddress = function (index) { + $scope.state.IPV6AuxiliaryAddressesError[index] = checkAuxiliaryAddress($scope.formValues.IPV6.AuxiliaryAddresses[index], $scope.formValues.IPV6.Gateway); + }; + + $scope.isValid = function () { + const validIPV4 = !_.reduce($scope.state.IPV4AuxiliaryAddressesError, (acc, item) => acc || item, false); + const validIPV6 = !_.reduce($scope.state.IPV6AuxiliaryAddressesError, (acc, item) => acc || item, false); + return validIPV4 && validIPV6; + }; + + function prepareAuxiliaryAddresses(ipamConfig, ipFormValues) { + ipamConfig.AuxiliaryAddresses = {}; + _.forEach(ipFormValues.AuxiliaryAddresses, (auxAddress, index) => { + const split = _.split(auxAddress, '='); + if (split.length === 2) { + ipamConfig.AuxiliaryAddresses[split[0]] = split[1]; + } else { + ipamConfig.AuxiliaryAddresses['device' + index] = auxAddress; + } + }); + } + function prepareIPAMConfiguration(config) { if ($scope.formValues.IPV4.Subnet) { let ipamConfig = {}; @@ -89,8 +142,8 @@ angular.module('portainer.docker').controller('CreateNetworkController', [ if ($scope.formValues.IPV4.IPRange) { ipamConfig.IPRange = $scope.formValues.IPV4.IPRange; } - if ($scope.formValues.IPV4.AuxAddress) { - ipamConfig.AuxAddress = $scope.formValues.IPV4.AuxAddress; + if ($scope.formValues.IPV4.AuxiliaryAddresses.length) { + prepareAuxiliaryAddresses(ipamConfig, $scope.formValues.IPV4); } config.IPAM.Config.push(ipamConfig); } @@ -103,8 +156,8 @@ angular.module('portainer.docker').controller('CreateNetworkController', [ if ($scope.formValues.IPV6.IPRange) { ipamConfig.IPRange = $scope.formValues.IPV6.IPRange; } - if ($scope.formValues.IPV6.AuxAddress) { - ipamConfig.AuxAddress = $scope.formValues.IPV6.AuxAddress; + if ($scope.formValues.IPV6.AuxiliaryAddresses.length) { + prepareAuxiliaryAddresses(ipamConfig, $scope.formValues.IPV6); } config.EnableIPv6 = true; config.IPAM.Config.push(ipamConfig); @@ -245,6 +298,8 @@ angular.module('portainer.docker').controller('CreateNetworkController', [ function initView() { var apiVersion = $scope.applicationState.endpoint.apiVersion; + $scope.state.IPV4AuxiliaryAddressesError = []; + $scope.state.IPV6AuxiliaryAddressesError = []; PluginService.networkPlugins(apiVersion < 1.25) .then(function success(data) { diff --git a/app/docker/views/networks/create/createnetwork.html b/app/docker/views/networks/create/createnetwork.html index 7cfc402c8..56a3f06bf 100644 --- a/app/docker/views/networks/create/createnetwork.html +++ b/app/docker/views/networks/create/createnetwork.html @@ -88,12 +88,32 @@
- +
+
+
- + +
+ +
+

Exclude ip cannot be the same as gateway.

+
+ + add excluded IP + +
@@ -117,12 +137,32 @@
- +
+
+
- + +
+ +
+

Exclude ip cannot be the same as gateway.

+
+ + add excluded IP + +
Advanced configuration @@ -205,7 +245,7 @@
diff --git a/app/kubernetes/views/configurations/edit/configurationController.js b/app/kubernetes/views/configurations/edit/configurationController.js index 3906d9f59..59427d11d 100644 --- a/app/kubernetes/views/configurations/edit/configurationController.js +++ b/app/kubernetes/views/configurations/edit/configurationController.js @@ -10,6 +10,7 @@ class KubernetesConfigurationController { constructor( $async, $state, + clipboard, Notifications, LocalStorage, KubernetesConfigurationService, @@ -21,6 +22,7 @@ class KubernetesConfigurationController { ) { this.$async = $async; this.$state = $state; + this.clipboard = clipboard; this.Notifications = Notifications; this.LocalStorage = LocalStorage; this.ModalService = ModalService; @@ -55,6 +57,13 @@ class KubernetesConfigurationController { this.selectTab(2); } + copyConfigurationValue(idx) { + this.clipboard.copyText(this.formValues.Data[idx].Value); + $('#copyValueNotification_' + idx) + .show() + .fadeOut(2500); + } + isFormValid() { if (this.formValues.IsSimple) { return this.formValues.Data.length > 0 && this.state.isDataValid; From b09b1b16914a208e28d13f45b9fa3af74a3baf7a Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 14 Jul 2020 12:24:29 +0300 Subject: [PATCH 054/195] feat(aci): show container ip (#4034) --- .../containergroups-datatable/containerGroupsDatatable.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html index f9936d78b..a024c5eb0 100644 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html @@ -66,7 +66,7 @@ {{ item.Location }} - :{{ p.port }} + {{ item.IPAddress }}:{{ p.port }} - From 1b3e2c8f69fdef2c666636a24ae1e903f7137524 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Tue, 14 Jul 2020 22:45:19 +0200 Subject: [PATCH 055/195] feat(kubernetes): add ingress details (#4013) * feat(kubernetes): add ingress details * fix(kubernetes): fix broken ingress generated links + ignore IP retrieval/display info on missing LB ingress ip * refactor(kubernetes): each ingress rule in apps port mappings has now its own row * feat(kubernetes): remove protocol column and concat it to container port * feat(kubernetes): edit display of ingress rules in application details * feat(kubernetes): minor UI update Co-authored-by: Anthony Lapenna --- .../applicationsPortsDatatable.html | 102 ++++++++++--- .../applicationsPortsDatatableController.js | 17 ++- app/kubernetes/converters/application.js | 53 ++++--- app/kubernetes/filters/applicationFilters.js | 14 ++ app/kubernetes/helpers/application/index.js | 17 ++- app/kubernetes/helpers/serviceHelper.js | 2 +- app/kubernetes/ingress/converter.js | 19 +++ app/kubernetes/ingress/helper.js | 7 + app/kubernetes/ingress/models.js | 16 ++ app/kubernetes/ingress/rest.js | 50 +++++++ app/kubernetes/ingress/service.js | 54 +++++++ app/kubernetes/models/application/models.js | 17 +++ app/kubernetes/models/port/models.js | 1 + app/kubernetes/models/service/models.js | 1 + app/kubernetes/services/applicationService.js | 59 ++++---- .../applications/applicationsController.js | 4 +- .../views/applications/edit/application.html | 138 ++++++++++-------- .../edit/applicationController.js | 15 ++ jsconfig.json | 1 + 19 files changed, 450 insertions(+), 137 deletions(-) create mode 100644 app/kubernetes/ingress/converter.js create mode 100644 app/kubernetes/ingress/helper.js create mode 100644 app/kubernetes/ingress/models.js create mode 100644 app/kubernetes/ingress/rest.js create mode 100644 app/kubernetes/ingress/service.js diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html index cf66330d6..814ff9ebf 100644 --- a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatable.html @@ -96,15 +96,13 @@ - - Protocol - - - + HTTP route + + + + {{ item.Name }} system external + - + + Load balancer {{ item.LoadBalancerIPAddress }} pending - Internal - Cluster + + + Internal + + + Cluster - - {{ item.Ports[0].Port }} - + + + + {{ item.Ports[0].Port }} + + access + + + + + + {{ item.Ports[0].TargetPort }}/{{ item.Ports[0].Protocol }} + + + + + - + + pending + + + + {{ $ctrl.buildIngressRuleURL(item.Ports[0].IngressRules[0]) | stripprotocol }} + + + + + + + + + + - + - + + {{ port.Port }} + access - {{ item.Ports[0].TargetPort }} - {{ item.Ports[0].Protocol }} - - - + {{ port.TargetPort }}/{{ port.Protocol }} + - - + - - @@ -155,12 +200,31 @@ access - {{ port.TargetPort }} - {{ port.Protocol }} + {{ port.TargetPort }}/{{ port.Protocol }} + + pending + + + + {{ $ctrl.buildIngressRuleURL(rule) | stripprotocol }} + + + + + Loading... + No application port mapping available. diff --git a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js index e7086a40c..848e0ad6b 100644 --- a/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js +++ b/app/kubernetes/components/datatables/applications-ports-datatable/applicationsPortsDatatableController.js @@ -1,6 +1,7 @@ import _ from 'lodash-es'; import { KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatatableController', [ '$scope', @@ -16,6 +17,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatata }); var ctrl = this; + this.KubernetesServiceTypes = KubernetesServiceTypes; this.settings = Object.assign(this.settings, { showSystem: false, @@ -49,7 +51,20 @@ angular.module('portainer.docker').controller('KubernetesApplicationsPortsDatata }; this.itemCanExpand = function (item) { - return item.Ports.length > 1; + return item.Ports.length > 1 || item.Ports[0].IngressRules.length > 1; + }; + + this.buildIngressRuleURL = function (rule) { + const hostname = rule.Host ? rule.Host : rule.IP; + return 'http://' + hostname + rule.Path; + }; + + this.portHasIngressRules = function (port) { + return port.IngressRules.length > 0; + }; + + this.ruleCanBeDisplayed = function (rule) { + return !rule.Host && !rule.IP ? false : true; }; this.hasExpandableItems = function () { diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 5c7cf7476..4491e2e4a 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -1,4 +1,4 @@ -import _ from 'lodash-es'; +import * as _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; import { @@ -25,9 +25,31 @@ import KubernetesStatefulSetConverter from 'Kubernetes/converters/statefulSet'; import KubernetesServiceConverter from 'Kubernetes/converters/service'; import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim'; import PortainerError from 'Portainer/error'; +import { KubernetesApplicationPort } from 'Kubernetes/models/application/models'; +import { KubernetesIngressHelper } from 'Kubernetes/ingress/helper'; + +function _apiPortsToPublishedPorts(pList, pRefs) { + const ports = _.map(pList, (item) => { + const res = new KubernetesApplicationPort(); + res.Port = item.port; + res.TargetPort = item.targetPort; + res.NodePort = item.nodePort; + res.Protocol = item.protocol; + return res; + }); + _.forEach(ports, (port) => { + if (isNaN(port.TargetPort)) { + const targetPort = _.find(pRefs, { name: port.TargetPort }); + if (targetPort) { + port.TargetPort = targetPort.containerPort; + } + } + }); + return ports; +} class KubernetesApplicationConverter { - static applicationCommon(res, data, service) { + static applicationCommon(res, data, service, ingressRules) { res.Id = data.metadata.uid; res.Name = data.metadata.name; res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-'; @@ -87,16 +109,11 @@ class KubernetesApplicationConverter { } } - const ports = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports)); - res.PublishedPorts = service.spec.ports; - _.forEach(res.PublishedPorts, (publishedPort) => { - if (isNaN(publishedPort.targetPort)) { - const targetPort = _.find(ports, { name: publishedPort.targetPort }); - if (targetPort) { - publishedPort.targetPort = targetPort.containerPort; - } - } - }); + const portsRefs = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports)); + const ports = _apiPortsToPublishedPorts(service.spec.ports, portsRefs); + const rules = KubernetesIngressHelper.findSBoundServiceIngressesRules(ingressRules, service); + _.forEach(ports, (port) => (port.IngressRules = _.filter(rules, (rule) => rule.Port === port.Port))); + res.PublishedPorts = ports; } res.Volumes = data.spec.template.spec.volumes ? data.spec.template.spec.volumes : []; @@ -193,9 +210,9 @@ class KubernetesApplicationConverter { ); } - static apiDeploymentToApplication(data, service) { + static apiDeploymentToApplication(data, service, ingressRules) { const res = new KubernetesApplication(); - KubernetesApplicationConverter.applicationCommon(res, data, service); + KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules); res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT; res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED; res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED; @@ -204,9 +221,9 @@ class KubernetesApplicationConverter { return res; } - static apiDaemonSetToApplication(data, service) { + static apiDaemonSetToApplication(data, service, ingressRules) { const res = new KubernetesApplication(); - KubernetesApplicationConverter.applicationCommon(res, data, service); + KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules); res.ApplicationType = KubernetesApplicationTypes.DAEMONSET; res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL; res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED; @@ -215,9 +232,9 @@ class KubernetesApplicationConverter { return res; } - static apiStatefulSetToapplication(data, service) { + static apiStatefulSetToapplication(data, service, ingressRules) { const res = new KubernetesApplication(); - KubernetesApplicationConverter.applicationCommon(res, data, service); + KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules); res.ApplicationType = KubernetesApplicationTypes.STATEFULSET; res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED; res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED; diff --git a/app/kubernetes/filters/applicationFilters.js b/app/kubernetes/filters/applicationFilters.js index 968218e06..351007566 100644 --- a/app/kubernetes/filters/applicationFilters.js +++ b/app/kubernetes/filters/applicationFilters.js @@ -1,5 +1,6 @@ import _ from 'lodash-es'; import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models'; +import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; angular .module('portainer.kubernetes') @@ -31,6 +32,19 @@ angular } }; }) + .filter('kubernetesApplicationPortsTableHeaderText', function () { + 'use strict'; + return function (serviceType) { + switch (serviceType) { + case KubernetesServiceTypes.LOAD_BALANCER: + return 'Load balancer'; + case KubernetesServiceTypes.CLUSTER_IP: + return 'Application'; + case KubernetesServiceTypes.NODE_PORT: + return 'Cluster node'; + } + }; + }) .filter('kubernetesApplicationCPUValue', function () { 'use strict'; return function (value) { diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index ab5c43b78..4075b2e0a 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -41,9 +41,10 @@ class KubernetesApplicationHelper { mapping.Ports = _.map(app.PublishedPorts, (item) => { const port = new KubernetesPortMappingPort(); - port.Port = mapping.ServiceType === KubernetesServiceTypes.NODE_PORT ? item.nodePort : item.port; - port.TargetPort = item.targetPort; - port.Protocol = item.protocol; + port.Port = mapping.ServiceType === KubernetesServiceTypes.NODE_PORT ? item.NodePort : item.Port; + port.TargetPort = item.TargetPort; + port.Protocol = item.Protocol; + port.IngressRules = item.IngressRules; return port; }); acc.push(mapping); @@ -249,13 +250,13 @@ class KubernetesApplicationHelper { static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) { const finalRes = _.map(publishedPorts, (port) => { const res = new KubernetesApplicationPublishedPortFormValue(); - res.Protocol = port.protocol; - res.ContainerPort = port.targetPort; + res.Protocol = port.Protocol; + res.ContainerPort = port.TargetPort; if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) { - res.LoadBalancerPort = port.port; - res.LoadBalancerNodePort = port.nodePort; + res.LoadBalancerPort = port.Port; + res.LoadBalancerNodePort = port.NodePort; } else if (serviceType === KubernetesServiceTypes.NODE_PORT) { - res.NodePort = port.nodePort; + res.NodePort = port.NodePort; } return res; }); diff --git a/app/kubernetes/helpers/serviceHelper.js b/app/kubernetes/helpers/serviceHelper.js index c4b35050d..c263a3766 100644 --- a/app/kubernetes/helpers/serviceHelper.js +++ b/app/kubernetes/helpers/serviceHelper.js @@ -7,7 +7,7 @@ class KubernetesServiceHelper { } static findApplicationBoundService(services, rawApp) { - return _.find(services, (item) => _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); + return _.find(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); } } export default KubernetesServiceHelper; diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js new file mode 100644 index 000000000..f931fd3ab --- /dev/null +++ b/app/kubernetes/ingress/converter.js @@ -0,0 +1,19 @@ +import * as _ from 'lodash-es'; +import { KubernetesIngressRule } from './models'; + +export class KubernetesIngressConverter { + static apiToModel(data) { + const rules = _.flatMap(data.spec.rules, (rule) => { + return _.map(rule.http.paths, (path) => { + const ingRule = new KubernetesIngressRule(); + ingRule.ServiceName = path.backend.serviceName; + ingRule.Host = rule.host; + ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined; + ingRule.Port = path.backend.servicePort; + ingRule.Path = path.path; + return ingRule; + }); + }); + return rules; + } +} diff --git a/app/kubernetes/ingress/helper.js b/app/kubernetes/ingress/helper.js new file mode 100644 index 000000000..7090e4596 --- /dev/null +++ b/app/kubernetes/ingress/helper.js @@ -0,0 +1,7 @@ +import * as _ from 'lodash-es'; + +export class KubernetesIngressHelper { + static findSBoundServiceIngressesRules(ingressRules, service) { + return _.filter(ingressRules, (r) => r.ServiceName === service.metadata.name); + } +} diff --git a/app/kubernetes/ingress/models.js b/app/kubernetes/ingress/models.js new file mode 100644 index 000000000..a8486ce94 --- /dev/null +++ b/app/kubernetes/ingress/models.js @@ -0,0 +1,16 @@ +/** + * KubernetesIngressRule Model + */ +const _KubernetesIngressRule = Object.freeze({ + ServiceName: '', + Host: '', + IP: '', + Port: '', + Path: '', +}); + +export class KubernetesIngressRule { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressRule))); + } +} diff --git a/app/kubernetes/ingress/rest.js b/app/kubernetes/ingress/rest.js new file mode 100644 index 000000000..42b87a80c --- /dev/null +++ b/app/kubernetes/ingress/rest.js @@ -0,0 +1,50 @@ +import { rawResponse } from 'Kubernetes/rest/response/transform'; + +angular.module('portainer.kubernetes').factory('KubernetesIngresses', [ + '$resource', + 'API_ENDPOINT_ENDPOINTS', + 'EndpointProvider', + function KubernetesIngressesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/networking.k8s.io/v1beta1' + (namespace ? '/namespaces/:namespace' : '') + '/ingresses/:id/:action'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', + }, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, + delete: { method: 'DELETE' }, + } + ); + }; + }, +]); diff --git a/app/kubernetes/ingress/service.js b/app/kubernetes/ingress/service.js new file mode 100644 index 000000000..72ce0ef5d --- /dev/null +++ b/app/kubernetes/ingress/service.js @@ -0,0 +1,54 @@ +import * as _ from 'lodash-es'; +import angular from 'angular'; +import PortainerError from 'Portainer/error'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; +import { KubernetesIngressConverter } from './converter'; + +class KubernetesIngressService { + /* @ngInject */ + constructor($async, KubernetesIngresses) { + this.$async = $async; + this.KubernetesIngresses = KubernetesIngresses; + + this.getAsync = this.getAsync.bind(this); + this.getAllAsync = this.getAllAsync.bind(this); + } + + /** + * GET + */ + async getAsync(namespace, name) { + try { + const params = new KubernetesCommonParams(); + params.id = name; + const [raw, yaml] = await Promise.all([this.KubernetesIngresses(namespace).get(params).$promise, this.KubernetesIngresses(namespace).getYaml(params).$promise]); + const res = { + Raw: KubernetesIngressConverter.apiToModel(raw), + Yaml: yaml.data, + }; + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve Ingress', err); + } + } + + async getAllAsync(namespace) { + try { + const data = await this.KubernetesIngresses(namespace).get().$promise; + const res = _.reduce(data.items, (arr, item) => _.concat(arr, KubernetesIngressConverter.apiToModel(item)), []); + return res; + } catch (err) { + throw new PortainerError('Unable to retrieve Ingresses', err); + } + } + + get(namespace, name) { + if (name) { + return this.$async(this.getAsync, namespace, name); + } + return this.$async(this.getAllAsync, namespace); + } +} + +export default KubernetesIngressService; +angular.module('portainer.kubernetes').service('KubernetesIngressService', KubernetesIngressService); diff --git a/app/kubernetes/models/application/models.js b/app/kubernetes/models/application/models.js index 3589a5ceb..32a953091 100644 --- a/app/kubernetes/models/application/models.js +++ b/app/kubernetes/models/application/models.js @@ -112,3 +112,20 @@ export class KubernetesApplicationConfigurationVolume { Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationConfigurationVolume))); } } + +/** + * KubernetesApplicationPort Model + */ +const _KubernetesApplicationPort = Object.freeze({ + IngressRules: [], // KubernetesIngressRule[] + NodePort: 0, + TargetPort: 0, + Port: 0, + Protocol: '', +}); + +export class KubernetesApplicationPort { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPort))); + } +} diff --git a/app/kubernetes/models/port/models.js b/app/kubernetes/models/port/models.js index 1409a1059..6b100400a 100644 --- a/app/kubernetes/models/port/models.js +++ b/app/kubernetes/models/port/models.js @@ -5,6 +5,7 @@ const _KubernetesPortMappingPort = Object.freeze({ Port: 0, TargetPort: 0, Protocol: '', + IngressRules: [], // KubernetesIngressRule[] }); export class KubernetesPortMappingPort { diff --git a/app/kubernetes/models/service/models.js b/app/kubernetes/models/service/models.js index 5c9abb61c..8dcc36d86 100644 --- a/app/kubernetes/models/service/models.js +++ b/app/kubernetes/models/service/models.js @@ -3,6 +3,7 @@ export const KubernetesServiceHeadlessClusterIP = 'None'; export const KubernetesServiceTypes = Object.freeze({ LOAD_BALANCER: 'LoadBalancer', NODE_PORT: 'NodePort', + CLUSTER_IP: 'ClusterIP', }); /** diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index ea99d26e1..d4172f606 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -27,7 +27,8 @@ class KubernetesApplicationService { KubernetesNamespaceService, KubernetesPodService, KubernetesHistoryService, - KubernetesHorizontalPodAutoScalerService + KubernetesHorizontalPodAutoScalerService, + KubernetesIngressService ) { this.$async = $async; this.Authentication = Authentication; @@ -41,6 +42,7 @@ class KubernetesApplicationService { this.KubernetesPodService = KubernetesPodService; this.KubernetesHistoryService = KubernetesHistoryService; this.KubernetesHorizontalPodAutoScalerService = KubernetesHorizontalPodAutoScalerService; + this.KubernetesIngressService = KubernetesIngressService; this.getAsync = this.getAsync.bind(this); this.getAllAsync = this.getAllAsync.bind(this); @@ -73,25 +75,26 @@ class KubernetesApplicationService { */ async getAsync(namespace, name) { try { - const [deployment, daemonSet, statefulSet, pods, autoScalers] = await Promise.allSettled([ + const [deployment, daemonSet, statefulSet, pods, autoScalers, ingresses] = await Promise.allSettled([ this.KubernetesDeploymentService.get(namespace, name), this.KubernetesDaemonSetService.get(namespace, name), this.KubernetesStatefulSetService.get(namespace, name), this.KubernetesPodService.get(namespace), this.KubernetesHorizontalPodAutoScalerService.get(namespace), + this.KubernetesIngressService.get(namespace), ]); let rootItem; - let converterFunction; + let converterFunc; if (deployment.status === 'fulfilled') { rootItem = deployment; - converterFunction = KubernetesApplicationConverter.apiDeploymentToApplication; + converterFunc = KubernetesApplicationConverter.apiDeploymentToApplication; } else if (daemonSet.status === 'fulfilled') { rootItem = daemonSet; - converterFunction = KubernetesApplicationConverter.apiDaemonSetToApplication; + converterFunc = KubernetesApplicationConverter.apiDaemonSetToApplication; } else if (statefulSet.status === 'fulfilled') { rootItem = statefulSet; - converterFunction = KubernetesApplicationConverter.apiStatefulSetToapplication; + converterFunc = KubernetesApplicationConverter.apiStatefulSetToapplication; } else { throw new PortainerError('Unable to determine which association to use'); } @@ -100,7 +103,7 @@ class KubernetesApplicationService { const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw); const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {}; - const application = converterFunction(rootItem.value.Raw, service.Raw); + const application = converterFunc(rootItem.value.Raw, service.Raw, ingresses.value); application.Yaml = rootItem.value.Yaml; application.Raw = rootItem.value.Raw; application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods.value, application.Raw); @@ -117,6 +120,8 @@ class KubernetesApplicationService { if (scaler && scaler.Yaml) { application.Yaml += '---\n' + scaler.Yaml; } + // TODO: refactor + // append ingress yaml ? return application; } catch (err) { throw err; @@ -126,33 +131,35 @@ class KubernetesApplicationService { async getAllAsync(namespace) { try { const namespaces = namespace ? [namespace] : _.map(await this.KubernetesNamespaceService.get(), 'Name'); + + const convertToApplication = (item, converterFunc, services, pods, ingresses) => { + const service = KubernetesServiceHelper.findApplicationBoundService(services, item); + const application = converterFunc(item, service, ingresses); + application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); + return application; + }; + const res = await Promise.all( _.map(namespaces, async (ns) => { - const [deployments, daemonSets, statefulSets, services, pods] = await Promise.all([ + const [deployments, daemonSets, statefulSets, services, pods, ingresses] = await Promise.all([ this.KubernetesDeploymentService.get(ns), this.KubernetesDaemonSetService.get(ns), this.KubernetesStatefulSetService.get(ns), this.KubernetesServiceService.get(ns), this.KubernetesPodService.get(ns), + this.KubernetesIngressService.get(ns), ]); - const deploymentApplications = _.map(deployments, (item) => { - const service = KubernetesServiceHelper.findApplicationBoundService(services, item); - const application = KubernetesApplicationConverter.apiDeploymentToApplication(item, service); - application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); - return application; - }); - const daemonSetApplications = _.map(daemonSets, (item) => { - const service = KubernetesServiceHelper.findApplicationBoundService(services, item); - const application = KubernetesApplicationConverter.apiDaemonSetToApplication(item, service); - application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); - return application; - }); - const statefulSetApplications = _.map(statefulSets, (item) => { - const service = KubernetesServiceHelper.findApplicationBoundService(services, item); - const application = KubernetesApplicationConverter.apiStatefulSetToapplication(item, service); - application.Pods = KubernetesApplicationHelper.associatePodsAndApplication(pods, item); - return application; - }); + + const deploymentApplications = _.map(deployments, (item) => + convertToApplication(item, KubernetesApplicationConverter.apiDeploymentToApplication, services, pods, ingresses) + ); + const daemonSetApplications = _.map(daemonSets, (item) => + convertToApplication(item, KubernetesApplicationConverter.apiDaemonSetToApplication, services, pods, ingresses) + ); + const statefulSetApplications = _.map(statefulSets, (item) => + convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses) + ); + return _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications); }) ); diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js index 859698a00..07553810e 100644 --- a/app/kubernetes/views/applications/applicationsController.js +++ b/app/kubernetes/views/applications/applicationsController.js @@ -87,9 +87,7 @@ class KubernetesApplicationsController { item.Expanded = false; item.Highlighted = false; if (item.Name === application.Name) { - if (item.Ports.length > 1) { - item.Expanded = true; - } + item.Expanded = true; item.Highlighted = true; } }); diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 0ea9118ba..248fefc0e 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -180,7 +180,8 @@
-
+ +
This application is exposed through an external load balancer. Use the links below to access the different ports exposed. @@ -210,55 +211,19 @@

-
- - - - - - - - - - - -
Container portLoad balancer port
{{ port.targetPort }} - {{ port.port }} - - access - -
-
-
+ +
This application is exposed globally on all nodes of your cluster. It can be reached using the IP address of any node in your cluster using the port configuration below.
-
- - - - - - - - - - - -
Container portCluster node port
{{ port.targetPort }}{{ port.nodePort }}
-
-
+ +
This application is only available for internal usage inside the cluster via the application name {{ ctrl.application.ServiceName }} @@ -270,22 +235,75 @@

Refer to the below port configuration to access the application.

-
- - - - - - - - - - - - - -
Container portApplication portProtocol
{{ port.targetPort }}{{ port.port }}{{ port.protocol }}
-
+
+ + +
+ + + + + + + + + + + + + + + + + + +
Container port{{ ctrl.application.ServiceType | kubernetesApplicationPortsTableHeaderText }} portHTTP route
{{ port.TargetPort }}/{{ port.Protocol }} + + {{ port.NodePort }} + + + {{ port.Port }} + + + access + + -
{{ port.TargetPort }}/{{ port.Protocol }} + + {{ port.NodePort }} + + + {{ port.Port }} + + + access + + + pending + + + + {{ ctrl.buildIngressRuleURL(rule) | stripprotocol }} + + +
@@ -306,10 +324,8 @@ Maximum instances Target CPU usage - + + diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index 0aa896c25..f8706c265 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -3,6 +3,7 @@ import _ from 'lodash-es'; import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; +import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; class KubernetesApplicationController { /* @ngInject */ @@ -34,6 +35,7 @@ class KubernetesApplicationController { this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; + this.KubernetesServiceTypes = KubernetesServiceTypes; this.onInit = this.onInit.bind(this); this.getApplication = this.getApplication.bind(this); @@ -85,6 +87,19 @@ class KubernetesApplicationController { return this.state.eventWarningCount; } + buildIngressRuleURL(rule) { + const hostname = rule.Host ? rule.Host : rule.IP; + return 'http://' + hostname + rule.Path; + } + + portHasIngressRules(port) { + return port.IngressRules.length > 0; + } + + ruleCanBeDisplayed(rule) { + return !rule.Host && !rule.IP ? false : true; + } + /** * ROLLBACK */ diff --git a/jsconfig.json b/jsconfig.json index c1b062209..493168e31 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -3,6 +3,7 @@ "target": "es2017", "allowSyntheticDefaultImports": false, "baseUrl": "app", + "module": "commonjs", "paths": { "Agent/*": ["agent/*"], "Azure/*": ["azure/*"], From 3c34fbd8f23be3c028e82bdc851a9230acf0f0ef Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 14 Jul 2020 23:46:38 +0300 Subject: [PATCH 056/195] refactor(router): show endpoint id in url (#3966) * refactor(module): provide basic endpoint id url * fix(stacks): fix route to include endpointId * fix(stacks): fix stacks urls * fix(sidebar): fix urls to docker routes * refactor(app): set endpoint id on change view * refactor(dashboard): revert to old version * refactor(sidebar): revert file * feat(app): wip load endpoint on route change * feat(home): show error * feat(app): load endpoint route * feat(sidebar): show endpoint per provider * refactor(app): revert * refactor(app): clean endpoint startup * feat(edge): check for edge k8s * refactor(endpoints): move all modules under endpoint route * refactor(stacks): move stacks route to docker * refactor(templates): move templates route to docker * refactor(app): check endpoint when entering docker module * fix(app): load endpoint when entering endpoints modules * feat(azure): check endpoint * feat(kubernetes): check endpoint * feat(home): show loading state when loading edge * style(app): revert small changes * refactor(sidebar): remove refernce to endpointId * fix(stacks): fix stacks route * style(docker): sort routes * feat(app): change route to home if endpoint failed * fix(services): guard against empty snapshots * feat(app): show error when failed to load endpoint * feat(app): reload home route when failing * refactor(router): replace resolvers with onEnter --- app/azure/_module.js | 14 +- app/docker/__module.js | 144 ++++++++++++++-- .../dockerSidebarContent.html | 10 +- app/docker/interceptors/infoInterceptor.js | 2 +- app/docker/interceptors/versionInterceptor.js | 2 +- app/docker/views/dashboard/dashboard.html | 2 +- .../views/dashboard/dashboardController.js | 5 +- app/kubernetes/__module.js | 29 ++-- app/portainer/__module.js | 142 ++++------------ .../stacks-datatable/stacksDatatable.html | 4 +- .../template-list/templateList.html | 2 +- app/portainer/services/stateManager.js | 6 +- app/portainer/views/home/homeController.js | 159 ++---------------- .../stacks/create/createStackController.js | 7 +- .../views/stacks/create/createstack.html | 2 +- app/portainer/views/stacks/edit/stack.html | 4 +- .../views/stacks/edit/stackController.js | 8 +- app/portainer/views/stacks/stacks.html | 2 +- app/portainer/views/templates/templates.html | 2 +- .../views/templates/templatesController.js | 15 +- 20 files changed, 245 insertions(+), 316 deletions(-) diff --git a/app/azure/_module.js b/app/azure/_module.js index a11a5aa5e..523fb20e3 100644 --- a/app/azure/_module.js +++ b/app/azure/_module.js @@ -6,8 +6,20 @@ angular.module('portainer.azure', ['portainer.app']).config([ var azure = { name: 'azure', url: '/azure', - parent: 'root', + parent: 'endpoint', abstract: true, + /* ngInject */ + async onEnter($state, endpoint, EndpointProvider, Notifications, StateManager) { + try { + EndpointProvider.setEndpointID(endpoint.Id); + EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + EndpointProvider.setOfflineModeFromStatus(endpoint.Status); + await StateManager.updateEndpointState(endpoint, []); + } catch (e) { + Notifications.error('Failed loading endpoint', e); + $state.go('portainer.home', {}, { reload: true }); + } + }, }; var containerInstances = { diff --git a/app/docker/__module.js b/app/docker/__module.js index 816877528..1aa2d827b 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -5,19 +5,53 @@ angular.module('portainer.docker', ['portainer.app']).config([ var docker = { name: 'docker', - parent: 'root', + parent: 'endpoint', abstract: true, - resolve: { - endpointID: [ - 'EndpointProvider', - '$state', - function (EndpointProvider, $state) { - var id = EndpointProvider.endpointID(); - if (!id) { - return $state.go('portainer.home'); + /* ngInject */ + async onEnter(endpoint, $state, EndpointService, EndpointProvider, LegacyExtensionManager, Notifications, StateManager, SystemService) { + try { + const status = await checkEndpointStatus(endpoint); + + if (endpoint.Type !== 4) { + await updateEndpointStatus(endpoint, status); + } + endpoint.Status = status; + + if (status === 2) { + if (!endpoint.Snapshots[0]) { + throw new Error('Endpoint is unreachable and there is no snapshot available for offline browsing.'); } - }, - ], + if (endpoint.Snapshots[0].Swarm) { + throw new Error('Endpoint is unreachable. Connect to another swarm manager.'); + } + } + + EndpointProvider.setEndpointID(endpoint.Id); + EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + EndpointProvider.setOfflineModeFromStatus(endpoint.Status); + + const extensions = await LegacyExtensionManager.initEndpointExtensions(endpoint); + await StateManager.updateEndpointState(endpoint, extensions); + } catch (e) { + Notifications.error('Failed loading endpoint', e); + $state.go('portainer.home', {}, { reload: true }); + } + + async function checkEndpointStatus(endpoint) { + try { + await SystemService.ping(endpoint.Id); + return 1; + } catch (e) { + return 2; + } + } + + async function updateEndpointStatus(endpoint, status) { + if (endpoint.Status === status) { + return; + } + await EndpointService.updateEndpoint(endpoint.Id, { Status: status }); + } }, }; @@ -144,6 +178,43 @@ angular.module('portainer.docker', ['portainer.app']).config([ }, }; + const customTemplates = { + name: 'portainer.templates.custom', + url: '/custom', + + views: { + 'content@': { + component: 'customTemplatesView', + }, + }, + }; + + const customTemplatesNew = { + name: 'portainer.templates.custom.new', + url: '/new?fileContent&type', + + views: { + 'content@': { + component: 'createCustomTemplateView', + }, + }, + params: { + fileContent: '', + type: '', + }, + }; + + const customTemplatesEdit = { + name: 'portainer.templates.custom.edit', + url: '/:id', + + views: { + 'content@': { + component: 'editCustomTemplateView', + }, + }, + }; + var dashboard = { name: 'docker.dashboard', url: '/dashboard', @@ -366,6 +437,39 @@ angular.module('portainer.docker', ['portainer.app']).config([ }, }; + var stacks = { + name: 'docker.stacks', + url: '/stacks', + views: { + 'content@': { + templateUrl: '~Portainer/views/stacks/stacks.html', + controller: 'StacksController', + }, + }, + }; + + var stack = { + name: 'docker.stacks.stack', + url: '/:name?id&type&external', + views: { + 'content@': { + templateUrl: '~Portainer/views/stacks/edit/stack.html', + controller: 'StackController', + }, + }, + }; + + var stackCreation = { + name: 'docker.stacks.newstack', + url: '/newstack', + views: { + 'content@': { + templateUrl: '~Portainer/views/stacks/create/createstack.html', + controller: 'CreateStackController', + }, + }, + }; + var swarm = { name: 'docker.swarm', url: '/swarm', @@ -416,6 +520,17 @@ angular.module('portainer.docker', ['portainer.app']).config([ }, }; + var templates = { + name: 'docker.templates', + url: '/templates', + views: { + 'content@': { + templateUrl: '~Portainer/views/templates/templates.html', + controller: 'TemplatesController', + }, + }, + }; + var volumes = { name: 'docker.volumes', url: '/volumes', @@ -471,6 +586,9 @@ angular.module('portainer.docker', ['portainer.app']).config([ $stateRegistryProvider.register(containerInspect); $stateRegistryProvider.register(containerLogs); $stateRegistryProvider.register(containerStats); + $stateRegistryProvider.register(customTemplates); + $stateRegistryProvider.register(customTemplatesNew); + $stateRegistryProvider.register(customTemplatesEdit); $stateRegistryProvider.register(docker); $stateRegistryProvider.register(dashboard); $stateRegistryProvider.register(host); @@ -493,11 +611,15 @@ angular.module('portainer.docker', ['portainer.app']).config([ $stateRegistryProvider.register(service); $stateRegistryProvider.register(serviceCreation); $stateRegistryProvider.register(serviceLogs); + $stateRegistryProvider.register(stacks); + $stateRegistryProvider.register(stack); + $stateRegistryProvider.register(stackCreation); $stateRegistryProvider.register(swarm); $stateRegistryProvider.register(swarmVisualizer); $stateRegistryProvider.register(tasks); $stateRegistryProvider.register(task); $stateRegistryProvider.register(taskLogs); + $stateRegistryProvider.register(templates); $stateRegistryProvider.register(volumes); $stateRegistryProvider.register(volume); $stateRegistryProvider.register(volumeBrowse); diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index 118b63fbe..ea8a47095 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -2,14 +2,14 @@ Dashboard diff --git a/app/docker/__module.js b/app/docker/__module.js index 1aa2d827b..5747164bf 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -7,51 +7,56 @@ angular.module('portainer.docker', ['portainer.app']).config([ name: 'docker', parent: 'endpoint', abstract: true, - /* ngInject */ - async onEnter(endpoint, $state, EndpointService, EndpointProvider, LegacyExtensionManager, Notifications, StateManager, SystemService) { - try { - const status = await checkEndpointStatus(endpoint); - - if (endpoint.Type !== 4) { - await updateEndpointStatus(endpoint, status); - } - endpoint.Status = status; - - if (status === 2) { - if (!endpoint.Snapshots[0]) { - throw new Error('Endpoint is unreachable and there is no snapshot available for offline browsing.'); - } - if (endpoint.Snapshots[0].Swarm) { - throw new Error('Endpoint is unreachable. Connect to another swarm manager.'); - } - } - - EndpointProvider.setEndpointID(endpoint.Id); - EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); - EndpointProvider.setOfflineModeFromStatus(endpoint.Status); - - const extensions = await LegacyExtensionManager.initEndpointExtensions(endpoint); - await StateManager.updateEndpointState(endpoint, extensions); - } catch (e) { - Notifications.error('Failed loading endpoint', e); - $state.go('portainer.home', {}, { reload: true }); - } - - async function checkEndpointStatus(endpoint) { - try { - await SystemService.ping(endpoint.Id); - return 1; - } catch (e) { - return 2; - } - } - - async function updateEndpointStatus(endpoint, status) { - if (endpoint.Status === status) { + onEnter: /* @ngInject */ function onEnter(endpoint, $async, $state, EndpointService, EndpointProvider, LegacyExtensionManager, Notifications, StateManager, SystemService) { + return $async(async () => { + if (![1, 2, 4].includes(endpoint.Type)) { + $state.go('portainer.home'); return; } - await EndpointService.updateEndpoint(endpoint.Id, { Status: status }); - } + try { + const status = await checkEndpointStatus(endpoint); + + if (endpoint.Type !== 4) { + await updateEndpointStatus(endpoint, status); + } + endpoint.Status = status; + + if (status === 2) { + if (!endpoint.Snapshots[0]) { + throw new Error('Endpoint is unreachable and there is no snapshot available for offline browsing.'); + } + if (endpoint.Snapshots[0].Swarm) { + throw new Error('Endpoint is unreachable. Connect to another swarm manager.'); + } + } + + EndpointProvider.setEndpointID(endpoint.Id); + EndpointProvider.setEndpointPublicURL(endpoint.PublicURL); + EndpointProvider.setOfflineModeFromStatus(endpoint.Status); + + const extensions = await LegacyExtensionManager.initEndpointExtensions(endpoint); + await StateManager.updateEndpointState(endpoint, extensions); + } catch (e) { + Notifications.error('Failed loading endpoint', e); + $state.go('portainer.home', {}, { reload: true }); + } + + async function checkEndpointStatus(endpoint) { + try { + await SystemService.ping(endpoint.Id); + return 1; + } catch (e) { + return 2; + } + } + + async function updateEndpointStatus(endpoint, status) { + if (endpoint.Status === status) { + return; + } + await EndpointService.updateEndpoint(endpoint.Id, { Status: status }); + } + }); }, }; @@ -179,7 +184,7 @@ angular.module('portainer.docker', ['portainer.app']).config([ }; const customTemplates = { - name: 'portainer.templates.custom', + name: 'docker.templates.custom', url: '/custom', views: { @@ -190,7 +195,7 @@ angular.module('portainer.docker', ['portainer.app']).config([ }; const customTemplatesNew = { - name: 'portainer.templates.custom.new', + name: 'docker.templates.custom.new', url: '/new?fileContent&type', views: { @@ -205,7 +210,7 @@ angular.module('portainer.docker', ['portainer.app']).config([ }; const customTemplatesEdit = { - name: 'portainer.templates.custom.edit', + name: 'docker.templates.custom.edit', url: '/:id', views: { diff --git a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js index 400293ee5..87734c8a2 100644 --- a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js +++ b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js @@ -8,5 +8,6 @@ angular.module('portainer.docker').component('dockerSidebarContent', { offlineMode: '<', toggle: '<', currentRouteName: '<', + endpointId: '<', }, }); diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index ea8a47095..15d739ae2 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -1,43 +1,43 @@ diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index 228f8fc96..6bc5c909a 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -8,24 +8,30 @@ angular.module('portainer.kubernetes', ['portainer.app']).config([ url: '/kubernetes', parent: 'endpoint', abstract: true, - /* @ngInject */ - async onEnter($state, endpoint, EndpointProvider, KubernetesHealthService, Notifications, StateManager) { - try { - if (endpoint.Type === 7) { - try { - await KubernetesHealthService.ping(); - endpoint.Status = 1; - } catch (e) { - endpoint.Status = 2; - } - } - EndpointProvider.setEndpointID(endpoint.Id); - await StateManager.updateEndpointState(endpoint, []); - } catch (e) { - Notifications.error('Failed loading endpoint', e); - $state.go('portainer.home', {}, { reload: true }); - } + onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, EndpointProvider, KubernetesHealthService, Notifications, StateManager) { + return $async(async () => { + if (![5, 6, 7].includes(endpoint.Type)) { + $state.go('portainer.home'); + return; + } + try { + if (endpoint.Type === 7) { + try { + await KubernetesHealthService.ping(); + endpoint.Status = 1; + } catch (e) { + endpoint.Status = 2; + } + } + + EndpointProvider.setEndpointID(endpoint.Id); + await StateManager.updateEndpointState(endpoint, []); + } catch (e) { + Notifications.error('Failed loading endpoint', e); + $state.go('portainer.home', {}, { reload: true }); + } + }); }, }; diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html index 354ba3760..bcdf93b7e 100644 --- a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html +++ b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.html @@ -1,18 +1,18 @@ diff --git a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js index c12568fa7..8b74d4a47 100644 --- a/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js +++ b/app/kubernetes/components/kubernetes-sidebar-content/kubernetesSidebarContent.js @@ -2,5 +2,6 @@ angular.module('portainer.kubernetes').component('kubernetesSidebarContent', { templateUrl: './kubernetesSidebarContent.html', bindings: { adminAccess: '<', + endpointId: '<', }, }); diff --git a/app/portainer/__module.js b/app/portainer/__module.js index af0d7591a..5f3907272 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -81,8 +81,7 @@ angular.module('portainer.app', []).config([ parent: 'root', abstract: true, resolve: { - /* @ngInject */ - endpoint($async, $state, $transition$, EndpointService, Notifications) { + endpoint: /* @ngInject */ function endpoint($async, $state, $transition$, EndpointService, Notifications) { return $async(async () => { try { const endpointId = +$transition$.params().endpointId; diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 44c1e0e16..9e7c3c0c2 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -13,9 +13,13 @@ Home - + - +
diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html index 573acd415..bd2c4e6b7 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html @@ -93,11 +93,11 @@
diff --git a/app/portainer/components/datatables/genericDatatableController.js b/app/portainer/components/datatables/genericDatatableController.js index 89367a114..21ccf17e9 100644 --- a/app/portainer/components/datatables/genericDatatableController.js +++ b/app/portainer/components/datatables/genericDatatableController.js @@ -43,12 +43,11 @@ angular.module('portainer.app').controller('GenericDatatableController', [ DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter); }; - this.changeOrderBy = changeOrderBy.bind(this); - function changeOrderBy(orderField) { + this.changeOrderBy = function changeOrderBy(orderField) { this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; this.state.orderBy = orderField; DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); - } + }; this.selectItem = function (item, event) { // Handle range select using shift diff --git a/app/portainer/services/authentication.js b/app/portainer/services/authentication.js index a5f1294fa..82ce17de8 100644 --- a/app/portainer/services/authentication.js +++ b/app/portainer/services/authentication.js @@ -41,7 +41,7 @@ angular.module('portainer.app').factory('Authentication', [ StateManager.clean(); EndpointProvider.clean(); - LocalStorage.clean(); + LocalStorage.cleanAuthData(); LocalStorage.storeLoginStateUUID(''); } diff --git a/app/portainer/services/endpointProvider.js b/app/portainer/services/endpointProvider.js index bf76c6e46..1a49f1321 100644 --- a/app/portainer/services/endpointProvider.js +++ b/app/portainer/services/endpointProvider.js @@ -24,6 +24,7 @@ angular.module('portainer.app').factory('EndpointProvider', [ }; service.clean = function () { + LocalStorage.cleanEndpointData(); endpoint = {}; }; diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index 1cd99cb6d..95bc618d5 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -1,7 +1,6 @@ angular.module('portainer.app').factory('LocalStorage', [ 'localStorageService', function LocalStorageFactory(localStorageService) { - 'use strict'; return { storeEndpointID: function (id) { localStorageService.set('ENDPOINT_ID', id); @@ -16,10 +15,10 @@ angular.module('portainer.app').factory('LocalStorage', [ return localStorageService.get('ENDPOINT_PUBLIC_URL'); }, storeLoginStateUUID: function (uuid) { - localStorageService.cookie.set('LOGIN_STATE_UUID', uuid); + localStorageService.set('LOGIN_STATE_UUID', uuid); }, getLoginStateUUID: function () { - return localStorageService.cookie.get('LOGIN_STATE_UUID'); + return localStorageService.get('LOGIN_STATE_UUID'); }, storeOfflineMode: function (isOffline) { localStorageService.set('ENDPOINT_OFFLINE_MODE', isOffline); @@ -46,10 +45,10 @@ angular.module('portainer.app').factory('LocalStorage', [ return localStorageService.get('APPLICATION_STATE'); }, storeUIState: function (state) { - localStorageService.cookie.set('UI_STATE', state); + localStorageService.set('UI_STATE', state); }, getUIState: function () { - return localStorageService.cookie.get('UI_STATE'); + return localStorageService.get('UI_STATE'); }, storeExtensionState: function (state) { localStorageService.set('EXTENSION_STATE', state); @@ -67,40 +66,40 @@ angular.module('portainer.app').factory('LocalStorage', [ localStorageService.remove('JWT'); }, storePaginationLimit: function (key, count) { - localStorageService.cookie.set('datatable_pagination_' + key, count); + localStorageService.set('datatable_pagination_' + key, count); }, getPaginationLimit: function (key) { - return localStorageService.cookie.get('datatable_pagination_' + key); + return localStorageService.get('datatable_pagination_' + key); }, getDataTableOrder: function (key) { - return localStorageService.cookie.get('datatable_order_' + key); + return localStorageService.get('datatable_order_' + key); }, storeDataTableOrder: function (key, data) { - localStorageService.cookie.set('datatable_order_' + key, data); + localStorageService.set('datatable_order_' + key, data); }, getDataTableTextFilters: function (key) { - return localStorageService.cookie.get('datatable_text_filter_' + key); + return localStorageService.get('datatable_text_filter_' + key); }, storeDataTableTextFilters: function (key, data) { - localStorageService.cookie.set('datatable_text_filter_' + key, data); + localStorageService.set('datatable_text_filter_' + key, data); }, getDataTableFilters: function (key) { - return localStorageService.cookie.get('datatable_filters_' + key); + return localStorageService.get('datatable_filters_' + key); }, storeDataTableFilters: function (key, data) { - localStorageService.cookie.set('datatable_filters_' + key, data); + localStorageService.set('datatable_filters_' + key, data); }, getDataTableSettings: function (key) { - return localStorageService.cookie.get('datatable_settings_' + key); + return localStorageService.get('datatable_settings_' + key); }, storeDataTableSettings: function (key, data) { - localStorageService.cookie.set('datatable_settings_' + key, data); + localStorageService.set('datatable_settings_' + key, data); }, getDataTableExpandedItems: function (key) { - return localStorageService.cookie.get('datatable_expandeditems_' + key); + return localStorageService.get('datatable_expandeditems_' + key); }, storeDataTableExpandedItems: function (key, data) { - localStorageService.cookie.set('datatable_expandeditems_' + key, data); + localStorageService.set('datatable_expandeditems_' + key, data); }, getDataTableSelectedItems: function (key) { return localStorageService.get('datatable_selecteditems_' + key); @@ -109,16 +108,16 @@ angular.module('portainer.app').factory('LocalStorage', [ localStorageService.set('datatable_selecteditems_' + key, data); }, storeSwarmVisualizerSettings: function (key, data) { - localStorageService.cookie.set('swarmvisualizer_' + key, data); + localStorageService.set('swarmvisualizer_' + key, data); }, getSwarmVisualizerSettings: function (key) { - return localStorageService.cookie.get('swarmvisualizer_' + key); + return localStorageService.get('swarmvisualizer_' + key); }, storeColumnVisibilitySettings: function (key, data) { - localStorageService.cookie.set('col_visibility_' + key, data); + localStorageService.set('col_visibility_' + key, data); }, getColumnVisibilitySettings: function (key) { - return localStorageService.cookie.get('col_visibility_' + key); + return localStorageService.get('col_visibility_' + key); }, storeJobImage: function (data) { localStorageService.set('job_image', data); @@ -133,12 +132,24 @@ angular.module('portainer.app').factory('LocalStorage', [ const activeTab = localStorageService.get('active_tab_' + key); return activeTab === null ? 0 : activeTab; }, + storeToolbarToggle(value) { + localStorageService.set('toolbar_toggle', value); + }, + getToolbarToggle() { + return localStorageService.get('toolbar_toggle'); + }, storeLogoutReason: (reason) => localStorageService.set('logout_reason', reason), getLogoutReason: () => localStorageService.get('logout_reason'), cleanLogoutReason: () => localStorageService.remove('logout_reason'), clean: function () { localStorageService.clearAll(); }, + cleanAuthData() { + localStorageService.remove('JWT', 'EXTENSION_STATE', 'APPLICATION_STATE', 'LOGIN_STATE_UUID'); + }, + cleanEndpointData() { + localStorageService.remove('ENDPOINT_ID', 'ENDPOINT_PUBLIC_URL', 'ENDPOINT_OFFLINE_MODE', 'ENDPOINTS_DATA', 'ENDPOINT_STATE'); + }, }; }, ]); diff --git a/app/portainer/views/main/mainController.js b/app/portainer/views/main/mainController.js index 71b50424c..00fb4436c 100644 --- a/app/portainer/views/main/mainController.js +++ b/app/portainer/views/main/mainController.js @@ -1,9 +1,9 @@ angular.module('portainer.app').controller('MainController', [ '$scope', - '$cookieStore', + 'LocalStorage', 'StateManager', 'EndpointProvider', - function ($scope, $cookieStore, StateManager, EndpointProvider) { + function ($scope, LocalStorage, StateManager, EndpointProvider) { /** * Sidebar Toggle & Cookie Control */ @@ -17,11 +17,8 @@ angular.module('portainer.app').controller('MainController', [ $scope.$watch($scope.getWidth, function (newValue) { if (newValue >= mobileView) { - if (angular.isDefined($cookieStore.get('toggle'))) { - $scope.toggle = !$cookieStore.get('toggle') ? false : true; - } else { - $scope.toggle = true; - } + const toggleValue = LocalStorage.getToolbarToggle(); + $scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : true; } else { $scope.toggle = false; } @@ -29,7 +26,7 @@ angular.module('portainer.app').controller('MainController', [ $scope.toggleSidebar = function () { $scope.toggle = !$scope.toggle; - $cookieStore.put('toggle', $scope.toggle); + LocalStorage.storeToolbarToggle($scope.toggle); }; window.onresize = function () { From 6f6bc24efdb6d6e761656070afc4791de9680f0c Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 22 Jul 2020 21:38:45 +0300 Subject: [PATCH 074/195] feat(containers): Ensure users cannot create privileged containers via the API (#3969) (#4077) * feat(containers): Ensure users cannot create privileged containers via the API * feat(containers): add rbac check in stack creation Co-authored-by: Maxime Bajeux --- .../handler/stacks/create_compose_stack.go | 19 +++-- api/http/handler/stacks/create_swarm_stack.go | 19 +++-- api/http/handler/stacks/handler.go | 20 ++++++ api/http/handler/stacks/stack_create.go | 20 ++++-- api/http/proxy/factory/docker/containers.go | 72 +++++++++++++++++++ api/http/proxy/factory/docker/transport.go | 3 +- 6 files changed, 135 insertions(+), 18 deletions(-) diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index b021190b8..37d8c1d7c 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -283,6 +283,7 @@ type composeStackDeploymentConfig struct { dockerhub *portainer.DockerHub registries []portainer.Registry isAdmin bool + user *portainer.User } func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) { @@ -302,12 +303,18 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai } filteredRegistries := security.FilterRegistries(registries, securityContext) + user, err := handler.DataStore.User().User(securityContext.UserID) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err} + } + config := &composeStackDeploymentConfig{ stack: stack, endpoint: endpoint, dockerhub: dockerhub, registries: filteredRegistries, isAdmin: securityContext.IsAdmin, + user: user, } return config, nil @@ -324,7 +331,12 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) return err } - if !settings.AllowBindMountsForRegularUsers && !config.isAdmin { + isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) + if err != nil { + return err + } + + if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers) && !isAdminOrEndpointAdmin { composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) stackContent, err := handler.FileService.GetFileContent(composeFilePath) @@ -332,13 +344,10 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) return err } - valid, err := handler.isValidStackFile(stackContent) + err = handler.isValidStackFile(stackContent, settings) if err != nil { return err } - if !valid { - return errors.New("bind-mount disabled for non administrator users") - } } handler.stackCreationMutex.Lock() diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index e28261bed..f9aac664a 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -292,6 +292,7 @@ type swarmStackDeploymentConfig struct { registries []portainer.Registry prune bool isAdmin bool + user *portainer.User } func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) { @@ -311,6 +312,11 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine } filteredRegistries := security.FilterRegistries(registries, securityContext) + user, err := handler.DataStore.User().User(securityContext.UserID) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err} + } + config := &swarmStackDeploymentConfig{ stack: stack, endpoint: endpoint, @@ -318,6 +324,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine registries: filteredRegistries, prune: prune, isAdmin: securityContext.IsAdmin, + user: user, } return config, nil @@ -329,7 +336,12 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err return err } - if !settings.AllowBindMountsForRegularUsers && !config.isAdmin { + isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) + if err != nil { + return err + } + + if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin { composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) stackContent, err := handler.FileService.GetFileContent(composeFilePath) @@ -337,13 +349,10 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err return err } - valid, err := handler.isValidStackFile(stackContent) + err = handler.isValidStackFile(stackContent, settings) if err != nil { return err } - if !valid { - return errors.New("bind-mount disabled for non administrator users") - } } handler.stackCreationMutex.Lock() diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index e560392c3..2be3ddc70 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -89,3 +89,23 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR } return false, nil } + +func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) { + isAdmin := user.Role == portainer.AdministratorRole + if isAdmin { + return true, nil + } + + rbacExtension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) + if err != nil && err != bolterrors.ErrObjectNotFound { + return false, errors.New("Unable to verify if RBAC extension is loaded") + } + + if rbacExtension == nil { + return false, nil + } + + _, endpointResourceAccess := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess] + + return endpointResourceAccess, nil +} diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 87f358abd..a785ff8a2 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -106,10 +106,10 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)} } -func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) { +func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error { composeConfigYAML, err := loader.ParseYAML(stackFileContent) if err != nil { - return false, err + return err } composeConfigFile := types.ConfigFile{ @@ -126,19 +126,25 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) options.SkipInterpolation = true }) if err != nil { - return false, err + return err } for key := range composeConfig.Services { service := composeConfig.Services[key] - for _, volume := range service.Volumes { - if volume.Type == "bind" { - return false, nil + if !settings.AllowBindMountsForRegularUsers { + for _, volume := range service.Volumes { + if volume.Type == "bind" { + return errors.New("bind-mount disabled for non administrator users") + } } } + + if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { + return errors.New("privileged mode disabled for non administrator users") + } } - return true, nil + return nil } func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError { diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index dfda0b04a..519282be4 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -1,12 +1,18 @@ package docker import ( + "bytes" "context" + "encoding/json" + "errors" + "io/ioutil" "net/http" "github.com/docker/docker/client" portainer "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" ) @@ -148,3 +154,69 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB return false } + +func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { + type PartialContainer struct { + HostConfig struct { + Privileged bool `json:"Privileged"` + } `json:"HostConfig"` + } + + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + user, err := transport.dataStore.User().User(tokenData.ID) + if err != nil { + return nil, err + } + + rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension) + if err != nil && err != bolterrors.ErrObjectNotFound { + return nil, err + } + + endpointResourceAccess := false + _, ok := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess] + if ok { + endpointResourceAccess = true + } + + if (rbacExtension != nil && !endpointResourceAccess && tokenData.Role != portainer.AdministratorRole) || (rbacExtension == nil && tokenData.Role != portainer.AdministratorRole) { + settings, err := transport.dataStore.Settings().Settings() + if err != nil { + return nil, err + } + + if !settings.AllowPrivilegedModeForRegularUsers { + body, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, err + } + + partialContainer := &PartialContainer{} + err = json.Unmarshal(body, partialContainer) + if err != nil { + return nil, err + } + + if partialContainer.HostConfig.Privileged { + return nil, errors.New("forbidden to use privileged mode") + } + + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + } + } + + response, err := transport.executeDockerRequest(request) + if err != nil { + return response, err + } + + if response.StatusCode == http.StatusCreated { + err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID) + } + + return response, err +} diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 5be06eb0b..6ecfb4615 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -189,7 +189,7 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) { switch requestPath := request.URL.Path; requestPath { case "/containers/create": - return transport.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl) + return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl) case "/containers/prune": return transport.administratorOperation(request) @@ -629,6 +629,7 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) ( return nil, err } + accessContext := ®istryAccessContext{ isAdmin: true, userID: tokenData.ID, From 4534ccb499d5cebc992148f98cbd85085ec44a1e Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 22 Jul 2020 21:41:07 +0300 Subject: [PATCH 075/195] fix(templates): replace templates links (#4083) --- .../components/custom-templates-list/customTemplatesList.html | 4 ++-- .../components/template-list/template-list-controller.js | 2 +- .../create-custom-template-view/createCustomTemplateView.html | 2 +- .../createCustomTemplateViewController.js | 2 +- .../custom-templates-view/customTemplatesView.html | 2 +- .../custom-templates-view/customTemplatesViewController.js | 2 +- .../edit-custom-template-view/editCustomTemplateView.html | 4 ++-- .../editCustomTemplateViewController.js | 2 +- app/portainer/views/stacks/create/createstack.html | 2 +- app/portainer/views/stacks/edit/stack.html | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/portainer/components/custom-templates-list/customTemplatesList.html b/app/portainer/components/custom-templates-list/customTemplatesList.html index 40ff84d42..9b160717c 100644 --- a/app/portainer/components/custom-templates-list/customTemplatesList.html +++ b/app/portainer/components/custom-templates-list/customTemplatesList.html @@ -5,7 +5,7 @@
{{ $ctrl.titleText }}
-
@@ -32,7 +32,7 @@ >
- + Edit diff --git a/app/portainer/components/template-list/template-list-controller.js b/app/portainer/components/template-list/template-list-controller.js index 58213d940..1b95ab2df 100644 --- a/app/portainer/components/template-list/template-list-controller.js +++ b/app/portainer/components/template-list/template-list-controller.js @@ -60,7 +60,7 @@ function TemplateListController($async, $state, DatatableService, Notifications, if (template.Type === 3) { type = 2; } - $state.go('portainer.templates.custom.new', { fileContent, type }); + $state.go('docker.templates.custom.new', { fileContent, type }); } catch (err) { Notifications.error('Failure', err, 'Failed to duplicate template'); } diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html index 99e33f5da..fa06c6535 100644 --- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html +++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html @@ -1,6 +1,6 @@ - Custom Templates > Create Custom template + Custom Templates > Create Custom template
diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js index ca1b10bb9..bfa4e9893 100644 --- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js +++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateViewController.js @@ -71,7 +71,7 @@ class CreateCustomTemplateViewController { await this.ResourceControlService.applyResourceControl(userId, accessControlData, ResourceControl); this.Notifications.success('Custom template successfully created'); - this.$state.go('portainer.templates.custom'); + this.$state.go('docker.templates.custom'); } catch (err) { this.Notifications.error('Deployment error', err, 'Unable to create custom template'); } finally { diff --git a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html index 2ceda91c4..71120c002 100644 --- a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html +++ b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesView.html @@ -1,6 +1,6 @@ - + diff --git a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js index a4e889fc2..ff456add3 100644 --- a/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js +++ b/app/portainer/views/custom-templates/custom-templates-view/customTemplatesViewController.js @@ -140,7 +140,7 @@ class CustomTemplatesViewController { const { ResourceControl: resourceControl } = await createAction(stackName, file, [], endpointId); await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); this.Notifications.success('Stack successfully deployed'); - this.$state.go('portainer.stacks'); + this.$state.go('docker.stacks'); } catch (err) { this.Notifications.error('Deployment error', err, 'Failed to deploy stack'); } finally { diff --git a/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html b/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html index f81badde6..4ffcbd4fa 100644 --- a/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html +++ b/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateView.html @@ -1,10 +1,10 @@ - + - Custom templates > {{ $ctrl.formValues.Title }} + Custom templates > {{ $ctrl.formValues.Title }}
diff --git a/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js b/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js index fd2353782..f06874b40 100644 --- a/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js +++ b/app/portainer/views/custom-templates/edit-custom-template-view/editCustomTemplateViewController.js @@ -73,7 +73,7 @@ class EditCustomTemplateViewController { await this.ResourceControlService.applyResourceControl(userId, this.formValues.AccessControlData, this.formValues.ResourceControl); this.Notifications.success('Custom template successfully updated'); - this.$state.go('portainer.templates.custom'); + this.$state.go('docker.templates.custom'); } catch (err) { this.Notifications.error('Failure', err, 'Unable to update custom template'); } finally { diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html index 9f8c5a098..09ebb8b05 100644 --- a/app/portainer/views/stacks/create/createstack.html +++ b/app/portainer/views/stacks/create/createstack.html @@ -209,7 +209,7 @@ - No custom template are available. Head over the custom template view to create one. + No custom template are available. Head over the custom template view to create one.
diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html index a00a408ba..7bc2c041c 100644 --- a/app/portainer/views/stacks/edit/stack.html +++ b/app/portainer/views/stacks/edit/stack.html @@ -42,7 +42,7 @@ Create template from stack From b50497301d2d378ae6bb5ca1b14531f665291d8f Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 23 Jul 2020 10:43:12 +0300 Subject: [PATCH 076/195] refactor(agent): refactor files-datatable to es6 (#4085) * refactor(host): rename files datatable * feat(agent): rename main file --- .../{files-datatable.html => filesDatatable.html} | 0 .../files-datatable/{files-datatable.js => index.js} | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) rename app/agent/components/files-datatable/{files-datatable.html => filesDatatable.html} (100%) rename app/agent/components/files-datatable/{files-datatable.js => index.js} (85%) diff --git a/app/agent/components/files-datatable/files-datatable.html b/app/agent/components/files-datatable/filesDatatable.html similarity index 100% rename from app/agent/components/files-datatable/files-datatable.html rename to app/agent/components/files-datatable/filesDatatable.html diff --git a/app/agent/components/files-datatable/files-datatable.js b/app/agent/components/files-datatable/index.js similarity index 85% rename from app/agent/components/files-datatable/files-datatable.js rename to app/agent/components/files-datatable/index.js index d75d0f970..a3ba1bb7e 100644 --- a/app/agent/components/files-datatable/files-datatable.js +++ b/app/agent/components/files-datatable/index.js @@ -1,5 +1,7 @@ +import angular from 'angular'; + angular.module('portainer.agent').component('filesDatatable', { - templateUrl: './files-datatable.html', + templateUrl: './filesDatatable.html', controller: 'GenericDatatableController', bindings: { titleText: '@', From 5abd35d4c1bb6adc902775881347e7e5836bb6c8 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 23 Jul 2020 10:43:37 +0300 Subject: [PATCH 077/195] refactor(agent): refactor pingService to es6 (#4093) related to #4071 --- app/agent/services/pingService.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/agent/services/pingService.js b/app/agent/services/pingService.js index cc3133f22..bd6380912 100644 --- a/app/agent/services/pingService.js +++ b/app/agent/services/pingService.js @@ -1,14 +1,11 @@ -angular.module('portainer.agent').service('AgentPingService', [ - 'AgentPing', - function AgentPingService(AgentPing) { - var service = {}; +import angular from 'angular'; - service.ping = ping; +angular.module('portainer.agent').service('AgentPingService', AgentPingService); - function ping() { - return AgentPing.ping().$promise; - } +function AgentPingService(AgentPing) { + return { ping }; - return service; - }, -]); + function ping() { + return AgentPing.ping().$promise; + } +} From 435f15ec6a6cb696444f2b7f121fbd8415b26798 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 23 Jul 2020 10:44:32 +0300 Subject: [PATCH 078/195] refactor(agent): refactor file-uploader to es6 (#4087) * refactor(host): convert fileUploader to es6 * refactor(agent): rename main file --- .../file-uploader/file-uploader-controller.js | 23 --------------- .../components/file-uploader/file-uploader.js | 7 ----- .../{file-uploader.html => fileUploader.html} | 0 .../file-uploader/fileUploaderController.js | 29 +++++++++++++++++++ app/agent/components/file-uploader/index.js | 10 +++++++ 5 files changed, 39 insertions(+), 30 deletions(-) delete mode 100644 app/agent/components/file-uploader/file-uploader-controller.js delete mode 100644 app/agent/components/file-uploader/file-uploader.js rename app/agent/components/file-uploader/{file-uploader.html => fileUploader.html} (100%) create mode 100644 app/agent/components/file-uploader/fileUploaderController.js create mode 100644 app/agent/components/file-uploader/index.js diff --git a/app/agent/components/file-uploader/file-uploader-controller.js b/app/agent/components/file-uploader/file-uploader-controller.js deleted file mode 100644 index d0c5ad798..000000000 --- a/app/agent/components/file-uploader/file-uploader-controller.js +++ /dev/null @@ -1,23 +0,0 @@ -angular.module('portainer.agent').controller('FileUploaderController', [ - '$q', - function FileUploaderController($q) { - var ctrl = this; - - ctrl.state = { - uploadInProgress: false, - }; - - ctrl.onFileSelected = onFileSelected; - - function onFileSelected(file) { - if (!file) { - return; - } - - ctrl.state.uploadInProgress = true; - $q.when(ctrl.uploadFile(file)).finally(function toggleProgress() { - ctrl.state.uploadInProgress = false; - }); - } - }, -]); diff --git a/app/agent/components/file-uploader/file-uploader.js b/app/agent/components/file-uploader/file-uploader.js deleted file mode 100644 index 6232c7f8d..000000000 --- a/app/agent/components/file-uploader/file-uploader.js +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('portainer.agent').component('fileUploader', { - templateUrl: './file-uploader.html', - controller: 'FileUploaderController', - bindings: { - uploadFile: ' Date: Thu, 23 Jul 2020 10:45:01 +0300 Subject: [PATCH 079/195] refactor(agent): refactor rest factories to es6 (#4090) * refactor(agent): replace v1 browse with es6 module * refactor(agent): refactor agentv1 to es6 * refactor(agent): replace agent factory with es6 * refactor(agent): refactor browse response to es6 * refactor(agent): refactor browse to es6 * refactor(agent): import angular * refactor(agent): refactor host to es6 * refactor(agent): refactor ping to es6 --- app/agent/rest/agent.js | 35 +++++++--------- app/agent/rest/browse.js | 69 +++++++++++++++---------------- app/agent/rest/host.js | 35 +++++++--------- app/agent/rest/ping.js | 63 ++++++++++++++-------------- app/agent/rest/response/browse.js | 2 +- app/agent/rest/v1/agent.js | 32 +++++++------- app/agent/rest/v1/browse.js | 66 ++++++++++++++--------------- 7 files changed, 143 insertions(+), 159 deletions(-) diff --git a/app/agent/rest/agent.js b/app/agent/rest/agent.js index d7d0c6a2f..00267ce00 100644 --- a/app/agent/rest/agent.js +++ b/app/agent/rest/agent.js @@ -1,19 +1,16 @@ -angular.module('portainer.agent').factory('Agent', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - 'StateManager', - function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/agents', - { - endpointId: EndpointProvider.endpointID, - version: StateManager.getAgentApiVersion, - }, - { - query: { method: 'GET', isArray: true }, - } - ); - }, -]); +import angular from 'angular'; + +angular.module('portainer.agent').factory('Agent', AgentFactory); + +function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { + return $resource( + `${API_ENDPOINT_ENDPOINTS}/:endpointId/docker/v:version/agents`, + { + endpointId: EndpointProvider.endpointID, + version: StateManager.getAgentApiVersion, + }, + { + query: { method: 'GET', isArray: true }, + } + ); +} diff --git a/app/agent/rest/browse.js b/app/agent/rest/browse.js index 852ee0535..66f8329d5 100644 --- a/app/agent/rest/browse.js +++ b/app/agent/rest/browse.js @@ -1,39 +1,36 @@ +import angular from 'angular'; + import { browseGetResponse } from './response/browse'; -angular.module('portainer.agent').factory('Browse', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - 'StateManager', - function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/browse/:action', - { - endpointId: EndpointProvider.endpointID, - version: StateManager.getAgentApiVersion, +angular.module('portainer.agent').factory('Browse', BrowseFactory); + +function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { + return $resource( + `${API_ENDPOINT_ENDPOINTS}/:endpointId/docker/v:version/browse/:action`, + { + endpointId: EndpointProvider.endpointID, + version: StateManager.getAgentApiVersion, + }, + { + ls: { + method: 'GET', + isArray: true, + params: { action: 'ls' }, }, - { - ls: { - method: 'GET', - isArray: true, - params: { action: 'ls' }, - }, - get: { - method: 'GET', - params: { action: 'get' }, - transformResponse: browseGetResponse, - responseType: 'arraybuffer', - }, - delete: { - method: 'DELETE', - params: { action: 'delete' }, - }, - rename: { - method: 'PUT', - params: { action: 'rename' }, - }, - } - ); - }, -]); + get: { + method: 'GET', + params: { action: 'get' }, + transformResponse: browseGetResponse, + responseType: 'arraybuffer', + }, + delete: { + method: 'DELETE', + params: { action: 'delete' }, + }, + rename: { + method: 'PUT', + params: { action: 'rename' }, + }, + } + ); +} diff --git a/app/agent/rest/host.js b/app/agent/rest/host.js index a5fb198ae..cd79b9763 100644 --- a/app/agent/rest/host.js +++ b/app/agent/rest/host.js @@ -1,19 +1,16 @@ -angular.module('portainer.agent').factory('Host', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - 'StateManager', - function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/host/:action', - { - endpointId: EndpointProvider.endpointID, - version: StateManager.getAgentApiVersion, - }, - { - info: { method: 'GET', params: { action: 'info' } }, - } - ); - }, -]); +import angular from 'angular'; + +angular.module('portainer.agent').factory('Host', HostFactory); + +function HostFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { + return $resource( + `${API_ENDPOINT_ENDPOINTS}/:endpointId/docker/v:version/host/:action`, + { + endpointId: EndpointProvider.endpointID, + version: StateManager.getAgentApiVersion, + }, + { + info: { method: 'GET', params: { action: 'info' } }, + } + ); +} diff --git a/app/agent/rest/ping.js b/app/agent/rest/ping.js index b6527dfb4..8726cd8d6 100644 --- a/app/agent/rest/ping.js +++ b/app/agent/rest/ping.js @@ -1,35 +1,32 @@ -angular.module('portainer.agent').factory('AgentPing', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - '$q', - function AgentPingFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/ping', - { - endpointId: EndpointProvider.endpointID, - }, - { - ping: { - method: 'GET', - interceptor: { - response: function versionInterceptor(response) { - var instance = response.resource; - var version = response.headers('Portainer-Agent-Api-Version') || 1; - instance.version = Number(version); - return instance; - }, - responseError: function versionResponseError(error) { - // 404 - agent is up - set version to 1 - if (error.status === 404) { - return { version: 1 }; - } - return $q.reject(error); - }, +import angular from 'angular'; + +angular.module('portainer.agent').factory('AgentPing', AgentPingFactory); + +function AgentPingFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) { + return $resource( + `${API_ENDPOINT_ENDPOINTS}/:endpointId/docker/ping`, + { + endpointId: EndpointProvider.endpointID, + }, + { + ping: { + method: 'GET', + interceptor: { + response: function versionInterceptor(response) { + const instance = response.resource; + const version = response.headers('Portainer-Agent-Api-Version') || 1; + instance.version = Number(version); + return instance; + }, + responseError: function versionResponseError(error) { + // 404 - agent is up - set version to 1 + if (error.status === 404) { + return { version: 1 }; + } + return $q.reject(error); }, }, - } - ); - }, -]); + }, + } + ); +} diff --git a/app/agent/rest/response/browse.js b/app/agent/rest/response/browse.js index 32e454305..b88f3212c 100644 --- a/app/agent/rest/response/browse.js +++ b/app/agent/rest/response/browse.js @@ -3,7 +3,7 @@ // This functions simply creates a response object and assign // the data to a field. export function browseGetResponse(data) { - var response = {}; + const response = {}; response.file = data; return response; } diff --git a/app/agent/rest/v1/agent.js b/app/agent/rest/v1/agent.js index 3d9e8d606..d42a3fb44 100644 --- a/app/agent/rest/v1/agent.js +++ b/app/agent/rest/v1/agent.js @@ -1,17 +1,15 @@ -angular.module('portainer.agent').factory('AgentVersion1', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents', - { - endpointId: EndpointProvider.endpointID, - }, - { - query: { method: 'GET', isArray: true }, - } - ); - }, -]); +import angular from 'angular'; + +angular.module('portainer.agent').factory('AgentVersion1', AgentFactory); + +function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + return $resource( + `${API_ENDPOINT_ENDPOINTS}/:endpointId/docker/agents`, + { + endpointId: EndpointProvider.endpointID, + }, + { + query: { method: 'GET', isArray: true }, + } + ); +} diff --git a/app/agent/rest/v1/browse.js b/app/agent/rest/v1/browse.js index 89c18b384..cfc06128c 100644 --- a/app/agent/rest/v1/browse.js +++ b/app/agent/rest/v1/browse.js @@ -1,37 +1,35 @@ +import angular from 'angular'; + import { browseGetResponse } from '../response/browse'; -angular.module('portainer.agent').factory('BrowseVersion1', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return $resource( - API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:volumeID/:action', - { - endpointId: EndpointProvider.endpointID, +angular.module('portainer.agent').factory('BrowseVersion1', BrowseFactory); + +function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + return $resource( + `${API_ENDPOINT_ENDPOINTS}/:endpointId/docker/browse/:volumeID/:action`, + { + endpointId: EndpointProvider.endpointID, + }, + { + ls: { + method: 'GET', + isArray: true, + params: { action: 'ls' }, }, - { - ls: { - method: 'GET', - isArray: true, - params: { action: 'ls' }, - }, - get: { - method: 'GET', - params: { action: 'get' }, - transformResponse: browseGetResponse, - responseType: 'arraybuffer', - }, - delete: { - method: 'DELETE', - params: { action: 'delete' }, - }, - rename: { - method: 'PUT', - params: { action: 'rename' }, - }, - } - ); - }, -]); + get: { + method: 'GET', + params: { action: 'get' }, + transformResponse: browseGetResponse, + responseType: 'arraybuffer', + }, + delete: { + method: 'DELETE', + params: { action: 'delete' }, + }, + rename: { + method: 'PUT', + params: { action: 'rename' }, + }, + } + ); +} From a473d738be5a6bc646420b8f7601dd88cb108fec Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 23 Jul 2020 10:45:12 +0300 Subject: [PATCH 080/195] refactor(agent): refactor volume browser to es6 (#4086) * refactor(agent): replace root with index * refactor(agent): use simple export * refactor(agent): replace function with class * refactor(agent): replace promise with async --- .../{volume-browser.js => index.js} | 6 +- .../volume-browser/volumeBrowser.html | 2 +- .../volume-browser/volumeBrowserController.js | 251 ++++++++++-------- 3 files changed, 139 insertions(+), 120 deletions(-) rename app/agent/components/volume-browser/{volume-browser.js => index.js} (57%) diff --git a/app/agent/components/volume-browser/volume-browser.js b/app/agent/components/volume-browser/index.js similarity index 57% rename from app/agent/components/volume-browser/volume-browser.js rename to app/agent/components/volume-browser/index.js index 5c2b2b78d..e52633a51 100644 --- a/app/agent/components/volume-browser/volume-browser.js +++ b/app/agent/components/volume-browser/index.js @@ -1,6 +1,10 @@ +import angular from 'angular'; + +import { VolumeBrowserController } from './volumeBrowserController'; + angular.module('portainer.agent').component('volumeBrowser', { templateUrl: './volumeBrowser.html', - controller: 'VolumeBrowserController', + controller: VolumeBrowserController, bindings: { volumeId: '<', nodeName: '<', diff --git a/app/agent/components/volume-browser/volumeBrowser.html b/app/agent/components/volume-browser/volumeBrowser.html index 5b85cfd94..c94a285bb 100644 --- a/app/agent/components/volume-browser/volumeBrowser.html +++ b/app/agent/components/volume-browser/volumeBrowser.html @@ -9,7 +9,7 @@ browse="$ctrl.browse(name)" rename="$ctrl.rename(name, newName)" download="$ctrl.download(name)" - delete="$ctrl.delete(name)" + delete="$ctrl.confirmDelete(name)" is-upload-allowed="$ctrl.isUploadEnabled" on-file-selected-for-upload="($ctrl.onFileSelectedForUpload)" > diff --git a/app/agent/components/volume-browser/volumeBrowserController.js b/app/agent/components/volume-browser/volumeBrowserController.js index fccde7753..faa3fc7e5 100644 --- a/app/agent/components/volume-browser/volumeBrowserController.js +++ b/app/agent/components/volume-browser/volumeBrowserController.js @@ -1,137 +1,152 @@ import _ from 'lodash-es'; -angular.module('portainer.agent').controller('VolumeBrowserController', [ - 'HttpRequestHelper', - 'VolumeBrowserService', - 'FileSaver', - 'Blob', - 'ModalService', - 'Notifications', - function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) { - var ctrl = this; - +export class VolumeBrowserController { + constructor($async, HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) { + Object.assign(this, { $async, HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications }); this.state = { path: '/', }; - this.rename = function (file, newName) { - var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; - var newFilePath = this.state.path === '/' ? newName : this.state.path + '/' + newName; + this.rename = this.rename.bind(this); + this.renameAsync = this.renameAsync.bind(this); + this.confirmDelete = this.confirmDelete.bind(this); + this.download = this.download.bind(this); + this.downloadAsync = this.downloadAsync.bind(this); + this.up = this.up.bind(this); + this.browse = this.browse.bind(this); + this.deleteFile = this.deleteFile.bind(this); + this.deleteFileAsync = this.deleteFileAsync.bind(this); + this.getFilesForPath = this.getFilesForPath.bind(this); + this.getFilesForPathAsync = this.getFilesForPathAsync.bind(this); + this.onFileSelectedForUpload = this.onFileSelectedForUpload.bind(this); + this.onFileSelectedForUploadAsync = this.onFileSelectedForUploadAsync.bind(this); + this.parentPath = this.parentPath.bind(this); + this.buildPath = this.buildPath.bind(this); + this.$onInit = this.$onInit.bind(this); + this.onFileUploaded = this.onFileUploaded.bind(this); + this.refreshList = this.refreshList.bind(this); + } - VolumeBrowserService.rename(this.volumeId, filePath, newFilePath) - .then(function success() { - Notifications.success('File successfully renamed', newFilePath); - return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path); - }) - .then(function success(data) { - ctrl.files = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to rename file'); - }); - }; + rename(file, newName) { + return this.$async(this.renameAsync, file, newName); + } + async renameAsync(file, newName) { + const filePath = this.state.path === '/' ? file : `${this.state.path}/${file}`; + const newFilePath = this.state.path === '/' ? newName : `${this.state.path}/${newName}`; - this.delete = function (file) { - var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; - - ModalService.confirmDeletion('Are you sure that you want to delete ' + filePath + ' ?', function onConfirm(confirmed) { - if (!confirmed) { - return; - } - deleteFile(filePath); - }); - }; - - this.download = function (file) { - var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; - VolumeBrowserService.get(this.volumeId, filePath) - .then(function success(data) { - var downloadData = new Blob([data.file]); - FileSaver.saveAs(downloadData, file); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to download file'); - }); - }; - - this.up = function () { - var parentFolder = parentPath(this.state.path); - browse(parentFolder); - }; - - this.browse = function (folder) { - var path = buildPath(this.state.path, folder); - browse(path); - }; - - function deleteFile(file) { - VolumeBrowserService.delete(ctrl.volumeId, file) - .then(function success() { - Notifications.success('File successfully deleted', file); - return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path); - }) - .then(function success(data) { - ctrl.files = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to delete file'); - }); + try { + await this.VolumeBrowserService.rename(this.volumeId, filePath, newFilePath); + this.Notifications.success('File successfully renamed', newFilePath); + this.files = await this.VolumeBrowserService.ls(this.volumeId, this.state.path); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to rename file'); } + } - function browse(path) { - VolumeBrowserService.ls(ctrl.volumeId, path) - .then(function success(data) { - ctrl.state.path = path; - ctrl.files = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to browse volume'); - }); - } + confirmDelete(file) { + const filePath = this.state.path === '/' ? file : `${this.state.path}/${file}`; - this.onFileSelectedForUpload = function onFileSelectedForUpload(file) { - VolumeBrowserService.upload(ctrl.state.path, file, ctrl.volumeId) - .then(function onFileUpload() { - onFileUploaded(); - }) - .catch(function onFileUpload(err) { - Notifications.error('Failure', err, 'Unable to upload file'); - }); - }; - - function parentPath(path) { - if (path.lastIndexOf('/') === 0) { - return '/'; + this.ModalService.confirmDeletion(`Are you sure that you want to delete ${filePath} ?`, (confirmed) => { + if (!confirmed) { + return; } + this.deleteFile(filePath); + }); + } - var split = _.split(path, '/'); - return _.join(_.slice(split, 0, split.length - 1), '/'); + download(file) { + return this.$async(this.downloadAsync, file); + } + async downloadAsync(file) { + const filePath = this.state.path === '/' ? file : `${this.state.path}/${file}`; + + try { + const data = await this.VolumeBrowserService.get(this.volumeId, filePath); + const downloadData = new Blob([data.file]); + this.FileSaver.saveAs(downloadData, file); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to download file'); + } + } + + up() { + const parentFolder = this.parentPath(this.state.path); + this.getFilesForPath(parentFolder); + } + + browse(folder) { + const path = this.buildPath(this.state.path, folder); + this.getFilesForPath(path); + } + + deleteFile(file) { + return this.$async(this.deleteFileAsync, file); + } + async deleteFileAsync(file) { + try { + await this.VolumeBrowserService.delete(this.volumeId, file); + this.Notifications.success('File successfully deleted', file); + this.files = await this.VolumeBrowserService.ls(this.volumeId, this.state.path); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to delete file'); + } + } + + getFilesForPath(path) { + return this.$async(this.getFilesForPathAsync, path); + } + async getFilesForPathAsync(path) { + try { + const files = await this.VolumeBrowserService.ls(this.volumeId, path); + this.state.path = path; + this.files = files; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to browse volume'); + } + } + + onFileSelectedForUpload(file) { + return this.$async(this.onFileSelectedForUploadAsync, file); + } + async onFileSelectedForUploadAsync(file) { + try { + await this.VolumeBrowserService.upload(this.state.path, file, this.volumeId); + this.onFileUploaded(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to upload file'); + } + } + + parentPath(path) { + if (path.lastIndexOf('/') === 0) { + return '/'; } - function buildPath(parent, file) { - if (parent === '/') { - return parent + file; - } - return parent + '/' + file; - } + const split = _.split(path, '/'); + return _.join(_.slice(split, 0, split.length - 1), '/'); + } - this.$onInit = function () { - HttpRequestHelper.setPortainerAgentTargetHeader(this.nodeName); - VolumeBrowserService.ls(this.volumeId, this.state.path) - .then(function success(data) { - ctrl.files = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to browse volume'); - }); - }; - - function onFileUploaded() { - refreshList(); + buildPath(parent, file) { + if (parent === '/') { + return parent + file; } + return `${parent}/${file}`; + } - function refreshList() { - browse(ctrl.state.path); + onFileUploaded() { + this.refreshList(); + } + + refreshList() { + this.getFilesForPath(this.state.path); + } + + async $onInit() { + this.HttpRequestHelper.setPortainerAgentTargetHeader(this.nodeName); + try { + this.files = await this.VolumeBrowserService.ls(this.volumeId, this.state.path); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to browse volume'); } - }, -]); + } +} From 1ef7347f19e3eda8a64c0a051f37754eeb6ebae7 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 23 Jul 2020 10:45:23 +0300 Subject: [PATCH 081/195] refactor(agent): refactor host-broswer to es6 (#4088) * refactor(host): replace host-browser with es6 class * refactor(host): replace promises with async * refactor(hosts): replace delete promise with async * refactor(host): replace upload file with async * refactor(host): replace template strings * fix(host): replace host root * feat(agent): rename main file --- .../host-browser/host-browser-controller.js | 149 ----------------- .../components/host-browser/host-browser.js | 5 - .../{host-browser.html => hostBrowser.html} | 2 +- .../host-browser/hostBrowserController.js | 158 ++++++++++++++++++ app/agent/components/host-browser/index.js | 7 + 5 files changed, 166 insertions(+), 155 deletions(-) delete mode 100644 app/agent/components/host-browser/host-browser-controller.js delete mode 100644 app/agent/components/host-browser/host-browser.js rename app/agent/components/host-browser/{host-browser.html => hostBrowser.html} (91%) create mode 100644 app/agent/components/host-browser/hostBrowserController.js create mode 100644 app/agent/components/host-browser/index.js diff --git a/app/agent/components/host-browser/host-browser-controller.js b/app/agent/components/host-browser/host-browser-controller.js deleted file mode 100644 index c9c1e5e0f..000000000 --- a/app/agent/components/host-browser/host-browser-controller.js +++ /dev/null @@ -1,149 +0,0 @@ -import _ from 'lodash-es'; - -angular.module('portainer.agent').controller('HostBrowserController', [ - 'HostBrowserService', - 'Notifications', - 'FileSaver', - 'ModalService', - function HostBrowserController(HostBrowserService, Notifications, FileSaver, ModalService) { - var ctrl = this; - var ROOT_PATH = '/host'; - ctrl.state = { - path: ROOT_PATH, - }; - - ctrl.goToParent = goToParent; - ctrl.browse = browse; - ctrl.renameFile = renameFile; - ctrl.downloadFile = downloadFile; - ctrl.deleteFile = confirmDeleteFile; - ctrl.isRoot = isRoot; - ctrl.onFileSelectedForUpload = onFileSelectedForUpload; - ctrl.$onInit = $onInit; - ctrl.getRelativePath = getRelativePath; - - function getRelativePath(path) { - path = path || ctrl.state.path; - var rootPathRegex = new RegExp('^' + ROOT_PATH + '/?'); - var relativePath = path.replace(rootPathRegex, '/'); - return relativePath; - } - - function goToParent() { - getFilesForPath(parentPath(this.state.path)); - } - - function isRoot() { - return ctrl.state.path === ROOT_PATH; - } - - function browse(folder) { - getFilesForPath(buildPath(ctrl.state.path, folder)); - } - - function getFilesForPath(path) { - HostBrowserService.ls(path) - .then(function onFilesLoaded(files) { - ctrl.state.path = path; - ctrl.files = files; - }) - .catch(function onLoadingFailed(err) { - Notifications.error('Failure', err, 'Unable to browse'); - }); - } - - function renameFile(name, newName) { - var filePath = buildPath(ctrl.state.path, name); - var newFilePath = buildPath(ctrl.state.path, newName); - - HostBrowserService.rename(filePath, newFilePath) - .then(function onRenameSuccess() { - Notifications.success('File successfully renamed', getRelativePath(newFilePath)); - return HostBrowserService.ls(ctrl.state.path); - }) - .then(function onFilesLoaded(files) { - ctrl.files = files; - }) - .catch(function notifyOnError(err) { - Notifications.error('Failure', err, 'Unable to rename file'); - }); - } - - function downloadFile(file) { - var filePath = buildPath(ctrl.state.path, file); - HostBrowserService.get(filePath) - .then(function onFileReceived(data) { - var downloadData = new Blob([data.file], { - type: 'text/plain;charset=utf-8', - }); - FileSaver.saveAs(downloadData, file); - }) - .catch(function notifyOnError(err) { - Notifications.error('Failure', err, 'Unable to download file'); - }); - } - - function confirmDeleteFile(name) { - var filePath = buildPath(ctrl.state.path, name); - - ModalService.confirmDeletion('Are you sure that you want to delete ' + getRelativePath(filePath) + ' ?', function onConfirm(confirmed) { - if (!confirmed) { - return; - } - return deleteFile(filePath); - }); - } - - function deleteFile(path) { - HostBrowserService.delete(path) - .then(function onDeleteSuccess() { - Notifications.success('File successfully deleted', getRelativePath(path)); - return HostBrowserService.ls(ctrl.state.path); - }) - .then(function onFilesLoaded(data) { - ctrl.files = data; - }) - .catch(function notifyOnError(err) { - Notifications.error('Failure', err, 'Unable to delete file'); - }); - } - - function $onInit() { - getFilesForPath(ROOT_PATH); - } - - function parentPath(path) { - if (path === ROOT_PATH) { - return ROOT_PATH; - } - - var split = _.split(path, '/'); - return _.join(_.slice(split, 0, split.length - 1), '/'); - } - - function buildPath(parent, file) { - if (parent.lastIndexOf('/') === parent.length - 1) { - return parent + file; - } - return parent + '/' + file; - } - - function onFileSelectedForUpload(file) { - HostBrowserService.upload(ctrl.state.path, file) - .then(function onFileUpload() { - onFileUploaded(); - }) - .catch(function onFileUpload(err) { - Notifications.error('Failure', err, 'Unable to upload file'); - }); - } - - function onFileUploaded() { - refreshList(); - } - - function refreshList() { - getFilesForPath(ctrl.state.path); - } - }, -]); diff --git a/app/agent/components/host-browser/host-browser.js b/app/agent/components/host-browser/host-browser.js deleted file mode 100644 index 9b16b5a01..000000000 --- a/app/agent/components/host-browser/host-browser.js +++ /dev/null @@ -1,5 +0,0 @@ -angular.module('portainer.agent').component('hostBrowser', { - controller: 'HostBrowserController', - templateUrl: './host-browser.html', - bindings: {}, -}); diff --git a/app/agent/components/host-browser/host-browser.html b/app/agent/components/host-browser/hostBrowser.html similarity index 91% rename from app/agent/components/host-browser/host-browser.html rename to app/agent/components/host-browser/hostBrowser.html index b81ecc744..915131118 100644 --- a/app/agent/components/host-browser/host-browser.html +++ b/app/agent/components/host-browser/hostBrowser.html @@ -9,7 +9,7 @@ browse="$ctrl.browse(name)" rename="$ctrl.renameFile(name, newName)" download="$ctrl.downloadFile(name)" - delete="$ctrl.deleteFile(name)" + delete="$ctrl.confirmDeleteFile(name)" is-upload-allowed="true" on-file-selected-for-upload="($ctrl.onFileSelectedForUpload)" > diff --git a/app/agent/components/host-browser/hostBrowserController.js b/app/agent/components/host-browser/hostBrowserController.js new file mode 100644 index 000000000..0f6cd7a34 --- /dev/null +++ b/app/agent/components/host-browser/hostBrowserController.js @@ -0,0 +1,158 @@ +import _ from 'lodash-es'; + +const ROOT_PATH = '/host'; + +export class HostBrowserController { + constructor($async, HostBrowserService, Notifications, FileSaver, ModalService) { + Object.assign(this, { $async, HostBrowserService, Notifications, FileSaver, ModalService }); + + this.state = { + path: ROOT_PATH, + }; + + this.goToParent = this.goToParent.bind(this); + this.browse = this.browse.bind(this); + this.confirmDeleteFile = this.confirmDeleteFile.bind(this); + this.isRoot = this.isRoot.bind(this); + this.getRelativePath = this.getRelativePath.bind(this); + this.getFilesForPath = this.getFilesForPath.bind(this); + this.getFilesForPathAsync = this.getFilesForPathAsync.bind(this); + this.downloadFile = this.downloadFile.bind(this); + this.downloadFileAsync = this.downloadFileAsync.bind(this); + this.renameFile = this.renameFile.bind(this); + this.renameFileAsync = this.renameFileAsync.bind(this); + this.deleteFile = this.deleteFile.bind(this); + this.deleteFileAsync = this.deleteFileAsync.bind(this); + this.onFileSelectedForUpload = this.onFileSelectedForUpload.bind(this); + this.onFileSelectedForUploadAsync = this.onFileSelectedForUploadAsync.bind(this); + } + + getRelativePath(path) { + path = path || this.state.path; + const rootPathRegex = new RegExp(`^${ROOT_PATH}/?`); + const relativePath = path.replace(rootPathRegex, '/'); + return relativePath; + } + + goToParent() { + this.getFilesForPath(this.parentPath(this.state.path)); + } + + isRoot() { + return this.state.path === ROOT_PATH; + } + + browse(folder) { + this.getFilesForPath(this.buildPath(this.state.path, folder)); + } + + getFilesForPath(path) { + return this.$async(this.getFilesForPathAsync, path); + } + async getFilesForPathAsync(path) { + try { + const files = await this.HostBrowserService.ls(path); + this.state.path = path; + this.files = files; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to browse'); + } + } + + renameFile(name, newName) { + return this.$async(this.renameFileAsync, name, newName); + } + async renameFileAsync(name, newName) { + const filePath = this.buildPath(this.state.path, name); + const newFilePath = this.buildPath(this.state.path, newName); + try { + await this.HostBrowserService.rename(filePath, newFilePath); + this.Notifications.success('File successfully renamed', this.getRelativePath(newFilePath)); + const files = await this.HostBrowserService.ls(this.state.path); + this.files = files; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to rename file'); + } + } + + downloadFile(fileName) { + return this.$async(this.downloadFileAsync, fileName); + } + async downloadFileAsync(fileName) { + const filePath = this.buildPath(this.state.path, fileName); + try { + const { file } = await this.HostBrowserService.get(filePath); + const downloadData = new Blob([file], { + type: 'text/plain;charset=utf-8', + }); + this.FileSaver.saveAs(downloadData, fileName); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to download file'); + } + } + + confirmDeleteFile(name) { + const filePath = this.buildPath(this.state.path, name); + + this.ModalService.confirmDeletion(`Are you sure that you want to delete ${this.getRelativePath(filePath)} ?`, (confirmed) => { + if (!confirmed) { + return; + } + return this.deleteFile(filePath); + }); + } + + deleteFile(path) { + this.$async(this.deleteFileAsync, path); + } + async deleteFileAsync(path) { + try { + await this.HostBrowserService.delete(path); + this.Notifications.success('File successfully deleted', this.getRelativePath(path)); + const files = await this.HostBrowserService.ls(this.state.path); + this.files = files; + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to delete file'); + } + } + + $onInit() { + this.getFilesForPath(ROOT_PATH); + } + + parentPath(path) { + if (path === ROOT_PATH) { + return ROOT_PATH; + } + + const split = _.split(path, '/'); + return _.join(_.slice(split, 0, split.length - 1), '/'); + } + + buildPath(parent, file) { + if (parent.lastIndexOf('/') === parent.length - 1) { + return parent + file; + } + return parent + '/' + file; + } + + onFileSelectedForUpload(file) { + return this.$async(this.onFileSelectedForUploadAsync, file); + } + async onFileSelectedForUploadAsync(file) { + try { + await this.HostBrowserService.upload(this.state.path, file); + this.onFileUploaded(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to upload file'); + } + } + + onFileUploaded() { + this.refreshList(); + } + + refreshList() { + this.getFilesForPath(this.state.path); + } +} diff --git a/app/agent/components/host-browser/index.js b/app/agent/components/host-browser/index.js new file mode 100644 index 000000000..d38643d6f --- /dev/null +++ b/app/agent/components/host-browser/index.js @@ -0,0 +1,7 @@ +import angular from 'angular'; +import { HostBrowserController } from './hostBrowserController'; + +angular.module('portainer.agent').component('hostBrowser', { + controller: HostBrowserController, + templateUrl: './hostBrowser.html', +}); From f761e65167d5e7222cd5ccd782312f889847c1e4 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 23 Jul 2020 10:45:47 +0300 Subject: [PATCH 082/195] refactor(agent): refactor agentService to es6 (#4091) --- app/agent/services/agentService.js | 75 ++++++++++++------------------ 1 file changed, 30 insertions(+), 45 deletions(-) diff --git a/app/agent/services/agentService.js b/app/agent/services/agentService.js index b9d938d94..579cc104b 100644 --- a/app/agent/services/agentService.js +++ b/app/agent/services/agentService.js @@ -1,50 +1,35 @@ +import angular from 'angular'; + import { AgentViewModel } from '../models/agent'; -angular.module('portainer.agent').factory('AgentService', [ - '$q', - 'Agent', - 'AgentVersion1', - 'HttpRequestHelper', - 'Host', - 'StateManager', - function AgentServiceFactory($q, Agent, AgentVersion1, HttpRequestHelper, Host, StateManager) { - 'use strict'; - var service = {}; +angular.module('portainer.agent').factory('AgentService', AgentServiceFactory); - service.agents = agents; - service.hostInfo = hostInfo; +function AgentServiceFactory(Agent, AgentVersion1, HttpRequestHelper, Host, StateManager) { + return { + agents, + hostInfo, + }; - function getAgentApiVersion() { - var state = StateManager.getState(); - return state.endpoint.agentApiVersion; + function getAgentApiVersion() { + const state = StateManager.getState(); + return state.endpoint.agentApiVersion; + } + + function hostInfo(nodeName) { + HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + return Host.info().$promise; + } + + async function agents() { + const agentVersion = getAgentApiVersion(); + const service = agentVersion > 1 ? Agent : AgentVersion1; + try { + const agents = await service.query({ version: agentVersion }).$promise; + return agents.map(function (item) { + return new AgentViewModel(item); + }); + } catch (err) { + throw { msg: 'Unable to retrieve agents', err }; } - - function hostInfo(nodeName) { - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); - return Host.info().$promise; - } - - function agents() { - var deferred = $q.defer(); - - var agentVersion = getAgentApiVersion(); - var service = agentVersion > 1 ? Agent : AgentVersion1; - - service - .query({ version: agentVersion }) - .$promise.then(function success(data) { - var agents = data.map(function (item) { - return new AgentViewModel(item); - }); - deferred.resolve(agents); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve agents', err: err }); - }); - - return deferred.promise; - } - - return service; - }, -]); + } +} From 822c4e117cb819496ee6033666b33e1d6e9f0085 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 23 Jul 2020 10:46:02 +0300 Subject: [PATCH 083/195] refactor(agent): refactor hostBrowserService to es6 (#4092) related to #4071 --- app/agent/services/hostBrowserService.js | 70 ++++++++++-------------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/app/agent/services/hostBrowserService.js b/app/agent/services/hostBrowserService.js index 8de01f815..fd2d5eed7 100644 --- a/app/agent/services/hostBrowserService.js +++ b/app/agent/services/hostBrowserService.js @@ -1,51 +1,39 @@ -angular.module('portainer.agent').factory('HostBrowserService', [ - 'Browse', - 'Upload', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - '$q', - 'StateManager', - function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q, StateManager) { - var service = {}; +import angular from 'angular'; - service.ls = ls; - service.get = get; - service.delete = deletePath; - service.rename = rename; - service.upload = upload; +angular.module('portainer.agent').factory('HostBrowserService', HostBrowserServiceFactory); - function ls(path) { - return Browse.ls({ path: path }).$promise; - } +function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { + return { ls, get, delete: deletePath, rename, upload }; - function get(path) { - return Browse.get({ path: path }).$promise; - } + function ls(path) { + return Browse.ls({ path: path }).$promise; + } - function deletePath(path) { - return Browse.delete({ path: path }).$promise; - } + function get(path) { + return Browse.get({ path: path }).$promise; + } - function rename(path, newPath) { - var payload = { - CurrentFilePath: path, - NewFilePath: newPath, - }; - return Browse.rename({}, payload).$promise; - } + function deletePath(path) { + return Browse.delete({ path: path }).$promise; + } - function upload(path, file, onProgress) { - var deferred = $q.defer(); - var agentVersion = StateManager.getAgentApiVersion(); - var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker' + (agentVersion > 1 ? '/v' + agentVersion : '') + '/browse/put'; + function rename(path, newPath) { + const payload = { + CurrentFilePath: path, + NewFilePath: newPath, + }; + return Browse.rename({}, payload).$promise; + } + function upload(path, file, onProgress) { + const agentVersion = StateManager.getAgentApiVersion(); + const url = `${API_ENDPOINT_ENDPOINTS}/${EndpointProvider.endpointID()}/docker${agentVersion > 1 ? '/v' + agentVersion : ''}/browse/put`; + + return new Promise((resolve, reject) => { Upload.upload({ url: url, data: { file: file, Path: path }, - }).then(deferred.resolve, deferred.reject, onProgress); - return deferred.promise; - } - - return service; - }, -]); + }).then(resolve, reject, onProgress); + }); + } +} From 99db41f96e86a692bb8b91b98377cc42e6f36361 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 23 Jul 2020 10:46:29 +0300 Subject: [PATCH 084/195] feat(agent): refactor volumeBrowserService to es6 (#4094) --- app/agent/services/volumeBrowserService.js | 108 ++++++++++----------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/app/agent/services/volumeBrowserService.js b/app/agent/services/volumeBrowserService.js index 002edb08b..233f5df22 100644 --- a/app/agent/services/volumeBrowserService.js +++ b/app/agent/services/volumeBrowserService.js @@ -1,61 +1,59 @@ -angular.module('portainer.agent').factory('VolumeBrowserService', [ - 'StateManager', - 'Browse', - 'BrowseVersion1', - '$q', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - 'Upload', - function VolumeBrowserServiceFactory(StateManager, Browse, BrowseVersion1, $q, API_ENDPOINT_ENDPOINTS, EndpointProvider, Upload) { - 'use strict'; - var service = {}; +import angular from 'angular'; - function getAgentApiVersion() { - var state = StateManager.getState(); - return state.endpoint.agentApiVersion; +angular.module('portainer.agent').factory('VolumeBrowserService', VolumeBrowserServiceFactory); + +function VolumeBrowserServiceFactory(StateManager, Browse, BrowseVersion1, API_ENDPOINT_ENDPOINTS, EndpointProvider, Upload) { + return { + ls, + get, + delete: deletePath, + rename, + upload, + }; + + function getAgentApiVersion() { + const state = StateManager.getState(); + return state.endpoint.agentApiVersion; + } + + function getBrowseService() { + const agentVersion = getAgentApiVersion(); + return agentVersion > 1 ? Browse : BrowseVersion1; + } + + function ls(volumeId, path) { + return getBrowseService().ls({ volumeID: volumeId, path, version: getAgentApiVersion() }).$promise; + } + + function get(volumeId, path) { + return getBrowseService().get({ volumeID: volumeId, path, version: getAgentApiVersion() }).$promise; + } + + function deletePath(volumeId, path) { + return getBrowseService().delete({ volumeID: volumeId, path, version: getAgentApiVersion() }).$promise; + } + + function rename(volumeId, path, newPath) { + const payload = { + CurrentFilePath: path, + NewFilePath: newPath, + }; + return getBrowseService().rename({ volumeID: volumeId, version: getAgentApiVersion() }, payload).$promise; + } + + function upload(path, file, volumeId, onProgress) { + const agentVersion = StateManager.getAgentApiVersion(); + if (agentVersion < 2) { + throw new Error('upload is not supported on this agent version'); } - function getBrowseService() { - var agentVersion = getAgentApiVersion(); - return agentVersion > 1 ? Browse : BrowseVersion1; - } - - service.ls = function (volumeId, path) { - return getBrowseService().ls({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; - }; - - service.get = function (volumeId, path) { - return getBrowseService().get({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; - }; - - service.delete = function (volumeId, path) { - return getBrowseService().delete({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; - }; - - service.rename = function (volumeId, path, newPath) { - var payload = { - CurrentFilePath: path, - NewFilePath: newPath, - }; - return getBrowseService().rename({ volumeID: volumeId, version: getAgentApiVersion() }, payload).$promise; - }; - - service.upload = function upload(path, file, volumeId, onProgress) { - var deferred = $q.defer(); - var agentVersion = StateManager.getAgentApiVersion(); - if (agentVersion < 2) { - deferred.reject('upload is not supported on this agent version'); - return; - } - var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker' + '/v' + agentVersion + '/browse/put?volumeID=' + volumeId; + const url = `${API_ENDPOINT_ENDPOINTS}/${EndpointProvider.endpointID()}/docker/v${agentVersion}/browse/put?volumeID=${volumeId}`; + return new Promise((resolve, reject) => { Upload.upload({ url: url, - data: { file: file, Path: path }, - }).then(deferred.resolve, deferred.reject, onProgress); - return deferred.promise; - }; - - return service; - }, -]); + data: { file, Path: path }, + }).then(resolve, reject, onProgress); + }); + } +} From 3953acf110a3987c17e60abd8a2dc414bbb645c7 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Fri, 24 Jul 2020 04:45:02 +0200 Subject: [PATCH 085/195] feat(k8s/volumes): introduce storage rollup panel (#4055) * feat(k8s/applications): storages rollup panel * feat(k8s/volumes): move storages table to volumes view * feat(k8s/volumes): minor UI update * feat(k8s/volumes): remember the selected tab * feat(api/k8s): update user default policies * feat(k8s/ui): minor UI update Co-authored-by: Anthony Lapenna --- api/kubernetes/cli/role.go | 5 + .../applicationsDatatable.html | 4 +- .../volumes-datatable/volumesDatatable.html | 4 +- .../converters/persistentVolumeClaim.js | 2 +- .../views/applications/applications.js | 3 + .../applications/applicationsController.js | 14 +- .../volumes-storages-datatable/controller.js | 79 ++++++++++ .../volumes-storages-datatable/index.js | 13 ++ .../volumes-storages-datatable/template.html | 139 ++++++++++++++++++ app/kubernetes/views/volumes/volumes.html | 30 +++- app/kubernetes/views/volumes/volumes.js | 3 + .../views/volumes/volumesController.js | 63 +++++++- 12 files changed, 336 insertions(+), 23 deletions(-) create mode 100644 app/kubernetes/views/volumes/components/volumes-storages-datatable/controller.js create mode 100644 app/kubernetes/views/volumes/components/volumes-storages-datatable/index.js create mode 100644 app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html diff --git a/api/kubernetes/cli/role.go b/api/kubernetes/cli/role.go index e19f1f22a..d3a233202 100644 --- a/api/kubernetes/cli/role.go +++ b/api/kubernetes/cli/role.go @@ -13,6 +13,11 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule { Resources: []string{"namespaces", "nodes"}, APIGroups: []string{""}, }, + { + Verbs: []string{"list"}, + Resources: []string{"storageclasses"}, + APIGroups: []string{"storage.k8s.io"}, + }, } } diff --git a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html index e30c5c73b..f1efdf899 100644 --- a/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html +++ b/app/kubernetes/components/datatables/applications-datatable/applicationsDatatable.html @@ -168,10 +168,10 @@ {{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }} - Loading... + Loading... - No application available. + No application available. diff --git a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html index 7955c6ed2..e37c7b9e6 100644 --- a/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/kubernetes/components/datatables/volumes-datatable/volumesDatatable.html @@ -159,10 +159,10 @@ - Loading... + Loading... - No volume available. + No volume available. diff --git a/app/kubernetes/converters/persistentVolumeClaim.js b/app/kubernetes/converters/persistentVolumeClaim.js index 29838a86c..8a7a76060 100644 --- a/app/kubernetes/converters/persistentVolumeClaim.js +++ b/app/kubernetes/converters/persistentVolumeClaim.js @@ -12,7 +12,7 @@ class KubernetesPersistentVolumeClaimConverter { res.Name = data.metadata.name; res.Namespace = data.metadata.namespace; res.CreationDate = data.metadata.creationTimestamp; - res.Storage = data.spec.resources.requests.storage.replace('i', '') + 'B'; + res.Storage = data.spec.resources.requests.storage.replace('i', 'B'); res.StorageClass = _.find(storageClasses, { Name: data.spec.storageClassName }); res.Yaml = yaml ? yaml.data : ''; res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] : ''; diff --git a/app/kubernetes/views/applications/applications.js b/app/kubernetes/views/applications/applications.js index e4b0de22c..7994db0eb 100644 --- a/app/kubernetes/views/applications/applications.js +++ b/app/kubernetes/views/applications/applications.js @@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesApplicationsView', { templateUrl: './applications.html', controller: 'KubernetesApplicationsController', controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, }); diff --git a/app/kubernetes/views/applications/applicationsController.js b/app/kubernetes/views/applications/applicationsController.js index 07553810e..a0f538590 100644 --- a/app/kubernetes/views/applications/applicationsController.js +++ b/app/kubernetes/views/applications/applicationsController.js @@ -1,5 +1,5 @@ import angular from 'angular'; -import _ from 'lodash-es'; +import * as _ from 'lodash-es'; import KubernetesStackHelper from 'Kubernetes/helpers/stackHelper'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; @@ -10,6 +10,7 @@ class KubernetesApplicationsController { this.$state = $state; this.Notifications = Notifications; this.KubernetesApplicationService = KubernetesApplicationService; + this.Authentication = Authentication; this.ModalService = ModalService; this.LocalStorage = LocalStorage; @@ -95,9 +96,10 @@ class KubernetesApplicationsController { async getApplicationsAsync() { try { - this.applications = await this.KubernetesApplicationService.get(); - this.stacks = KubernetesStackHelper.stacksFromApplications(this.applications); - this.ports = KubernetesApplicationHelper.portMappingsFromApplications(this.applications); + const applications = await this.KubernetesApplicationService.get(); + this.applications = applications; + this.stacks = KubernetesStackHelper.stacksFromApplications(applications); + this.ports = KubernetesApplicationHelper.portMappingsFromApplications(applications); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve applications'); } @@ -109,14 +111,12 @@ class KubernetesApplicationsController { async onInit() { this.state = { - activeTab: 0, + activeTab: this.LocalStorage.getActiveTab('applications'), currentName: this.$state.$current.name, isAdmin: this.Authentication.isAdmin(), viewReady: false, }; - this.state.activeTab = this.LocalStorage.getActiveTab('applications'); - await this.getApplications(); this.state.viewReady = true; diff --git a/app/kubernetes/views/volumes/components/volumes-storages-datatable/controller.js b/app/kubernetes/views/volumes/components/volumes-storages-datatable/controller.js new file mode 100644 index 000000000..7b5339570 --- /dev/null +++ b/app/kubernetes/views/volumes/components/volumes-storages-datatable/controller.js @@ -0,0 +1,79 @@ +import _ from 'lodash-es'; + +angular.module('portainer.kubernetes').controller('KubernetesVolumesStoragesDatatableController', [ + '$scope', + '$controller', + 'DatatableService', + function ($scope, $controller, DatatableService) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + this.state = Object.assign(this.state, { + expandedItems: [], + expandAll: false, + }); + + this.onSettingsRepeaterChange = function () { + DatatableService.setDataTableSettings(this.tableKey, this.settings); + }; + + this.expandItem = function (item, expanded) { + if (!this.itemCanExpand(item)) { + return; + } + + item.Expanded = expanded; + if (!expanded) { + item.Highlighted = false; + } + }; + + this.itemCanExpand = function (item) { + return item.Volumes.length > 0; + }; + + this.hasExpandableItems = function () { + return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length; + }; + + this.expandAll = function () { + this.state.expandAll = !this.state.expandAll; + _.forEach(this.state.filteredDataSet, (item) => { + if (this.itemCanExpand(item)) { + this.expandItem(item, this.state.expandAll); + } + }); + }; + + this.$onInit = function () { + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; + }, +]); diff --git a/app/kubernetes/views/volumes/components/volumes-storages-datatable/index.js b/app/kubernetes/views/volumes/components/volumes-storages-datatable/index.js new file mode 100644 index 000000000..8a48e18c2 --- /dev/null +++ b/app/kubernetes/views/volumes/components/volumes-storages-datatable/index.js @@ -0,0 +1,13 @@ +angular.module('portainer.kubernetes').component('kubernetesVolumesStoragesDatatable', { + templateUrl: './template.html', + controller: 'KubernetesVolumesStoragesDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + }, +}); diff --git a/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html b/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html new file mode 100644 index 000000000..5d1f090c4 --- /dev/null +++ b/app/kubernetes/views/volumes/components/volumes-storages-datatable/template.html @@ -0,0 +1,139 @@ +
+ + +
+
{{ $ctrl.titleText }}
+
+ + Table settings + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Storage + + + + + + Usage + + + +
+ + + + {{ item.Name }}{{ item.Size }}
+ + {{ vol.PersistentVolumeClaim.Name }} + + + {{ vol.PersistentVolumeClaim.Storage }} +
Loading...
No storage available.
+
+ +
+
+
diff --git a/app/kubernetes/views/volumes/volumes.html b/app/kubernetes/views/volumes/volumes.html index 4c9ba2f81..71a328591 100644 --- a/app/kubernetes/views/volumes/volumes.html +++ b/app/kubernetes/views/volumes/volumes.html @@ -7,14 +7,28 @@
- - + + + + + Volumes + + + + + Storage + + + + + +
diff --git a/app/kubernetes/views/volumes/volumes.js b/app/kubernetes/views/volumes/volumes.js index 98829ddc1..f004c20ff 100644 --- a/app/kubernetes/views/volumes/volumes.js +++ b/app/kubernetes/views/volumes/volumes.js @@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesVolumesView', { templateUrl: './volumes.html', controller: 'KubernetesVolumesController', controllerAs: 'ctrl', + bindings: { + $transition$: '<', + }, }); diff --git a/app/kubernetes/views/volumes/volumesController.js b/app/kubernetes/views/volumes/volumesController.js index 2fcb899a3..90d44cf9f 100644 --- a/app/kubernetes/views/volumes/volumesController.js +++ b/app/kubernetes/views/volumes/volumesController.js @@ -1,14 +1,52 @@ -import _ from 'lodash-es'; +import * as _ from 'lodash-es'; +import filesizeParser from 'filesize-parser'; import angular from 'angular'; import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; +function buildStorages(storages, volumes) { + _.forEach(storages, (s) => { + const filteredVolumes = _.filter(volumes, ['PersistentVolumeClaim.StorageClass.Name', s.Name, 'PersistentVolumeClaim.StorageClass.Provisioner', s.Provisioner]); + s.Volumes = filteredVolumes; + s.Size = computeSize(filteredVolumes); + }); + return storages; +} + +function computeSize(volumes) { + let hasT, + hasG, + hasM = false; + const size = _.sumBy(volumes, (v) => { + const storage = v.PersistentVolumeClaim.Storage; + if (!hasT && _.endsWith(storage, 'TB')) { + hasT = true; + } else if (!hasG && _.endsWith(storage, 'GB')) { + hasG = true; + } else if (!hasM && _.endsWith(storage, 'MB')) { + hasM = true; + } + return filesizeParser(storage, { base: 10 }); + }); + if (hasT) { + return size / 1000 / 1000 / 1000 / 1000 + 'TB'; + } else if (hasG) { + return size / 1000 / 1000 / 1000 + 'GB'; + } else if (hasM) { + return size / 1000 / 1000 + 'MB'; + } + return size; +} + class KubernetesVolumesController { /* @ngInject */ - constructor($async, $state, Notifications, ModalService, KubernetesVolumeService, KubernetesApplicationService) { + constructor($async, $state, Notifications, ModalService, LocalStorage, EndpointProvider, KubernetesStorageService, KubernetesVolumeService, KubernetesApplicationService) { this.$async = $async; this.$state = $state; this.Notifications = Notifications; this.ModalService = ModalService; + this.LocalStorage = LocalStorage; + this.EndpointProvider = EndpointProvider; + this.KubernetesStorageService = KubernetesStorageService; this.KubernetesVolumeService = KubernetesVolumeService; this.KubernetesApplicationService = KubernetesApplicationService; @@ -19,6 +57,10 @@ class KubernetesVolumesController { this.removeActionAsync = this.removeActionAsync.bind(this); } + selectTab(index) { + this.LocalStorage.storeActiveTab('volumes', index); + } + async removeActionAsync(selectedItems) { let actionCount = selectedItems.length; for (const volume of selectedItems) { @@ -48,12 +90,17 @@ class KubernetesVolumesController { async getVolumesAsync() { try { - const [volumes, applications] = await Promise.all([this.KubernetesVolumeService.get(), this.KubernetesApplicationService.get()]); + const [volumes, applications, storages] = await Promise.all([ + this.KubernetesVolumeService.get(), + this.KubernetesApplicationService.get(), + this.KubernetesStorageService.get(this.state.endpointId), + ]); this.volumes = _.map(volumes, (volume) => { volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications); return volume; }); + this.storages = buildStorages(storages, volumes); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retreive resource pools'); } @@ -66,6 +113,10 @@ class KubernetesVolumesController { async onInit() { this.state = { viewReady: false, + // endpointId: this.$transition$.params().endpointId, // TODO: use this when moving to endpointID in URL + currentName: this.$state.$current.name, + endpointId: this.EndpointProvider.endpointID(), + activeTab: this.LocalStorage.getActiveTab('volumes'), }; await this.getVolumes(); @@ -76,6 +127,12 @@ class KubernetesVolumesController { $onInit() { return this.$async(this.onInit); } + + $onDestroy() { + if (this.state.currentName !== this.$state.$current.name) { + this.LocalStorage.storeActiveTab('volumes', 0); + } + } } export default KubernetesVolumesController; From e78aaec558f91b339e126f68cd93f1b63a32be54 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sat, 25 Jul 2020 11:10:46 +1200 Subject: [PATCH 086/195] feat(api/bolt): update DBVersion to 25 --- api/bolt/migrator/migrate_dbversion23.go | 18 ++---------------- api/bolt/migrator/migrate_dbversion24.go | 20 ++++++++++++++++++++ api/bolt/migrator/migrator.go | 10 +++++++++- api/portainer.go | 2 +- 4 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 api/bolt/migrator/migrate_dbversion24.go diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go index 01c97d152..f106038b5 100644 --- a/api/bolt/migrator/migrate_dbversion23.go +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -1,20 +1,6 @@ package migrator -import ( - "github.com/portainer/portainer/api" -) - func (m *Migrator) updateSettingsToDB24() error { - legacySettings, err := m.settingsService.Settings() - if err != nil { - return err - } - - if legacySettings.TemplatesURL == "" { - legacySettings.TemplatesURL = portainer.DefaultTemplatesURL - } - - legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout - - return m.settingsService.UpdateSettings(legacySettings) + // Placeholder for 1.24.1 backports + return nil } diff --git a/api/bolt/migrator/migrate_dbversion24.go b/api/bolt/migrator/migrate_dbversion24.go new file mode 100644 index 000000000..d1dc5f0cf --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion24.go @@ -0,0 +1,20 @@ +package migrator + +import ( + "github.com/portainer/portainer/api" +) + +func (m *Migrator) updateSettingsToDB25() error { + legacySettings, err := m.settingsService.Settings() + if err != nil { + return err + } + + if legacySettings.TemplatesURL == "" { + legacySettings.TemplatesURL = portainer.DefaultTemplatesURL + } + + legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout + + return m.settingsService.UpdateSettings(legacySettings) +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index a933b6519..598681f28 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -321,7 +321,7 @@ func (m *Migrator) Migrate() error { } } - // Portainer 2.0 + // Portainer 1.24.1 if m.currentDBVersion < 24 { err := m.updateSettingsToDB24() if err != nil { @@ -329,5 +329,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 2.0 + if m.currentDBVersion < 25 { + err := m.updateSettingsToDB25() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/portainer.go b/api/portainer.go index 428b07c97..fa2a8afcc 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1118,7 +1118,7 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "2.0.0-dev" // DBVersion is the version number of the Portainer database - DBVersion = 24 + DBVersion = 25 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved From adf33385cea6321e0bd4cd9788da56fc6e762cf7 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sat, 25 Jul 2020 02:14:46 +0300 Subject: [PATCH 087/195] feat(containers): Prevent non-admin users from running containers using the host namespace pid (#4098) * feat(containers): prevent non-admin users from running containers using the host namespace pid (#3970) * feat(containers): Prevent non-admin users from running containers using the host namespace pid * feat(containers): add rbac check for swarm stack too * feat(containers): remove forgotten conflict * feat(containers): init EnableHostNamespaceUse to true and return 403 on forbidden action * feat(containers): change enableHostNamespaceUse to restrictHostNamespaceUse in html * feat(settings): rename EnableHostNamespaceUse to AllowHostNamespaceForRegularUsers * feat(database): trigger migration for AllowHostNamespace * feat(containers): check container creation authorization Co-authored-by: Maxime Bajeux --- api/bolt/init.go | 1 + api/bolt/migrator/migrate_dbversion23.go | 10 ++++- api/http/handler/settings/settings_public.go | 2 + api/http/handler/settings/settings_update.go | 5 +++ .../handler/stacks/create_compose_stack.go | 6 ++- api/http/handler/stacks/stack_create.go | 4 ++ api/http/proxy/factory/docker/containers.go | 43 +++++++++++-------- api/portainer.go | 1 + app/portainer/models/settings.js | 1 + app/portainer/services/stateManager.js | 5 +++ app/portainer/views/settings/settings.html | 11 +++++ .../views/settings/settingsController.js | 4 ++ 12 files changed, 72 insertions(+), 21 deletions(-) diff --git a/api/bolt/init.go b/api/bolt/init.go index 244f4e961..982702071 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -28,6 +28,7 @@ func (store *Store) Init() error { AllowBindMountsForRegularUsers: true, AllowPrivilegedModeForRegularUsers: true, AllowVolumeBrowserForRegularUsers: false, + AllowHostNamespaceForRegularUsers: true, EnableHostManagementFeatures: false, EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, TemplatesURL: portainer.DefaultTemplatesURL, diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go index f106038b5..74f6436cf 100644 --- a/api/bolt/migrator/migrate_dbversion23.go +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -1,6 +1,12 @@ package migrator func (m *Migrator) updateSettingsToDB24() error { - // Placeholder for 1.24.1 backports - return nil + legacySettings, err := m.settingsService.Settings() + if err != nil { + return err + } + + legacySettings.AllowHostNamespaceForRegularUsers = true + + return m.settingsService.UpdateSettings(legacySettings) } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 5af534d45..3dfcd5325 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -18,6 +18,7 @@ type publicSettingsResponse struct { EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` OAuthLoginURI string `json:"OAuthLoginURI"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` } // GET request on /api/settings/public @@ -33,6 +34,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index d0a7f896a..a0a05c9e8 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -22,6 +22,7 @@ type settingsUpdatePayload struct { OAuthSettings *portainer.OAuthSettings AllowBindMountsForRegularUsers *bool AllowPrivilegedModeForRegularUsers *bool + AllowHostNamespaceForRegularUsers *bool AllowVolumeBrowserForRegularUsers *bool EnableHostManagementFeatures *bool SnapshotInterval *string @@ -125,6 +126,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures } + if payload.AllowHostNamespaceForRegularUsers != nil { + settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers + } + if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 37d8c1d7c..0e7f1c2e3 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -336,7 +336,11 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) return err } - if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers) && !isAdminOrEndpointAdmin { + if (!settings.AllowBindMountsForRegularUsers || + !settings.AllowPrivilegedModeForRegularUsers || + !settings.AllowHostNamespaceForRegularUsers) && + !isAdminOrEndpointAdmin { + composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) stackContent, err := handler.FileService.GetFileContent(composeFilePath) diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index a785ff8a2..2da04589e 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -142,6 +142,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { return errors.New("privileged mode disabled for non administrator users") } + + if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { + return errors.New("pid host disabled for non administrator users") + } } return nil diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index 519282be4..cdf1fdb8a 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -158,10 +158,15 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { type PartialContainer struct { HostConfig struct { - Privileged bool `json:"Privileged"` + Privileged bool `json:"Privileged"` + PidMode string `json:"PidMode"` } `json:"HostConfig"` } + forbiddenResponse := &http.Response{ + StatusCode: http.StatusForbidden, + } + tokenData, err := security.RetrieveTokenData(request) if err != nil { return nil, err @@ -189,24 +194,26 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, err } - if !settings.AllowPrivilegedModeForRegularUsers { - body, err := ioutil.ReadAll(request.Body) - if err != nil { - return nil, err - } - - partialContainer := &PartialContainer{} - err = json.Unmarshal(body, partialContainer) - if err != nil { - return nil, err - } - - if partialContainer.HostConfig.Privileged { - return nil, errors.New("forbidden to use privileged mode") - } - - request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + body, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, err } + + partialContainer := &PartialContainer{} + err = json.Unmarshal(body, partialContainer) + if err != nil { + return nil, err + } + + if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged { + return forbiddenResponse, errors.New("forbidden to use privileged mode") + } + + if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" { + return forbiddenResponse, errors.New("forbidden to use pid host namespace") + } + + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) } response, err := transport.executeDockerRequest(request) diff --git a/api/portainer.go b/api/portainer.go index fa2a8afcc..8ff870800 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -524,6 +524,7 @@ type ( EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` UserSessionTimeout string `json:"UserSessionTimeout"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index f7498f81b..49cdd0faa 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -13,6 +13,7 @@ export function SettingsViewModel(data) { this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; this.UserSessionTimeout = data.UserSessionTimeout; + this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; } export function PublicSettingsViewModel(settings) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 43a021cce..9f57c2959 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -76,6 +76,11 @@ angular.module('portainer.app').factory('StateManager', [ LocalStorage.storeApplicationState(state.application); }; + manager.updateAllowHostNamespaceForRegularUsers = function (allowHostNamespaceForRegularUsers) { + state.application.allowHostNamespaceForRegularUsers = allowHostNamespaceForRegularUsers; + LocalStorage.storeApplicationState(state.application); + }; + function assignStateFromStatusAndSettings(status, settings) { state.application.analytics = status.Analytics; state.application.version = status.Version; diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 07ab23533..0b3f4a848 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -108,6 +108,17 @@
+
+
+ + +
+
diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index f2c766c34..7328d1479 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -32,6 +32,7 @@ angular.module('portainer.app').controller('SettingsController', [ enableHostManagementFeatures: false, enableVolumeBrowser: false, enableEdgeComputeFeatures: false, + restrictHostNamespaceForRegularUsers: false, }; $scope.removeFilteredContainerLabel = function (index) { @@ -64,6 +65,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser; settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures; settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures; + settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers; $scope.state.actionInProgress = true; updateSettings(settings); @@ -77,6 +79,7 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateSnapshotInterval(settings.SnapshotInterval); StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures); StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers); + StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers); StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures); $state.reload(); }) @@ -102,6 +105,7 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers; $scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures; $scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; + $scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings'); From 43bbc14c58b242851399a7a4c84889504a0f47c1 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Sat, 25 Jul 2020 02:23:44 +0300 Subject: [PATCH 088/195] feat(app/package): upgrade angularjs to 1.8 (#4073) * chore(yarn): upgrade angularjs * refactor(app): use $onInit instead of initComponent * feat(app/package): remove angular-cookies dependency Co-authored-by: Anthony Lapenna --- app/__module.js | 1 - .../networkMacvlanFormController.js | 5 +-- .../storidgeProfileSelectorController.js | 5 +-- .../porAccessControlFormController.js | 5 +-- .../porAccessControlPanelController.js | 5 +-- .../porEndpointSecurityController.js | 5 +-- package.json | 11 +++-- yarn.lock | 44 +++++++------------ 8 files changed, 32 insertions(+), 49 deletions(-) diff --git a/app/__module.js b/app/__module.js index fc3034f39..489ebe0a3 100644 --- a/app/__module.js +++ b/app/__module.js @@ -14,7 +14,6 @@ angular.module('portainer', [ 'ui.router', 'ui.select', 'isteven-multi-select', - 'ngCookies', 'ngSanitize', 'ngFileUpload', 'ngMessages', diff --git a/app/docker/components/network-macvlan-form/networkMacvlanFormController.js b/app/docker/components/network-macvlan-form/networkMacvlanFormController.js index e3873648a..959adca46 100644 --- a/app/docker/components/network-macvlan-form/networkMacvlanFormController.js +++ b/app/docker/components/network-macvlan-form/networkMacvlanFormController.js @@ -22,7 +22,8 @@ angular.module('portainer.docker').controller('NetworkMacvlanFormController', [ return !ctrl.data.SelectedNetworkConfig; }; - function initComponent() { + this.$onInit = $onInit; + function $onInit() { var isAdmin = Authentication.isAdmin(); ctrl.isAdmin = isAdmin; @@ -44,7 +45,5 @@ angular.module('portainer.docker').controller('NetworkMacvlanFormController', [ Notifications.error('Failure', err, 'Unable to retrieve informations for macvlan'); }); } - - initComponent(); }, ]); diff --git a/app/integrations/storidge/components/profileSelector/storidgeProfileSelectorController.js b/app/integrations/storidge/components/profileSelector/storidgeProfileSelectorController.js index ab47573b2..9c3d2beb6 100644 --- a/app/integrations/storidge/components/profileSelector/storidgeProfileSelectorController.js +++ b/app/integrations/storidge/components/profileSelector/storidgeProfileSelectorController.js @@ -4,7 +4,8 @@ angular.module('portainer.integrations.storidge').controller('StoridgeProfileSel function (StoridgeProfileService, Notifications) { var ctrl = this; - function initComponent() { + this.$onInit = $onInit; + function $onInit() { StoridgeProfileService.profiles() .then(function success(data) { ctrl.profiles = data; @@ -13,7 +14,5 @@ angular.module('portainer.integrations.storidge').controller('StoridgeProfileSel Notifications.error('Failure', err, 'Unable to retrieve Storidge profiles'); }); } - - initComponent(); }, ]); diff --git a/app/portainer/components/accessControlForm/porAccessControlFormController.js b/app/portainer/components/accessControlForm/porAccessControlFormController.js index e65e6f030..97b4b7ee8 100644 --- a/app/portainer/components/accessControlForm/porAccessControlFormController.js +++ b/app/portainer/components/accessControlForm/porAccessControlFormController.js @@ -44,7 +44,8 @@ angular.module('portainer.app').controller('porAccessControlFormController', [ }); } - function initComponent() { + this.$onInit = $onInit; + function $onInit() { var isAdmin = Authentication.isAdmin(); ctrl.isAdmin = isAdmin; @@ -79,7 +80,5 @@ angular.module('portainer.app').controller('porAccessControlFormController', [ Notifications.error('Failure', err, 'Unable to retrieve access control information'); }); } - - initComponent(); }, ]); diff --git a/app/portainer/components/accessControlPanel/porAccessControlPanelController.js b/app/portainer/components/accessControlPanel/porAccessControlPanelController.js index 8667482e5..ce846cb66 100644 --- a/app/portainer/components/accessControlPanel/porAccessControlPanelController.js +++ b/app/portainer/components/accessControlPanel/porAccessControlPanelController.js @@ -86,7 +86,8 @@ angular.module('portainer.app').controller('porAccessControlPanelController', [ }); } - function initComponent() { + this.$onInit = $onInit; + function $onInit() { var userDetails = Authentication.getUserDetails(); var isAdmin = Authentication.isAdmin(); var userId = userDetails.ID; @@ -138,7 +139,5 @@ angular.module('portainer.app').controller('porAccessControlPanelController', [ Notifications.error('Failure', err, 'Unable to retrieve access control information'); }); } - - initComponent(); }, ]); diff --git a/app/portainer/components/endpointSecurity/porEndpointSecurityController.js b/app/portainer/components/endpointSecurity/porEndpointSecurityController.js index 186d2bbe8..1b6bdc0f4 100644 --- a/app/portainer/components/endpointSecurity/porEndpointSecurityController.js +++ b/app/portainer/components/endpointSecurity/porEndpointSecurityController.js @@ -2,7 +2,8 @@ angular.module('portainer.app').controller('porEndpointSecurityController', [ function () { var ctrl = this; - function initComponent() { + this.$onInit = $onInit; + function $onInit() { if (ctrl.endpoint) { var endpoint = ctrl.endpoint; var TLS = endpoint.TLSConfig.TLS; @@ -27,7 +28,5 @@ angular.module('portainer.app').controller('porEndpointSecurityController', [ } } } - - initComponent(); }, ]); diff --git a/package.json b/package.json index 293fd6c14..077ecadc2 100644 --- a/package.json +++ b/package.json @@ -53,21 +53,20 @@ "@babel/polyfill": "^7.2.5", "@fortawesome/fontawesome-free": "^5.11.2", "@uirouter/angularjs": "1.0.11", - "angular": "~1.5.0", + "angular": "1.8.0", "angular-clipboard": "^1.6.2", - "angular-cookies": "~1.5.0", "angular-file-saver": "^1.1.3", "angular-google-analytics": "github:revolunet/angular-google-analytics#semver:~1.1.9", "angular-json-tree": "1.0.1", "angular-jwt": "~0.1.8", "angular-loading-bar": "~0.9.0", "angular-local-storage": "~0.5.2", - "angular-messages": "~1.5.0", - "angular-mocks": "~1.5.0", + "angular-messages": "1.8.0", + "angular-mocks": "1.8.0", "angular-moment-picker": "^0.10.2", "angular-multiselect": "github:portainer/angular-multi-select#semver:~v4.0.1", - "angular-resource": "~1.5.0", - "angular-sanitize": "~1.5.0", + "angular-resource": "1.8.0", + "angular-sanitize": "1.8.0", "angular-ui-bootstrap": "~2.5.0", "angular-utils-pagination": "~0.11.1", "angularjs-scroll-glue": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 77b495a16..c7cc60ff1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,11 +1197,6 @@ angular-clipboard@^1.6.2: resolved "https://registry.yarnpkg.com/angular-clipboard/-/angular-clipboard-1.7.0.tgz#9621a6ce66eab1ea9549aa8bfb3b71352307554f" integrity sha512-4/eg3zZw1MJpIsMc+mWzeVNyWBu8YWpXPTdmbgyPRp/6f0xB6I3XR2iC6Mb4mg/5E9q6exCd0sX2yiIsw+ZLJw== -angular-cookies@~1.5.0: - version "1.5.11" - resolved "https://registry.yarnpkg.com/angular-cookies/-/angular-cookies-1.5.11.tgz#88558de7c5044dcc3abeb79614d7ef8107ba49c0" - integrity sha1-iFWN58UETcw6vreWFNfvgQe6ScA= - angular-file-saver@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/angular-file-saver/-/angular-file-saver-1.1.3.tgz#dcaec0695214f226a4caafc8c16d21a9a61f7d1b" @@ -1236,20 +1231,20 @@ angular-local-storage@~0.5.2: resolved "https://registry.yarnpkg.com/angular-local-storage/-/angular-local-storage-0.5.2.tgz#7079beb0aa5ca91386d223125efefd13ca0ecd0c" integrity sha1-cHm+sKpcqROG0iMSXv79E8oOzQw= -angular-messages@~1.5.0: - version "1.5.11" - resolved "https://registry.yarnpkg.com/angular-messages/-/angular-messages-1.5.11.tgz#ea99f0163594fcb0a2db701b3038339250decc90" - integrity sha1-6pnwFjWU/LCi23AbMDgzklDezJA= +angular-messages@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/angular-messages/-/angular-messages-1.8.0.tgz#dcdb8ea7d85a939526921e8cf05950601dc2b8fb" + integrity sha512-LSlyTv80y1vg8Cfdz+RJ0BrraWkq0qsOZHyrHFT8NyYWC5yp6HQabvZvTpKv9YA+6o05wwAU90qXHSrnUlSflA== angular-mocks@1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.6.1.tgz#2f44a1b3ac608e93751305bce176c274221d8abd" integrity sha1-L0Shs6xgjpN1EwW84XbCdCIdir0= -angular-mocks@~1.5.0: - version "1.5.11" - resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.5.11.tgz#a0e1dd0ea55fd77ee7a757d75536c5e964c86f81" - integrity sha1-oOHdDqVf137np1fXVTbF6WTIb4E= +angular-mocks@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.8.0.tgz#9d161fe2102e47e46ff78a4226e4dfec27c060c4" + integrity sha512-oFKJIqR6zcsW6V9UQMuUYCXIcTy+n7oYExXxZkvU57hwJQdU1nuNZcSMQA9URiEAPyo7v3bqm0eOP3Ezi7Bigg== angular-moment-picker@^0.10.2: version "0.10.2" @@ -1266,20 +1261,20 @@ angular-moment-picker@^0.10.2: angular-mocks "1.6.1" angular-sanitize "1.6.1" -angular-resource@~1.5.0: - version "1.5.11" - resolved "https://registry.yarnpkg.com/angular-resource/-/angular-resource-1.5.11.tgz#d93ea619184a2e0ee3ae338265758363172929f0" - integrity sha1-2T6mGRhKLg7jrjOCZXWDYxcpKfA= +angular-resource@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/angular-resource/-/angular-resource-1.8.0.tgz#578ef122e7cb7bcc6c0ad6c2451dc3d27fd570ba" + integrity sha512-9woUq3kDwoT7R6SjKX8vaJMhOplYBm9sqRAxKgDhDIdPyA8iBowqQIusf9+8Q+z/HlXb8ZXvKspJyKXrxmKdvg== angular-sanitize@1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.6.1.tgz#cacffec3199ed66297afbb1ef366ec66616b3b3f" integrity sha1-ys/+wxme1mKXr7se82bsZmFrOz8= -angular-sanitize@~1.5.0: - version "1.5.11" - resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.5.11.tgz#ebfb3f343e543f9b2ef050fb4c2e9ee048d1772f" - integrity sha1-6/s/ND5UP5su8FD7TC6e4EjRdy8= +angular-sanitize@1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/angular-sanitize/-/angular-sanitize-1.8.0.tgz#9f80782d3afeec3bcc0bb92b3ca6f1f421cfbca6" + integrity sha512-j5GiOPCvfcDWK5svEOVoPb11X3UDVy/mdHPRWuy14Iyw86xaq+Bb+x/em2sAOa5MQQeY5ciLXbF3RRp8iCKcNg== angular-ui-bootstrap@~2.5.0: version "2.5.6" @@ -1291,16 +1286,11 @@ angular-utils-pagination@~0.11.1: resolved "https://registry.yarnpkg.com/angular-utils-pagination/-/angular-utils-pagination-0.11.1.tgz#efad7c8879beb30ad3d77707f93e3d0ef51f2c66" integrity sha1-7618iHm+swrT13cH+T49DvUfLGY= -angular@1.x, angular@^1.3: +angular@1.8.0, angular@1.x, angular@^1.3: version "1.8.0" resolved "https://registry.yarnpkg.com/angular/-/angular-1.8.0.tgz#b1ec179887869215cab6dfd0df2e42caa65b1b51" integrity sha512-VdaMx+Qk0Skla7B5gw77a8hzlcOakwF8mjlW13DpIWIDlfqwAbSSLfd8N/qZnzEmQF4jC4iofInd3gE7vL8ZZg== -angular@~1.5.0: - version "1.5.11" - resolved "https://registry.yarnpkg.com/angular/-/angular-1.5.11.tgz#8c5ba7386f15965c9acf3429f6881553aada30d6" - integrity sha1-jFunOG8VllyazzQp9ogVU6raMNY= - angularjs-scroll-glue@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/angularjs-scroll-glue/-/angularjs-scroll-glue-2.2.0.tgz#07d3399ac16ca874c63b6b5ee2ee30558b37e5d1" From 7e7a8e521bb7770263355c4ce897a498ef918a45 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Sat, 25 Jul 2020 11:32:31 +1200 Subject: [PATCH 089/195] feat(app/package): remove angular-cookies dependency --- app/vendors.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/vendors.js b/app/vendors.js index db03b38b4..c925c910f 100644 --- a/app/vendors.js +++ b/app/vendors.js @@ -18,7 +18,6 @@ import angular from 'angular'; import 'moment'; import '@uirouter/angularjs'; import 'ui-select'; -import 'angular-cookies'; import 'angular-sanitize'; import 'ng-file-upload'; import 'angular-messages'; From 990f3cad88094c79694b96d8f5351f08ee61845c Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 27 Jul 2020 09:19:33 +1200 Subject: [PATCH 090/195] chore(github/stalebot): update stalebot config --- .github/stale.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/stale.yml b/.github/stale.yml index 85d558ed5..725d77f2e 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -12,14 +12,14 @@ issues: # Issues with these labels will never be considered stale exemptLabels: - kind/enhancement - - kind/feature - kind/question - kind/style - kind/workaround + - kind/bug - bug/need-confirmation - bug/confirmed - status/discuss - + # Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled) onlyLabels: [] @@ -35,9 +35,9 @@ issues: # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > - This issue has been marked as stale as it has not had recent activity, + This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. - If you believe that it has been incorrectly labelled as stale, + If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed. # Comment to post when removing the stale label. @@ -49,7 +49,6 @@ issues: Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment and mention @itsconquest. One of our staff will then review the issue. - + Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed. - \ No newline at end of file From 0cebe6588a197d4f64e7a76cb7961efd70723c74 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Mon, 27 Jul 2020 09:24:41 +1200 Subject: [PATCH 091/195] chore(github/stalebot): update stalebot config --- .github/stale.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index 725d77f2e..45d35a93a 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -15,7 +15,6 @@ issues: - kind/question - kind/style - kind/workaround - - kind/bug - bug/need-confirmation - bug/confirmed - status/discuss From 2bc6b2dff7b0b2a384e1ff92760e838227adb4f7 Mon Sep 17 00:00:00 2001 From: DarkAEther <30438425+DarkAEther@users.noreply.github.com> Date: Mon, 27 Jul 2020 02:58:33 +0530 Subject: [PATCH 092/195] feat(docker/container-creation): sort volumes in container creation view (#4078) * #3635 fix(containers) sort volumes in container creation view * fix(3635) sort volumes in container creation view --- .../views/containers/create/createContainerController.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index d7379656b..bebf468de 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -189,7 +189,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ function preparePortBindings(config) { const bindings = ContainerHelper.preparePortBindings(config.HostConfig.PortBindings); - config.ExposedPorts={}; + config.ExposedPorts = {}; _.forEach(bindings, (_, key) => (config.ExposedPorts[key] = {})); config.HostConfig.PortBindings = bindings; } @@ -612,7 +612,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [ Volume.query( {}, function (d) { - $scope.availableVolumes = d.Volumes; + $scope.availableVolumes = d.Volumes.sort((vol1, vol2) => { + return vol1.Name.localeCompare(vol2.Name); + }); }, function (e) { Notifications.error('Failure', e, 'Unable to retrieve volumes'); From 07efd4bddae3460e12bf7fb5517d6273806fb4fb Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 27 Jul 2020 00:31:14 +0300 Subject: [PATCH 093/195] feat(settings): add setting to disable device mapping for regular users (#4099) * feat(settings): add setting to disable device mapping for regular users * feat(settings): introduce device mapping service * feat(containers): hide devices field when setting is on * feat(containers): prevent passing of devices when not allowed * feat(stacks): prevent non admin from device mapping * feat(stacks): disallow swarm stack creation for user * refactor(settings): replace disableDeviceMapping with allow * fix(stacks): remove check for disable device mappings from swarm * feat(settings): rename field to disable * feat(settings): supply default value for disableDeviceMapping * feat(container): check for endpoint admin * style(server): sort imports --- api/bolt/init.go | 1 + api/bolt/migrator/migrate_dbversion23.go | 1 + api/http/handler/settings/settings_public.go | 4 +++- api/http/handler/settings/settings_update.go | 5 +++++ .../handler/stacks/create_compose_stack.go | 3 ++- api/http/handler/stacks/stack_create.go | 6 +++++- api/http/proxy/factory/docker/containers.go | 13 +++++++++--- api/portainer.go | 3 ++- .../create/createContainerController.js | 20 +++++++++++++++++-- .../containers/create/createcontainer.html | 2 +- app/portainer/models/settings.js | 4 +++- app/portainer/services/stateManager.js | 6 ++++++ app/portainer/views/settings/settings.html | 10 ++++++++++ .../views/settings/settingsController.js | 4 ++++ 14 files changed, 71 insertions(+), 11 deletions(-) diff --git a/api/bolt/init.go b/api/bolt/init.go index 982702071..1369e6a1e 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -29,6 +29,7 @@ func (store *Store) Init() error { AllowPrivilegedModeForRegularUsers: true, AllowVolumeBrowserForRegularUsers: false, AllowHostNamespaceForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, EnableHostManagementFeatures: false, EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, TemplatesURL: portainer.DefaultTemplatesURL, diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go index 74f6436cf..ba38987c5 100644 --- a/api/bolt/migrator/migrate_dbversion23.go +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -7,6 +7,7 @@ func (m *Migrator) updateSettingsToDB24() error { } legacySettings.AllowHostNamespaceForRegularUsers = true + legacySettings.AllowDeviceMappingForRegularUsers = true return m.settingsService.UpdateSettings(legacySettings) } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 3dfcd5325..097c14676 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -15,10 +15,11 @@ type publicSettingsResponse struct { AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` OAuthLoginURI string `json:"OAuthLoginURI"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` } // GET request on /api/settings/public @@ -35,6 +36,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index a0a05c9e8..d92307b08 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -24,6 +24,7 @@ type settingsUpdatePayload struct { AllowPrivilegedModeForRegularUsers *bool AllowHostNamespaceForRegularUsers *bool AllowVolumeBrowserForRegularUsers *bool + AllowDeviceMappingForRegularUsers *bool EnableHostManagementFeatures *bool SnapshotInterval *string TemplatesURL *string @@ -149,6 +150,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * handler.JWTService.SetUserSessionDuration(userSessionDuration) } + if payload.AllowDeviceMappingForRegularUsers != nil { + settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers + } + tlsError := handler.updateTLS(settings) if tlsError != nil { return tlsError diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 0e7f1c2e3..af79889f4 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -338,7 +338,8 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || - !settings.AllowHostNamespaceForRegularUsers) && + !settings.AllowHostNamespaceForRegularUsers || + !settings.AllowDeviceMappingForRegularUsers) && !isAdminOrEndpointAdmin { composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 2da04589e..ba3b5388d 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -10,7 +10,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" httperrors "github.com/portainer/portainer/api/http/errors" "github.com/portainer/portainer/api/http/security" @@ -146,6 +146,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" { return errors.New("pid host disabled for non administrator users") } + + if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 { + return errors.New("device mapping disabled for non administrator users") + } } return nil diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index cdf1fdb8a..d957be030 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -158,8 +158,9 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { type PartialContainer struct { HostConfig struct { - Privileged bool `json:"Privileged"` - PidMode string `json:"PidMode"` + Privileged bool `json:"Privileged"` + PidMode string `json:"PidMode"` + Devices []interface{} `json:"Devices"` } `json:"HostConfig"` } @@ -188,7 +189,9 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req endpointResourceAccess = true } - if (rbacExtension != nil && !endpointResourceAccess && tokenData.Role != portainer.AdministratorRole) || (rbacExtension == nil && tokenData.Role != portainer.AdministratorRole) { + isAdmin := (rbacExtension != nil && endpointResourceAccess) || tokenData.Role == portainer.AdministratorRole + + if !isAdmin { settings, err := transport.dataStore.Settings().Settings() if err != nil { return nil, err @@ -213,6 +216,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return forbiddenResponse, errors.New("forbidden to use pid host namespace") } + if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { + return nil, errors.New("forbidden to use device mapping") + } + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) } diff --git a/api/portainer.go b/api/portainer.go index 8ff870800..d503e0bbf 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -518,13 +518,14 @@ type ( AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` SnapshotInterval string `json:"SnapshotInterval"` TemplatesURL string `json:"TemplatesURL"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` UserSessionTimeout string `json:"UserSessionTimeout"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index bebf468de..965364b72 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -30,6 +30,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ 'SettingsService', 'PluginService', 'HttpRequestHelper', + 'ExtensionService', function ( $q, $scope, @@ -55,7 +56,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [ SystemService, SettingsService, PluginService, - HttpRequestHelper + HttpRequestHelper, + ExtensionService ) { $scope.create = create; @@ -604,7 +606,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); } - function initView() { + async function initView() { var nodeName = $transition$.params().nodeName; $scope.formValues.NodeName = nodeName; HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); @@ -685,6 +687,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); $scope.isAdmin = Authentication.isAdmin(); + $scope.showDeviceMapping = await shouldShowDevices(); } function validateForm(accessControlData, isAdmin) { @@ -897,6 +900,19 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } } + async function shouldShowDevices() { + const isAdmin = Authentication.isAdmin(); + const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application; + + if (isAdmin || allowDeviceMappingForRegularUsers) { + return true; + } + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (rbacEnabled) { + return Authentication.hasAuthorizations(['EndpointResourcesAccess']); + } + } + initView(); }, ]); diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index e6d1ea848..39ba8be71 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -625,7 +625,7 @@
-
+
diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 49cdd0faa..6d4abe08d 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -7,13 +7,14 @@ export function SettingsViewModel(data) { this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers; this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers; this.AllowVolumeBrowserForRegularUsers = data.AllowVolumeBrowserForRegularUsers; + this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; + this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers; this.SnapshotInterval = data.SnapshotInterval; this.TemplatesURL = data.TemplatesURL; this.EnableHostManagementFeatures = data.EnableHostManagementFeatures; this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; this.UserSessionTimeout = data.UserSessionTimeout; - this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; } export function PublicSettingsViewModel(settings) { @@ -25,6 +26,7 @@ export function PublicSettingsViewModel(settings) { this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; this.LogoURL = settings.LogoURL; this.OAuthLoginURI = settings.OAuthLoginURI; + this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; } export function LDAPSettingsViewModel(data) { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 9f57c2959..f7ebc9ea5 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -79,6 +79,11 @@ angular.module('portainer.app').factory('StateManager', [ manager.updateAllowHostNamespaceForRegularUsers = function (allowHostNamespaceForRegularUsers) { state.application.allowHostNamespaceForRegularUsers = allowHostNamespaceForRegularUsers; LocalStorage.storeApplicationState(state.application); + } + + manager.updateAllowDeviceMappingForRegularUsers = function updateAllowDeviceMappingForRegularUsers(allowDeviceMappingForRegularUsers) { + state.application.allowDeviceMappingForRegularUsers = allowDeviceMappingForRegularUsers; + LocalStorage.storeApplicationState(state.application); }; function assignStateFromStatusAndSettings(status, settings) { @@ -89,6 +94,7 @@ angular.module('portainer.app').factory('StateManager', [ state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures; state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers; state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; + state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; state.application.validity = moment().unix(); } diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 0b3f4a848..b742af0fb 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -119,6 +119,16 @@
+
+
+ + +
+
diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 7328d1479..135bfb8a9 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -33,6 +33,7 @@ angular.module('portainer.app').controller('SettingsController', [ enableVolumeBrowser: false, enableEdgeComputeFeatures: false, restrictHostNamespaceForRegularUsers: false, + allowDeviceMappingForRegularUsers: false, }; $scope.removeFilteredContainerLabel = function (index) { @@ -66,6 +67,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures; settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures; settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers; + settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers; $scope.state.actionInProgress = true; updateSettings(settings); @@ -81,6 +83,7 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers); StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers); StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures); + StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers); $state.reload(); }) .catch(function error(err) { @@ -106,6 +109,7 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures; $scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; $scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers; + $scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings'); From fa9eeaf3b1807389bfde903950561d1664eb90cd Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Mon, 27 Jul 2020 10:11:32 +0300 Subject: [PATCH 094/195] feat(settings): introduce disable stack management setting (#4100) * feat(stacks): add a setting to disable the creation of stacks for non-admin users * feat(settings): introduce a setting to prevent non-admin from stack creation * feat(settings): update stack creation setting * feat(settings): fail stack creation if user is non admin * fix(settings): save preventStackCreation setting to state * feat(stacks): disable add button when settings is enabled * format(stacks): remove line * feat(stacks): setting to hide stacks from users * feat(settings): rename disable stacks setting * refactor(settings): rename setting to disableStackManagementForRegularUsers * feat(settings): hide stacks for non admin when settings is set * refactor(settings): replace disableDeviceMapping with allow * feat(dashboard): hide stacks if settings disabled and non admin * refactor(sidebar): check if user is endpoint admin * feat(settings): set the default value for stack management * feat(settings): rename field label * fix(sidebar): refresh show stacks state * fix(docker): hide stacks when not admin --- api/bolt/init.go | 21 +-- api/bolt/migrator/migrate_dbversion23.go | 1 + api/http/handler/settings/settings_public.go | 42 +++--- api/http/handler/settings/settings_update.go | 39 +++--- api/http/handler/stacks/handler.go | 32 ++--- api/http/handler/stacks/stack_create.go | 23 +++ api/portainer.go | 33 ++--- .../docker-sidebar-content.js | 1 + .../dockerSidebarContent.html | 2 +- app/docker/views/dashboard/dashboard.html | 2 +- .../views/dashboard/dashboardController.js | 22 ++- .../stacks-datatable/stacksDatatable.html | 9 +- .../stacks-datatable/stacksDatatable.js | 1 + app/portainer/models/settings.js | 4 +- app/portainer/services/stateManager.js | 6 + app/portainer/views/settings/settings.html | 10 ++ .../views/settings/settingsController.js | 4 + app/portainer/views/sidebar/sidebar.html | 1 + .../views/sidebar/sidebarController.js | 25 +++- app/portainer/views/stacks/stacks.html | 1 + .../views/stacks/stacksController.js | 132 ++++++++++-------- 21 files changed, 264 insertions(+), 147 deletions(-) diff --git a/api/bolt/init.go b/api/bolt/init.go index 1369e6a1e..885966bbe 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -24,16 +24,17 @@ func (store *Store) Init() error { portainer.LDAPGroupSearchSettings{}, }, }, - OAuthSettings: portainer.OAuthSettings{}, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowVolumeBrowserForRegularUsers: false, - AllowHostNamespaceForRegularUsers: true, - AllowDeviceMappingForRegularUsers: true, - EnableHostManagementFeatures: false, - EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, - TemplatesURL: portainer.DefaultTemplatesURL, - UserSessionTimeout: portainer.DefaultUserSessionTimeout, + OAuthSettings: portainer.OAuthSettings{}, + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowVolumeBrowserForRegularUsers: false, + AllowHostNamespaceForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + EnableHostManagementFeatures: false, + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + TemplatesURL: portainer.DefaultTemplatesURL, + UserSessionTimeout: portainer.DefaultUserSessionTimeout, } err = store.SettingsService.UpdateSettings(defaultSettings) diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go index ba38987c5..856e77856 100644 --- a/api/bolt/migrator/migrate_dbversion23.go +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -8,6 +8,7 @@ func (m *Migrator) updateSettingsToDB24() error { legacySettings.AllowHostNamespaceForRegularUsers = true legacySettings.AllowDeviceMappingForRegularUsers = true + legacySettings.AllowStackManagementForRegularUsers = true return m.settingsService.UpdateSettings(legacySettings) } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 097c14676..6bd3a581b 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -6,20 +6,21 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" ) type publicSettingsResponse struct { - LogoURL string `json:"LogoURL"` - AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - OAuthLoginURI string `json:"OAuthLoginURI"` + LogoURL string `json:"LogoURL"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + OAuthLoginURI string `json:"OAuthLoginURI"` } // GET request on /api/settings/public @@ -30,15 +31,16 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * } publicSettings := &publicSettingsResponse{ - LogoURL: settings.LogoURL, - AuthenticationMethod: settings.AuthenticationMethod, - AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, - AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, - AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, - AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, - AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, - EnableHostManagementFeatures: settings.EnableHostManagementFeatures, - EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, + LogoURL: settings.LogoURL, + AuthenticationMethod: settings.AuthenticationMethod, + AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, + AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, + AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, + EnableHostManagementFeatures: settings.EnableHostManagementFeatures, + EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", settings.OAuthSettings.AuthorizationURI, settings.OAuthSettings.ClientID, diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index d92307b08..59fbffd28 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -9,28 +9,29 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" "github.com/portainer/portainer/api/filesystem" ) type settingsUpdatePayload struct { - LogoURL *string - BlackListedLabels []portainer.Pair - AuthenticationMethod *int - LDAPSettings *portainer.LDAPSettings - OAuthSettings *portainer.OAuthSettings - AllowBindMountsForRegularUsers *bool - AllowPrivilegedModeForRegularUsers *bool - AllowHostNamespaceForRegularUsers *bool - AllowVolumeBrowserForRegularUsers *bool - AllowDeviceMappingForRegularUsers *bool - EnableHostManagementFeatures *bool - SnapshotInterval *string - TemplatesURL *string - EdgeAgentCheckinInterval *int - EnableEdgeComputeFeatures *bool - UserSessionTimeout *string + LogoURL *string + BlackListedLabels []portainer.Pair + AuthenticationMethod *int + LDAPSettings *portainer.LDAPSettings + OAuthSettings *portainer.OAuthSettings + AllowBindMountsForRegularUsers *bool + AllowPrivilegedModeForRegularUsers *bool + AllowHostNamespaceForRegularUsers *bool + AllowVolumeBrowserForRegularUsers *bool + AllowDeviceMappingForRegularUsers *bool + AllowStackManagementForRegularUsers *bool + EnableHostManagementFeatures *bool + SnapshotInterval *string + TemplatesURL *string + EdgeAgentCheckinInterval *int + EnableEdgeComputeFeatures *bool + UserSessionTimeout *string } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -131,6 +132,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers } + if payload.AllowStackManagementForRegularUsers != nil { + settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers + } + if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index 2be3ddc70..cf80b4ecd 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -58,8 +58,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) { - if securityContext.IsAdmin { - return true, nil + user, err := handler.DataStore.User().User(securityContext.UserID) + if err != nil { + return false, err } userTeamIDs := make([]portainer.TeamID, 0) @@ -71,23 +72,7 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR return true, nil } - _, err := handler.DataStore.Extension().Extension(portainer.RBACExtension) - if err == bolterrors.ErrObjectNotFound { - return false, nil - } else if err != nil && err != bolterrors.ErrObjectNotFound { - return false, err - } - - user, err := handler.DataStore.User().User(securityContext.UserID) - if err != nil { - return false, err - } - - _, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess] - if ok { - return true, nil - } - return false, nil + return handler.userIsAdminOrEndpointAdmin(user, endpointID) } func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) { @@ -109,3 +94,12 @@ func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpoin return endpointResourceAccess, nil } + +func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) { + user, err := handler.DataStore.User().User(securityContext.UserID) + if err != nil { + return false, err + } + + return handler.userIsAdminOrEndpointAdmin(user, endpointID) +} diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index ba3b5388d..23e18bf5c 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -46,6 +46,29 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} } + settings, err := handler.DataStore.Settings().Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + if !settings.AllowStackManagementForRegularUsers { + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err} + } + + canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID)) + + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err} + } + + if !canCreate { + errMsg := "Stack creation is disabled for non-admin users" + return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)} + } + } + endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/portainer.go b/api/portainer.go index d503e0bbf..a60f244bd 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -510,22 +510,23 @@ type ( // Settings represents the application settings Settings struct { - LogoURL string `json:"LogoURL"` - BlackListedLabels []Pair `json:"BlackListedLabels"` - AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` - LDAPSettings LDAPSettings `json:"LDAPSettings"` - OAuthSettings OAuthSettings `json:"OAuthSettings"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - UserSessionTimeout string `json:"UserSessionTimeout"` + LogoURL string `json:"LogoURL"` + BlackListedLabels []Pair `json:"BlackListedLabels"` + AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` + LDAPSettings LDAPSettings `json:"LDAPSettings"` + OAuthSettings OAuthSettings `json:"OAuthSettings"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + UserSessionTimeout string `json:"UserSessionTimeout"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js index 87734c8a2..088165ac3 100644 --- a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js +++ b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js @@ -9,5 +9,6 @@ angular.module('portainer.docker').component('dockerSidebarContent', { toggle: '<', currentRouteName: '<', endpointId: '<', + showStacks: '<', }, }); diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index 15d739ae2..5af425761 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -8,7 +8,7 @@ Custom Templates
-
-
+
diff --git a/app/docker/views/dashboard/dashboardController.js b/app/docker/views/dashboard/dashboardController.js index ea2980660..f0c9722a7 100644 --- a/app/docker/views/dashboard/dashboardController.js +++ b/app/docker/views/dashboard/dashboardController.js @@ -1,6 +1,7 @@ angular.module('portainer.docker').controller('DashboardController', [ '$scope', '$q', + 'Authentication', 'ContainerService', 'ImageService', 'NetworkService', @@ -11,10 +12,12 @@ angular.module('portainer.docker').controller('DashboardController', [ 'EndpointService', 'Notifications', 'EndpointProvider', + 'ExtensionService', 'StateManager', function ( $scope, $q, + Authentication, ContainerService, ImageService, NetworkService, @@ -25,6 +28,7 @@ angular.module('portainer.docker').controller('DashboardController', [ EndpointService, Notifications, EndpointProvider, + ExtensionService, StateManager ) { $scope.dismissInformationPanel = function (id) { @@ -32,12 +36,15 @@ angular.module('portainer.docker').controller('DashboardController', [ }; $scope.offlineMode = false; + $scope.showStacks = false; - function initView() { + async function initView() { const endpointMode = $scope.applicationState.endpoint.mode; const endpointId = EndpointProvider.endpointID(); $scope.endpointId = endpointId; + $scope.showStacks = await shouldShowStacks(); + $q.all({ containers: ContainerService.containers(1), images: ImageService.images(false), @@ -64,6 +71,19 @@ angular.module('portainer.docker').controller('DashboardController', [ }); } + async function shouldShowStacks() { + const isAdmin = Authentication.isAdmin(); + const { allowStackManagementForRegularUsers } = $scope.applicationState.application; + + if (isAdmin || allowStackManagementForRegularUsers) { + return true; + } + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (rbacEnabled) { + return Authentication.hasAuthorizations(['EndpointResourcesAccess']); + } + } + initView(); }, ]); diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html index 5e1f8b9b0..7b642041b 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html @@ -2,7 +2,7 @@
-
{{ $ctrl.titleText }}
+
{{ $ctrl.titleText }}
Settings @@ -52,7 +52,7 @@ > Remove -
@@ -144,7 +144,10 @@
+
+
+ +
diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 135bfb8a9..8a53a94a6 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -34,6 +34,7 @@ angular.module('portainer.app').controller('SettingsController', [ enableEdgeComputeFeatures: false, restrictHostNamespaceForRegularUsers: false, allowDeviceMappingForRegularUsers: false, + allowStackManagementForRegularUsers: false, }; $scope.removeFilteredContainerLabel = function (index) { @@ -68,6 +69,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures; settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers; settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers; + settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers; $scope.state.actionInProgress = true; updateSettings(settings); @@ -84,6 +86,7 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers); StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures); StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers); + StateManager.updateAllowStackManagementForRegularUsers(settings.AllowStackManagementForRegularUsers); $state.reload(); }) .catch(function error(err) { @@ -110,6 +113,7 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; $scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers; $scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers; + $scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings'); diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 9e7c3c0c2..efdba0a39 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -24,6 +24,7 @@ ng-if="applicationState.endpoint.mode && applicationState.endpoint.mode.provider !== 'AZURE' && applicationState.endpoint.mode.provider !== 'KUBERNETES'" current-route-name="$state.current.name" toggle="toggle" + show-stacks="showStacks" endpoint-api-version="applicationState.endpoint.apiVersion" swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'" standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE'" diff --git a/app/portainer/views/sidebar/sidebarController.js b/app/portainer/views/sidebar/sidebarController.js index 92e56ae62..fde87e3ec 100644 --- a/app/portainer/views/sidebar/sidebarController.js +++ b/app/portainer/views/sidebar/sidebarController.js @@ -7,7 +7,8 @@ angular.module('portainer.app').controller('SidebarController', [ 'Authentication', 'UserService', 'EndpointProvider', - function ($q, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) { + 'ExtensionService', + function ($q, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider, ExtensionService) { function checkPermissions(memberships) { var isLeader = false; angular.forEach(memberships, function (membership) { @@ -18,9 +19,10 @@ angular.module('portainer.app').controller('SidebarController', [ $scope.isTeamLeader = isLeader; } - function initView() { + async function initView() { $scope.uiVersion = StateManager.getState().application.version; $scope.logo = StateManager.getState().application.logo; + $scope.showStacks = await shouldShowStacks(); let userDetails = Authentication.getUserDetails(); let isAdmin = Authentication.isAdmin(); @@ -41,5 +43,24 @@ angular.module('portainer.app').controller('SidebarController', [ } initView(); + + async function shouldShowStacks() { + const isAdmin = Authentication.isAdmin(); + const { allowStackManagementForRegularUsers } = $scope.applicationState.application; + + if (isAdmin || allowStackManagementForRegularUsers) { + return true; + } + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (rbacEnabled) { + return Authentication.hasAuthorizations(['EndpointResourcesAccess']); + } + + return false; + } + + $transitions.onEnter({}, async () => { + $scope.showStacks = await shouldShowStacks(); + }); }, ]); diff --git a/app/portainer/views/stacks/stacks.html b/app/portainer/views/stacks/stacks.html index fcba0faa8..e0ef1bbd8 100644 --- a/app/portainer/views/stacks/stacks.html +++ b/app/portainer/views/stacks/stacks.html @@ -18,6 +18,7 @@ remove-action="removeAction" offline-mode="offlineMode" refresh-callback="getStacks" + create-enabled="createEnabled" >
diff --git a/app/portainer/views/stacks/stacksController.js b/app/portainer/views/stacks/stacksController.js index 51df14d77..d6811a894 100644 --- a/app/portainer/views/stacks/stacksController.js +++ b/app/portainer/views/stacks/stacksController.js @@ -1,65 +1,85 @@ -angular.module('portainer.app').controller('StacksController', [ - '$scope', - '$state', - 'Notifications', - 'StackService', - 'ModalService', - 'EndpointProvider', - function ($scope, $state, Notifications, StackService, ModalService, EndpointProvider) { - $scope.removeAction = function (selectedItems) { - ModalService.confirmDeletion('Do you want to remove the selected stack(s)? Associated services will be removed as well.', function onConfirm(confirmed) { - if (!confirmed) { - return; - } - deleteSelectedStacks(selectedItems); - }); - }; +angular.module('portainer.app').controller('StacksController', StacksController); - function deleteSelectedStacks(stacks) { - var endpointId = EndpointProvider.endpointID(); - var actionCount = stacks.length; - angular.forEach(stacks, function (stack) { - StackService.remove(stack, stack.External, endpointId) - .then(function success() { - Notifications.success('Stack successfully removed', stack.Name); - var index = $scope.stacks.indexOf(stack); - $scope.stacks.splice(index, 1); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name); - }) - .finally(function final() { - --actionCount; - if (actionCount === 0) { - $state.reload(); - } - }); - }); - } +/* @ngInject */ +function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, StateManager, ExtensionService) { + $scope.removeAction = function (selectedItems) { + ModalService.confirmDeletion('Do you want to remove the selected stack(s)? Associated services will be removed as well.', function onConfirm(confirmed) { + if (!confirmed) { + return; + } + deleteSelectedStacks(selectedItems); + }); + }; - $scope.offlineMode = false; - - $scope.getStacks = getStacks; - function getStacks() { - var endpointMode = $scope.applicationState.endpoint.mode; - var endpointId = EndpointProvider.endpointID(); - - StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId) - .then(function success(data) { - var stacks = data; - $scope.stacks = stacks; - $scope.offlineMode = EndpointProvider.offlineMode(); + function deleteSelectedStacks(stacks) { + var endpointId = EndpointProvider.endpointID(); + var actionCount = stacks.length; + angular.forEach(stacks, function (stack) { + StackService.remove(stack, stack.External, endpointId) + .then(function success() { + Notifications.success('Stack successfully removed', stack.Name); + var index = $scope.stacks.indexOf(stack); + $scope.stacks.splice(index, 1); }) .catch(function error(err) { - $scope.stacks = []; - Notifications.error('Failure', err, 'Unable to retrieve stacks'); + Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } }); + }); + } + + $scope.offlineMode = false; + $scope.createEnabled = false; + + $scope.getStacks = getStacks; + + function getStacks() { + var endpointMode = $scope.applicationState.endpoint.mode; + var endpointId = EndpointProvider.endpointID(); + + StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId) + .then(function success(data) { + var stacks = data; + $scope.stacks = stacks; + $scope.offlineMode = EndpointProvider.offlineMode(); + }) + .catch(function error(err) { + $scope.stacks = []; + Notifications.error('Failure', err, 'Unable to retrieve stacks'); + }); + } + + async function loadCreateEnabled() { + const appState = StateManager.getState().application; + if (appState.allowStackManagementForRegularUsers) { + return true; } - function initView() { - getStacks(); + let isAdmin = true; + if (appState.authentication) { + isAdmin = Authentication.isAdmin(); + } + if (isAdmin) { + return true; } - initView(); - }, -]); + const RBACExtensionEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (!RBACExtensionEnabled) { + return false; + } + + return Authentication.hasAuthorizations(['EndpointResourcesAccess']); + } + + async function initView() { + getStacks(); + $scope.createEnabled = await loadCreateEnabled(); + } + + initView(); +} From 1edf9813307e279f78428d52e172bfecc0792a07 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 28 Jul 2020 00:52:54 +0300 Subject: [PATCH 095/195] fix(container-creation): preselect network (#4117) --- .../views/containers/create/createContainerController.js | 5 ++--- app/docker/views/containers/create/createcontainer.html | 9 +++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 965364b72..412cfdadf 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -626,10 +626,9 @@ angular.module('portainer.docker').controller('CreateContainerController', [ var provider = $scope.applicationState.endpoint.mode.provider; var apiVersion = $scope.applicationState.endpoint.apiVersion; NetworkService.networks(provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', false, provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25) - .then(function success(data) { - var networks = data; + .then(function success(networks) { networks.push({ Name: 'container' }); - $scope.availableNetworks = networks; + $scope.availableNetworks = networks.sort((a, b) => a.Name.localeCompare(b.Name)); if (_.find(networks, { Name: 'nat' })) { $scope.config.HostConfig.NetworkMode = 'nat'; diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 39ba8be71..5ee68a229 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -394,9 +394,14 @@
- -
From 1ff5708183c0900b9123856b350546d051964f45 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 28 Jul 2020 00:53:21 +0300 Subject: [PATCH 096/195] fix(datatables): select table items (#4116) --- .../components/datatables/genericDatatableController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/portainer/components/datatables/genericDatatableController.js b/app/portainer/components/datatables/genericDatatableController.js index 21ccf17e9..62f4c960a 100644 --- a/app/portainer/components/datatables/genericDatatableController.js +++ b/app/portainer/components/datatables/genericDatatableController.js @@ -67,6 +67,7 @@ angular.module('portainer.app').controller('GenericDatatableController', [ }); this.state.firstClickedItem = item; } else if (event) { + item.Checked = true; this.state.firstClickedItem = item; } this.state.selectedItems = this.state.filteredDataSet.filter((i) => i.Checked); From fec85c77d68cf4b902549f5085991b4812817dca Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 28 Jul 2020 00:54:12 +0300 Subject: [PATCH 097/195] fix(extensions): load extensions file (#4115) --- api/portainer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/portainer.go b/api/portainer.go index a60f244bd..4f17208a3 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1152,7 +1152,7 @@ const ( // DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance DefaultEdgeAgentCheckinIntervalInSeconds = 5 // LocalExtensionManifestFile represents the name of the local manifest file for extensions - LocalExtensionManifestFile = "/extensions.json" + LocalExtensionManifestFile = "/app/extensions.json" // DefaultTemplatesURL represents the URL to the official templates supported by Portainer DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json" // DefaultUserSessionTimeout represents the default timeout after which the user session is cleared From 1a3f77137ad393098d3bd1296a8b567df04b1ace Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 28 Jul 2020 10:08:15 +0300 Subject: [PATCH 098/195] feat(settings): introduce setting to disable container caps for non-admins (#4109) * feat(settings): introduce settings to allow/disable * feat(settings): update the setting * feat(docker): prevent user from using caps if disabled * refactor(stacks): revert file * style(api): remove portainer ns --- api/bolt/init.go | 23 +++++----- api/bolt/migrator/migrate_dbversion24.go | 2 + api/http/handler/settings/settings_public.go | 44 ++++++++++--------- api/http/handler/settings/settings_update.go | 39 +++++++++------- .../handler/stacks/create_compose_stack.go | 3 +- api/http/handler/stacks/stack_create.go | 4 ++ api/http/proxy/factory/docker/containers.go | 6 +++ api/portainer.go | 35 ++++++++------- .../create/createContainerController.js | 32 +++++++++----- .../containers/create/createcontainer.html | 2 +- app/portainer/models/settings.js | 2 + app/portainer/services/stateManager.js | 10 ++++- app/portainer/views/settings/settings.html | 12 ++++- .../views/settings/settingsController.js | 4 ++ 14 files changed, 136 insertions(+), 82 deletions(-) diff --git a/api/bolt/init.go b/api/bolt/init.go index 885966bbe..29793a823 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -24,17 +24,18 @@ func (store *Store) Init() error { portainer.LDAPGroupSearchSettings{}, }, }, - OAuthSettings: portainer.OAuthSettings{}, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowVolumeBrowserForRegularUsers: false, - AllowHostNamespaceForRegularUsers: true, - AllowDeviceMappingForRegularUsers: true, - AllowStackManagementForRegularUsers: true, - EnableHostManagementFeatures: false, - EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, - TemplatesURL: portainer.DefaultTemplatesURL, - UserSessionTimeout: portainer.DefaultUserSessionTimeout, + OAuthSettings: portainer.OAuthSettings{}, + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowVolumeBrowserForRegularUsers: false, + AllowHostNamespaceForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + EnableHostManagementFeatures: false, + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + TemplatesURL: portainer.DefaultTemplatesURL, + UserSessionTimeout: portainer.DefaultUserSessionTimeout, } err = store.SettingsService.UpdateSettings(defaultSettings) diff --git a/api/bolt/migrator/migrate_dbversion24.go b/api/bolt/migrator/migrate_dbversion24.go index d1dc5f0cf..4749607c5 100644 --- a/api/bolt/migrator/migrate_dbversion24.go +++ b/api/bolt/migrator/migrate_dbversion24.go @@ -16,5 +16,7 @@ func (m *Migrator) updateSettingsToDB25() error { legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout + legacySettings.AllowContainerCapabilitiesForRegularUsers = true + return m.settingsService.UpdateSettings(legacySettings) } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 6bd3a581b..f0d4f422a 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -10,17 +10,18 @@ import ( ) type publicSettingsResponse struct { - LogoURL string `json:"LogoURL"` - AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - OAuthLoginURI string `json:"OAuthLoginURI"` + LogoURL string `json:"LogoURL"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + OAuthLoginURI string `json:"OAuthLoginURI"` } // GET request on /api/settings/public @@ -31,16 +32,17 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * } publicSettings := &publicSettingsResponse{ - LogoURL: settings.LogoURL, - AuthenticationMethod: settings.AuthenticationMethod, - AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, - AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, - AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, - AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, - AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, - AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, - EnableHostManagementFeatures: settings.EnableHostManagementFeatures, - EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, + LogoURL: settings.LogoURL, + AuthenticationMethod: settings.AuthenticationMethod, + AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, + AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, + AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, + AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers, + EnableHostManagementFeatures: settings.EnableHostManagementFeatures, + EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", settings.OAuthSettings.AuthorizationURI, settings.OAuthSettings.ClientID, diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 59fbffd28..7b2a74f0e 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -15,23 +15,24 @@ import ( ) type settingsUpdatePayload struct { - LogoURL *string - BlackListedLabels []portainer.Pair - AuthenticationMethod *int - LDAPSettings *portainer.LDAPSettings - OAuthSettings *portainer.OAuthSettings - AllowBindMountsForRegularUsers *bool - AllowPrivilegedModeForRegularUsers *bool - AllowHostNamespaceForRegularUsers *bool - AllowVolumeBrowserForRegularUsers *bool - AllowDeviceMappingForRegularUsers *bool - AllowStackManagementForRegularUsers *bool - EnableHostManagementFeatures *bool - SnapshotInterval *string - TemplatesURL *string - EdgeAgentCheckinInterval *int - EnableEdgeComputeFeatures *bool - UserSessionTimeout *string + LogoURL *string + BlackListedLabels []portainer.Pair + AuthenticationMethod *int + LDAPSettings *portainer.LDAPSettings + OAuthSettings *portainer.OAuthSettings + AllowBindMountsForRegularUsers *bool + AllowPrivilegedModeForRegularUsers *bool + AllowHostNamespaceForRegularUsers *bool + AllowVolumeBrowserForRegularUsers *bool + AllowDeviceMappingForRegularUsers *bool + AllowStackManagementForRegularUsers *bool + AllowContainerCapabilitiesForRegularUsers *bool + EnableHostManagementFeatures *bool + SnapshotInterval *string + TemplatesURL *string + EdgeAgentCheckinInterval *int + EnableEdgeComputeFeatures *bool + UserSessionTimeout *string } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -136,6 +137,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers } + if payload.AllowContainerCapabilitiesForRegularUsers != nil { + settings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers + } + if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index af79889f4..aa47eacd7 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -339,7 +339,8 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers || - !settings.AllowDeviceMappingForRegularUsers) && + !settings.AllowDeviceMappingForRegularUsers || + !settings.AllowContainerCapabilitiesForRegularUsers) && !isAdminOrEndpointAdmin { composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 23e18bf5c..daec00366 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -173,6 +173,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 { return errors.New("device mapping disabled for non administrator users") } + + if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { + return errors.New("container capabilities disabled for non administrator users") + } } return nil diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index d957be030..df723668c 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -161,6 +161,8 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req Privileged bool `json:"Privileged"` PidMode string `json:"PidMode"` Devices []interface{} `json:"Devices"` + CapAdd []string `json:"CapAdd"` + CapDrop []string `json:"CapDrop"` } `json:"HostConfig"` } @@ -220,6 +222,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, errors.New("forbidden to use device mapping") } + if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { + return nil, errors.New("forbidden to use container capabilities") + } + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) } diff --git a/api/portainer.go b/api/portainer.go index 4f17208a3..c5a0db217 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -510,23 +510,24 @@ type ( // Settings represents the application settings Settings struct { - LogoURL string `json:"LogoURL"` - BlackListedLabels []Pair `json:"BlackListedLabels"` - AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` - LDAPSettings LDAPSettings `json:"LDAPSettings"` - OAuthSettings OAuthSettings `json:"OAuthSettings"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - UserSessionTimeout string `json:"UserSessionTimeout"` + LogoURL string `json:"LogoURL"` + BlackListedLabels []Pair `json:"BlackListedLabels"` + AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` + LDAPSettings LDAPSettings `json:"LDAPSettings"` + OAuthSettings OAuthSettings `json:"OAuthSettings"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` + SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + UserSessionTimeout string `json:"UserSessionTimeout"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 412cfdadf..4ce386800 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -611,6 +611,10 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.formValues.NodeName = nodeName; HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + $scope.isAdmin = Authentication.isAdmin(); + $scope.showDeviceMapping = await shouldShowDevices(); + $scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled(); + Volume.query( {}, function (d) { @@ -647,7 +651,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ loadFromContainerSpec(); } else { $scope.fromContainer = {}; - $scope.formValues.capabilities = new ContainerCapabilities(); + $scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : []; } }, function (e) { @@ -684,9 +688,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) { $scope.availableLoggingDrivers = loggingDrivers; }); - - $scope.isAdmin = Authentication.isAdmin(); - $scope.showDeviceMapping = await shouldShowDevices(); } function validateForm(accessControlData, isAdmin) { @@ -899,17 +900,26 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } } - async function shouldShowDevices() { + async function isAdminOrEndpointAdmin() { const isAdmin = Authentication.isAdmin(); - const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application; - - if (isAdmin || allowDeviceMappingForRegularUsers) { + if (isAdmin) { return true; } + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); - if (rbacEnabled) { - return Authentication.hasAuthorizations(['EndpointResourcesAccess']); - } + return rbacEnabled ? Authentication.hasAuthorizations(['EndpointResourcesAccess']) : false; + } + + async function shouldShowDevices() { + const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application; + + return allowDeviceMappingForRegularUsers || isAdminOrEndpointAdmin(); + } + + async function checkIfContainerCapabilitiesEnabled() { + const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application; + + return allowContainerCapabilitiesForRegularUsers || isAdminOrEndpointAdmin(); } initView(); diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 5ee68a229..eb1d70aa4 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -181,7 +181,7 @@
  • Labels
  • Restart policy
  • Runtime & Resources
  • -
  • Capabilities
  • +
  • Capabilities
  • diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index e7003b314..bdd6c35a5 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -10,6 +10,7 @@ export function SettingsViewModel(data) { this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers; this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers; this.AllowStackManagementForRegularUsers = data.AllowStackManagementForRegularUsers; + this.AllowContainerCapabilitiesForRegularUsers = data.AllowContainerCapabilitiesForRegularUsers; this.SnapshotInterval = data.SnapshotInterval; this.TemplatesURL = data.TemplatesURL; this.EnableHostManagementFeatures = data.EnableHostManagementFeatures; @@ -24,6 +25,7 @@ export function PublicSettingsViewModel(settings) { this.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers; this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; this.AllowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers; + this.AllowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers; this.AuthenticationMethod = settings.AuthenticationMethod; this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures; this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 37aeeb4bf..dcfdc36a6 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -79,8 +79,8 @@ angular.module('portainer.app').factory('StateManager', [ manager.updateAllowHostNamespaceForRegularUsers = function (allowHostNamespaceForRegularUsers) { state.application.allowHostNamespaceForRegularUsers = allowHostNamespaceForRegularUsers; LocalStorage.storeApplicationState(state.application); - } - + }; + manager.updateAllowDeviceMappingForRegularUsers = function updateAllowDeviceMappingForRegularUsers(allowDeviceMappingForRegularUsers) { state.application.allowDeviceMappingForRegularUsers = allowDeviceMappingForRegularUsers; LocalStorage.storeApplicationState(state.application); @@ -91,6 +91,11 @@ angular.module('portainer.app').factory('StateManager', [ LocalStorage.storeApplicationState(state.application); }; + manager.updateAllowContainerCapabilitiesForRegularUsers = function updateAllowContainerCapabilitiesForRegularUsers(allowContainerCapabilitiesForRegularUsers) { + state.application.allowContainerCapabilitiesForRegularUsers = allowContainerCapabilitiesForRegularUsers; + LocalStorage.storeApplicationState(state.application); + }; + function assignStateFromStatusAndSettings(status, settings) { state.application.analytics = status.Analytics; state.application.version = status.Version; @@ -101,6 +106,7 @@ angular.module('portainer.app').factory('StateManager', [ state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; state.application.allowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers; + state.application.allowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers; state.application.validity = moment().unix(); } diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 7606f6c2e..183fd0c90 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -116,7 +116,7 @@ +
    @@ -139,6 +139,16 @@
    +
    +
    + + +
    +
    diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 8a53a94a6..8bdfcb420 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -35,6 +35,7 @@ angular.module('portainer.app').controller('SettingsController', [ restrictHostNamespaceForRegularUsers: false, allowDeviceMappingForRegularUsers: false, allowStackManagementForRegularUsers: false, + disableContainerCapabilitiesForRegularUsers: false, }; $scope.removeFilteredContainerLabel = function (index) { @@ -70,6 +71,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers; settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers; settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers; + settings.AllowContainerCapabilitiesForRegularUsers = !$scope.formValues.disableContainerCapabilitiesForRegularUsers; $scope.state.actionInProgress = true; updateSettings(settings); @@ -87,6 +89,7 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures); StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers); StateManager.updateAllowStackManagementForRegularUsers(settings.AllowStackManagementForRegularUsers); + StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers); $state.reload(); }) .catch(function error(err) { @@ -114,6 +117,7 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers; $scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers; $scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers; + $scope.formValues.disableContainerCapabilitiesForRegularUsers = !settings.AllowContainerCapabilitiesForRegularUsers; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings'); From 7539f09f980c454ca491c3cca33d74493d7123c8 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 29 Jul 2020 05:52:23 +0300 Subject: [PATCH 099/195] feat(containers): disable edit container on security setting restricting regular users (#4111) * feat(settings): add info about container edit disable * feat(settings): set security settings * feat(containers): hide recreate button when setting is enabled * feat(settings): rephrase security notice * fix(settings): save allowHostNamespaceForRegularUsers to state --- .../containers/edit/containerController.js | 21 +++++++++++++++++-- app/portainer/models/settings.js | 1 + app/portainer/services/stateManager.js | 13 ++++++++++++ app/portainer/views/settings/settings.html | 6 ++++++ .../views/settings/settingsController.js | 15 +++++++++++++ 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index 6528669f1..aee0f07f1 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -21,6 +21,7 @@ angular.module('portainer.docker').controller('ContainerController', [ 'ImageService', 'HttpRequestHelper', 'Authentication', + 'StateManager', function ( $q, $scope, @@ -40,7 +41,8 @@ angular.module('portainer.docker').controller('ContainerController', [ RegistryService, ImageService, HttpRequestHelper, - Authentication + Authentication, + StateManager ) { $scope.activityTime = 0; $scope.portBindings = []; @@ -94,9 +96,24 @@ angular.module('portainer.docker').controller('ContainerController', [ const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id']; const autoRemove = $scope.container.HostConfig.AutoRemove; const admin = Authentication.isAdmin(); + const appState = StateManager.getState(); + const { + allowContainerCapabilitiesForRegularUsers, + allowHostNamespaceForRegularUsers, + allowDeviceMappingForRegularUsers, + allowBindMountsForRegularUsers, + allowPrivilegedModeForRegularUsers, + } = appState.application; + + const settingRestrictsRegularUsers = + !allowContainerCapabilitiesForRegularUsers || + !allowBindMountsForRegularUsers || + !allowDeviceMappingForRegularUsers || + !allowHostNamespaceForRegularUsers || + !allowPrivilegedModeForRegularUsers; ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then((rbacEnabled) => { - $scope.displayRecreateButton = !inSwarm && !autoRemove && (rbacEnabled ? admin : true); + $scope.displayRecreateButton = !inSwarm && !autoRemove && (settingRestrictsRegularUsers || rbacEnabled ? admin : true); }); }) .catch(function error(err) { diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index bdd6c35a5..bec5e7894 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -26,6 +26,7 @@ export function PublicSettingsViewModel(settings) { this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; this.AllowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers; this.AllowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers; + this.AllowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers; this.AuthenticationMethod = settings.AuthenticationMethod; this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures; this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index dcfdc36a6..7b28dfc86 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -96,6 +96,16 @@ angular.module('portainer.app').factory('StateManager', [ LocalStorage.storeApplicationState(state.application); }; + manager.updateAllowBindMountsForRegularUsers = function updateAllowBindMountsForRegularUsers(allowBindMountsForRegularUsers) { + state.application.allowBindMountsForRegularUsers = allowBindMountsForRegularUsers; + LocalStorage.storeApplicationState(state.application); + }; + + manager.updateAllowPrivilegedModeForRegularUsers = function (AllowPrivilegedModeForRegularUsers) { + state.application.allowPrivilegedModeForRegularUsers = AllowPrivilegedModeForRegularUsers; + LocalStorage.storeApplicationState(state.application); + }; + function assignStateFromStatusAndSettings(status, settings) { state.application.analytics = status.Analytics; state.application.version = status.Version; @@ -107,6 +117,9 @@ angular.module('portainer.app').factory('StateManager', [ state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers; state.application.allowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers; state.application.allowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers; + state.application.allowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers; + state.application.allowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers; + state.application.allowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers; state.application.validity = moment().unix(); } diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index 183fd0c90..b9e3c97cf 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -149,6 +149,12 @@
    + +
    + + Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings. + +
    diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 8bdfcb420..09e6f862a 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -38,6 +38,19 @@ angular.module('portainer.app').controller('SettingsController', [ disableContainerCapabilitiesForRegularUsers: false, }; + $scope.isContainerEditDisabled = function isContainerEditDisabled() { + const { + restrictBindMounts, + restrictHostNamespaceForRegularUsers, + restrictPrivilegedMode, + disableDeviceMappingForRegularUsers, + disableContainerCapabilitiesForRegularUsers, + } = this.formValues; + return ( + restrictBindMounts || restrictHostNamespaceForRegularUsers || restrictPrivilegedMode || disableDeviceMappingForRegularUsers || disableContainerCapabilitiesForRegularUsers + ); + }; + $scope.removeFilteredContainerLabel = function (index) { var settings = $scope.settings; settings.BlackListedLabels.splice(index, 1); @@ -90,6 +103,8 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers); StateManager.updateAllowStackManagementForRegularUsers(settings.AllowStackManagementForRegularUsers); StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers); + StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers); + StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers); $state.reload(); }) .catch(function error(err) { From 93d8c179f1b7483d42c95ad946cc1cc7297c9947 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 29 Jul 2020 12:10:46 +0300 Subject: [PATCH 100/195] feat(containers): enforce disable bind mounts (#4110) * feat(containers): enforce disable bind mounts * refactor(docker): move check for endpoint admin to a function * feat(docker): check if service has bind mounts * feat(services): allow bind mounts for endpoint admin * feat(container): enable bind mounts for endpoint admin * fix(services): fix typo --- api/http/proxy/factory/docker/containers.go | 27 +++------ api/http/proxy/factory/docker/services.go | 55 +++++++++++++++++++ api/http/proxy/factory/docker/transport.go | 32 ++++++++++- .../create/createContainerController.js | 12 +++- .../containers/create/createcontainer.html | 4 +- .../create/createServiceController.js | 27 ++++++++- .../views/services/create/createservice.html | 2 +- 7 files changed, 132 insertions(+), 27 deletions(-) diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index df723668c..3f9ecf9a1 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -9,8 +9,7 @@ import ( "net/http" "github.com/docker/docker/client" - portainer "github.com/portainer/portainer/api" - bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" @@ -163,6 +162,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req Devices []interface{} `json:"Devices"` CapAdd []string `json:"CapAdd"` CapDrop []string `json:"CapDrop"` + Binds []string `json:"Binds"` } `json:"HostConfig"` } @@ -175,25 +175,12 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, err } - user, err := transport.dataStore.User().User(tokenData.ID) + isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request) if err != nil { return nil, err } - rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension) - if err != nil && err != bolterrors.ErrObjectNotFound { - return nil, err - } - - endpointResourceAccess := false - _, ok := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess] - if ok { - endpointResourceAccess = true - } - - isAdmin := (rbacExtension != nil && endpointResourceAccess) || tokenData.Role == portainer.AdministratorRole - - if !isAdmin { + if !isAdminOrEndpointAdmin { settings, err := transport.dataStore.Settings().Settings() if err != nil { return nil, err @@ -219,13 +206,17 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req } if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { - return nil, errors.New("forbidden to use device mapping") + return forbiddenResponse, errors.New("forbidden to use device mapping") } if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { return nil, errors.New("forbidden to use container capabilities") } + if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) { + return forbiddenResponse, errors.New("forbidden to use bind mounts") + } + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) } diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go index 8863ea3fd..08f01a23c 100644 --- a/api/http/proxy/factory/docker/services.go +++ b/api/http/proxy/factory/docker/services.go @@ -1,7 +1,11 @@ package docker import ( + "bytes" "context" + "encoding/json" + "errors" + "io/ioutil" "net/http" "github.com/docker/docker/api/types" @@ -85,3 +89,54 @@ func selectorServiceLabels(responseObject map[string]interface{}) map[string]int } return nil } + +func (transport *Transport) decorateServiceCreationOperation(request *http.Request) (*http.Response, error) { + type PartialService struct { + TaskTemplate struct { + ContainerSpec struct { + Mounts []struct { + Type string + } + } + } + } + + forbiddenResponse := &http.Response{ + StatusCode: http.StatusForbidden, + } + + isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request) + if err != nil { + return nil, err + } + + if !isAdminOrEndpointAdmin { + settings, err := transport.dataStore.Settings().Settings() + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, err + } + + partialService := &PartialService{} + err = json.Unmarshal(body, partialService) + if err != nil { + return nil, err + } + + if !settings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) { + for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts { + if mount.Type == "bind" { + return forbiddenResponse, errors.New("forbidden to use bind mounts") + } + } + } + + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + } + + return transport.replaceRegistryAuthenticationHeader(request) +} diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 6ecfb4615..604d05d65 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -225,7 +225,7 @@ func (transport *Transport) proxyContainerRequest(request *http.Request) (*http. func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) { switch requestPath := request.URL.Path; requestPath { case "/services/create": - return transport.replaceRegistryAuthenticationHeader(request) + return transport.decorateServiceCreationOperation(request) case "/services": return transport.rewriteOperation(request, transport.serviceListOperation) @@ -629,7 +629,6 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) ( return nil, err } - accessContext := ®istryAccessContext{ isAdmin: true, userID: tokenData.ID, @@ -707,3 +706,32 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest return operationContext, nil } + +func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return false, err + } + + if tokenData.Role == portainer.AdministratorRole { + return true, nil + } + + user, err := transport.dataStore.User().User(tokenData.ID) + if err != nil { + return false, err + } + + rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension) + if err != nil && err != bolterrors.ErrObjectNotFound { + return false, err + } + + if rbacExtension == nil { + return false, nil + } + + _, endpointResourceAccess := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess] + + return endpointResourceAccess, nil +} diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 4ce386800..2d69e3fb2 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -614,6 +614,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.isAdmin = Authentication.isAdmin(); $scope.showDeviceMapping = await shouldShowDevices(); $scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled(); + $scope.isAdminOrEndpointAdmin = await checkIfAdminOrEndpointAdmin(); Volume.query( {}, @@ -678,7 +679,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ SettingsService.publicSettings() .then(function success(data) { - $scope.allowBindMounts = data.AllowBindMountsForRegularUsers; + $scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || data.AllowBindMountsForRegularUsers; $scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers; }) .catch(function error(err) { @@ -922,6 +923,15 @@ angular.module('portainer.docker').controller('CreateContainerController', [ return allowContainerCapabilitiesForRegularUsers || isAdminOrEndpointAdmin(); } + async function checkIfAdminOrEndpointAdmin() { + if (Authentication.isAdmin()) { + return true; + } + + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + return rbacEnabled ? Authentication.hasAuthorizations(['EndpointResourcesAccess']) : false; + } + initView(); }, ]); diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index eb1d70aa4..14e0a89ef 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -334,8 +334,8 @@
    -
    -
    +
    +
    diff --git a/app/docker/views/services/create/createServiceController.js b/app/docker/views/services/create/createServiceController.js index 7500f5371..facba38b5 100644 --- a/app/docker/views/services/create/createServiceController.js +++ b/app/docker/views/services/create/createServiceController.js @@ -33,6 +33,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [ 'SettingsService', 'WebhookService', 'EndpointProvider', + 'ExtensionService', function ( $q, $scope, @@ -58,7 +59,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [ NodeService, SettingsService, WebhookService, - EndpointProvider + EndpointProvider, + ExtensionService ) { $scope.formValues = { Name: '', @@ -106,6 +108,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [ actionInProgress: false, }; + $scope.allowBindMounts = false; + $scope.refreshSlider = function () { $timeout(function () { $scope.$broadcast('rzSliderForceRender'); @@ -562,8 +566,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [ secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], configs: apiVersion >= 1.3 ? ConfigService.configs() : [], nodes: NodeService.nodes(), - settings: SettingsService.publicSettings(), availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25), + allowBindMounts: checkIfAllowedBindMounts(), }) .then(function success(data) { $scope.availableVolumes = data.volumes; @@ -572,8 +576,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [ $scope.availableConfigs = data.configs; $scope.availableLoggingDrivers = data.availableLoggingDrivers; initSlidersMaxValuesBasedOnNodeData(data.nodes); - $scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers; $scope.isAdmin = Authentication.isAdmin(); + $scope.allowBindMounts = data.allowBindMounts; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to initialize view'); @@ -581,5 +585,22 @@ angular.module('portainer.docker').controller('CreateServiceController', [ } initView(); + + async function checkIfAllowedBindMounts() { + const isAdmin = Authentication.isAdmin(); + + const settings = await SettingsService.publicSettings(); + const { AllowBindMountsForRegularUsers } = settings; + + if (isAdmin || AllowBindMountsForRegularUsers) { + return true; + } + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (rbacEnabled) { + return Authentication.hasAuthorizations(['EndpointResourcesAccess']); + } + + return false; + } }, ]); diff --git a/app/docker/views/services/create/createservice.html b/app/docker/views/services/create/createservice.html index 639274cc8..cc304b3e1 100644 --- a/app/docker/views/services/create/createservice.html +++ b/app/docker/views/services/create/createservice.html @@ -305,7 +305,7 @@
    -
    +
    From 63bf654d8d0ec85a86d831cb66962cf35a7303ab Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 30 Jul 2020 00:53:57 +0300 Subject: [PATCH 101/195] fix(serverless/ACI): show container instance title (#4126) --- .../container-instance-details/containerInstanceDetails.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html b/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html index 88c854516..37940a149 100644 --- a/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html +++ b/app/azure/views/containerinstances/container-instance-details/containerInstanceDetails.html @@ -1,5 +1,5 @@ - + Container instances > {{ $ctrl.container.Name }} From 4431d748c2c4c4a6d5de76a821e23dc547d011f7 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Thu, 30 Jul 2020 00:25:59 +0200 Subject: [PATCH 102/195] feat(k8s/application): expose tolerations and affinities (#4063) * feat(k8s/application): expose placement conditions * feat(k8s/applications): minor UI update * feat(k8s/application): update message for admin and non admin users * feat(kubernetes/applications): minor UI update Co-authored-by: Anthony Lapenna --- app/assets/css/app.css | 33 +++- .../applicationsStacksDatatableController.js | 2 +- app/kubernetes/converters/pod.js | 32 --- app/kubernetes/filters/applicationFilters.js | 20 ++ .../models/application/models/constants.js | 40 ++++ .../{models.js => models/index.js} | 41 +--- app/kubernetes/models/pod/models.js | 21 -- .../{converters/node.js => node/converter.js} | 4 +- .../nodeFilters.js => node/filters.js} | 0 app/kubernetes/{models => }/node/models.js | 2 + app/kubernetes/{rest/node.js => node/rest.js} | 0 .../nodeService.js => node/service.js} | 2 +- app/kubernetes/pod/converter.js | 54 +++++ .../{filters/podFilters.js => pod/filters.js} | 0 app/kubernetes/pod/models/affinities.js | 65 +++++++ app/kubernetes/pod/models/index.js | 42 ++++ .../podService.js => pod/service.js} | 4 +- .../views/applications/edit/application.html | 20 +- .../edit/applicationController.js | 95 ++++++++- .../placements-datatable/controller.js | 71 +++++++ .../components/placements-datatable/index.js | 15 ++ .../placements-datatable/template.html | 184 ++++++++++++++++++ 22 files changed, 635 insertions(+), 112 deletions(-) delete mode 100644 app/kubernetes/converters/pod.js create mode 100644 app/kubernetes/models/application/models/constants.js rename app/kubernetes/models/application/{models.js => models/index.js} (68%) delete mode 100644 app/kubernetes/models/pod/models.js rename app/kubernetes/{converters/node.js => node/converter.js} (96%) rename app/kubernetes/{filters/nodeFilters.js => node/filters.js} (100%) rename app/kubernetes/{models => }/node/models.js (96%) rename app/kubernetes/{rest/node.js => node/rest.js} (100%) rename app/kubernetes/{services/nodeService.js => node/service.js} (95%) create mode 100644 app/kubernetes/pod/converter.js rename app/kubernetes/{filters/podFilters.js => pod/filters.js} (100%) create mode 100644 app/kubernetes/pod/models/affinities.js create mode 100644 app/kubernetes/pod/models/index.js rename app/kubernetes/{services/podService.js => pod/service.js} (95%) create mode 100644 app/kubernetes/views/applications/edit/components/placements-datatable/controller.js create mode 100644 app/kubernetes/views/applications/edit/components/placements-datatable/index.js create mode 100644 app/kubernetes/views/applications/edit/components/placements-datatable/template.html diff --git a/app/assets/css/app.css b/app/assets/css/app.css index b521dd890..cdb30d4ac 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -599,10 +599,6 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { padding-left: 0; } -.switch input { - display: none; -} - .small-select { display: inline-block; padding: 0px 6px; @@ -618,17 +614,26 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { margin-left: 21px; } +/* switch box */ +:root { + --switch-size: 24px; +} + +.switch input { + display: none; +} + .switch i, .bootbox-form .checkbox i { display: inline-block; vertical-align: middle; cursor: pointer; - padding-right: 24px; + padding-right: var(--switch-size); transition: all ease 0.2s; -webkit-transition: all ease 0.2s; -moz-transition: all ease 0.2s; -o-transition: all ease 0.2s; - border-radius: 24px; + border-radius: var(--switch-size); box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, 0.5); } @@ -636,9 +641,9 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { .bootbox-form .checkbox i:before { display: block; content: ''; - width: 24px; - height: 24px; - border-radius: 24px; + width: var(--switch-size); + height: var(--switch-size); + border-radius: var(--switch-size); background: white; box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.5); } @@ -646,11 +651,19 @@ ul.sidebar .sidebar-list .sidebar-sublist a.active { .switch :checked + i, .bootbox-form .checkbox :checked ~ i { padding-right: 0; - padding-left: 24px; + padding-left: var(--switch-size); -webkit-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; -moz-box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.5), inset 0 0 40px #337ab7; } +/* !switch box */ + +/* small switch box */ +.switch.small { + --switch-size: 12px; +} + +/* !small switch box */ .boxselector_wrapper { display: flex; diff --git a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js index cc77ba205..faeef91c3 100644 --- a/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js +++ b/app/kubernetes/components/datatables/applications-stacks-datatable/applicationsStacksDatatableController.js @@ -21,7 +21,7 @@ angular.module('portainer.docker').controller('KubernetesApplicationsStacksDatat showSystem: false, }); - this.onSettingsRepeaterChange = function () { + this.onSettingsShowSystemChange = function () { DatatableService.setDataTableSettings(this.tableKey, this.settings); }; diff --git a/app/kubernetes/converters/pod.js b/app/kubernetes/converters/pod.js deleted file mode 100644 index af3d7713b..000000000 --- a/app/kubernetes/converters/pod.js +++ /dev/null @@ -1,32 +0,0 @@ -import _ from 'lodash-es'; -import { KubernetesPod } from 'Kubernetes/models/pod/models'; -class KubernetesPodConverter { - static computeStatus(statuses) { - const containerStatuses = _.map(statuses, 'state'); - const running = _.filter(containerStatuses, (s) => s.running).length; - const waiting = _.filter(containerStatuses, (s) => s.waiting).length; - if (waiting) { - return 'Waiting'; - } else if (!running) { - return 'Terminated'; - } - return 'Running'; - } - - static apiToPod(data) { - const res = new KubernetesPod(); - res.Id = data.metadata.uid; - res.Name = data.metadata.name; - res.Namespace = data.metadata.namespace; - res.Images = _.map(data.spec.containers, 'image'); - res.Status = KubernetesPodConverter.computeStatus(data.status.containerStatuses); - res.Restarts = _.sumBy(data.status.containerStatuses, 'restartCount'); - res.Node = data.spec.nodeName; - res.CreationDate = data.status.startTime; - res.Containers = data.spec.containers; - res.Labels = data.metadata.labels; - return res; - } -} - -export default KubernetesPodConverter; diff --git a/app/kubernetes/filters/applicationFilters.js b/app/kubernetes/filters/applicationFilters.js index d0f37f78c..b58e783cb 100644 --- a/app/kubernetes/filters/applicationFilters.js +++ b/app/kubernetes/filters/applicationFilters.js @@ -2,6 +2,7 @@ import _ from 'lodash-es'; import { KubernetesApplicationDataAccessPolicies } from 'Kubernetes/models/application/models'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import { KubernetesApplicationTypes, KubernetesApplicationTypeStrings } from 'Kubernetes/models/application/models'; +import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models'; angular .module('portainer.kubernetes') @@ -99,4 +100,23 @@ angular return 'All the instances of this application are sharing the same data.'; } }; + }) + .filter('kubernetesApplicationConstraintNodeAffinityValue', function () { + 'use strict'; + return function (values, operator) { + if (operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN || operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN) { + return values; + } else if ( + operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS || + operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST + ) { + return ''; + } else if ( + operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN || + operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN + ) { + return values[0]; + } + return ''; + }; }); diff --git a/app/kubernetes/models/application/models/constants.js b/app/kubernetes/models/application/models/constants.js new file mode 100644 index 000000000..f8cfa0ff1 --- /dev/null +++ b/app/kubernetes/models/application/models/constants.js @@ -0,0 +1,40 @@ +export const KubernetesApplicationDeploymentTypes = Object.freeze({ + REPLICATED: 1, + GLOBAL: 2, +}); + +export const KubernetesApplicationDataAccessPolicies = Object.freeze({ + SHARED: 1, + ISOLATED: 2, +}); + +export const KubernetesApplicationTypes = Object.freeze({ + DEPLOYMENT: 1, + DAEMONSET: 2, + STATEFULSET: 3, +}); + +export const KubernetesApplicationTypeStrings = Object.freeze({ + DEPLOYMENT: 'Deployment', + DAEMONSET: 'DaemonSet', + STATEFULSET: 'StatefulSet', +}); + +export const KubernetesApplicationPublishingTypes = Object.freeze({ + INTERNAL: 1, + CLUSTER: 2, + LOAD_BALANCER: 3, +}); + +export const KubernetesApplicationQuotaDefaults = { + CpuLimit: 0.1, + MemoryLimit: 64, // MB +}; + +export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack'; + +export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name'; + +export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner'; + +export const KubernetesPortainerApplicationNote = 'io.portainer.kubernetes.application.note'; diff --git a/app/kubernetes/models/application/models.js b/app/kubernetes/models/application/models/index.js similarity index 68% rename from app/kubernetes/models/application/models.js rename to app/kubernetes/models/application/models/index.js index 32a953091..dbd7b8fbb 100644 --- a/app/kubernetes/models/application/models.js +++ b/app/kubernetes/models/application/models/index.js @@ -1,43 +1,4 @@ -export const KubernetesApplicationDeploymentTypes = Object.freeze({ - REPLICATED: 1, - GLOBAL: 2, -}); - -export const KubernetesApplicationDataAccessPolicies = Object.freeze({ - SHARED: 1, - ISOLATED: 2, -}); - -export const KubernetesApplicationTypes = Object.freeze({ - DEPLOYMENT: 1, - DAEMONSET: 2, - STATEFULSET: 3, -}); - -export const KubernetesApplicationTypeStrings = Object.freeze({ - DEPLOYMENT: 'Deployment', - DAEMONSET: 'DaemonSet', - STATEFULSET: 'StatefulSet', -}); - -export const KubernetesApplicationPublishingTypes = Object.freeze({ - INTERNAL: 1, - CLUSTER: 2, - LOAD_BALANCER: 3, -}); - -export const KubernetesApplicationQuotaDefaults = { - CpuLimit: 0.1, - MemoryLimit: 64, // MB -}; - -export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack'; - -export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name'; - -export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner'; - -export const KubernetesPortainerApplicationNote = 'io.portainer.kubernetes.application.note'; +export * from './constants'; /** * KubernetesApplication Model (Composite) diff --git a/app/kubernetes/models/pod/models.js b/app/kubernetes/models/pod/models.js deleted file mode 100644 index 3989913c6..000000000 --- a/app/kubernetes/models/pod/models.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * KubernetesPod Model - */ -const _KubernetesPod = Object.freeze({ - Id: '', - Name: '', - Namespace: '', - Images: [], - Status: '', - Restarts: 0, - Node: '', - CreationDate: '', - Containers: [], - Labels: [], -}); - -export class KubernetesPod { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPod))); - } -} diff --git a/app/kubernetes/converters/node.js b/app/kubernetes/node/converter.js similarity index 96% rename from app/kubernetes/converters/node.js rename to app/kubernetes/node/converter.js index 7548a1678..ecc8a70a8 100644 --- a/app/kubernetes/converters/node.js +++ b/app/kubernetes/node/converter.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; -import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/models/node/models'; +import { KubernetesNode, KubernetesNodeDetails } from 'Kubernetes/node/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; class KubernetesNodeConverter { @@ -11,6 +11,7 @@ class KubernetesNodeConverter { res.Id = data.metadata.uid; const hostName = _.find(data.status.addresses, { type: 'Hostname' }); res.Name = hostName ? hostName.address : data.metadata.Name; + res.Labels = data.metadata.labels; res.Role = _.has(data.metadata.labels, 'node-role.kubernetes.io/master') ? 'Master' : 'Worker'; const ready = _.find(data.status.conditions, { type: KubernetesNodeConditionTypes.READY }); @@ -39,6 +40,7 @@ class KubernetesNodeConverter { res.Version = data.status.nodeInfo.kubeletVersion; const internalIP = _.find(data.status.addresses, { type: 'InternalIP' }); res.IPAddress = internalIP ? internalIP.address : '-'; + res.Taints = data.spec.taints ? data.spec.taints : []; return res; } diff --git a/app/kubernetes/filters/nodeFilters.js b/app/kubernetes/node/filters.js similarity index 100% rename from app/kubernetes/filters/nodeFilters.js rename to app/kubernetes/node/filters.js diff --git a/app/kubernetes/models/node/models.js b/app/kubernetes/node/models.js similarity index 96% rename from app/kubernetes/models/node/models.js rename to app/kubernetes/node/models.js index aeee2789b..8b77bfece 100644 --- a/app/kubernetes/models/node/models.js +++ b/app/kubernetes/node/models.js @@ -4,12 +4,14 @@ const _KubernetesNode = Object.freeze({ Id: '', Name: '', + Labels: {}, Role: '', Status: '', CPU: 0, Memory: '', Version: '', IPAddress: '', + Taints: [], }); export class KubernetesNode { diff --git a/app/kubernetes/rest/node.js b/app/kubernetes/node/rest.js similarity index 100% rename from app/kubernetes/rest/node.js rename to app/kubernetes/node/rest.js diff --git a/app/kubernetes/services/nodeService.js b/app/kubernetes/node/service.js similarity index 95% rename from app/kubernetes/services/nodeService.js rename to app/kubernetes/node/service.js index 58b2d787e..30aa9157c 100644 --- a/app/kubernetes/services/nodeService.js +++ b/app/kubernetes/node/service.js @@ -2,7 +2,7 @@ import angular from 'angular'; import _ from 'lodash-es'; import PortainerError from 'Portainer/error'; -import KubernetesNodeConverter from 'Kubernetes/converters/node'; +import KubernetesNodeConverter from 'Kubernetes/node/converter'; import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; class KubernetesNodeService { diff --git a/app/kubernetes/pod/converter.js b/app/kubernetes/pod/converter.js new file mode 100644 index 000000000..121f312bc --- /dev/null +++ b/app/kubernetes/pod/converter.js @@ -0,0 +1,54 @@ +import _ from 'lodash-es'; +import { KubernetesPod, KubernetesPodToleration, KubernetesPodAffinity } from 'Kubernetes/pod/models'; + +function computeStatus(statuses) { + const containerStatuses = _.map(statuses, 'state'); + const running = _.filter(containerStatuses, (s) => s.running).length; + const waiting = _.filter(containerStatuses, (s) => s.waiting).length; + if (waiting) { + return 'Waiting'; + } else if (!running) { + return 'Terminated'; + } + return 'Running'; +} + +function computeAffinity(affinity) { + const res = new KubernetesPodAffinity(); + if (affinity) { + res.NodeAffinity = affinity.nodeAffinity || {}; + } + return res; +} + +function computeTolerations(tolerations) { + return _.map(tolerations, (item) => { + const res = new KubernetesPodToleration(); + res.Key = item.key; + res.Operator = item.operator; + res.Value = item.value; + res.TolerationSeconds = item.tolerationSeconds; + res.Effect = item.effect; + return res; + }); +} + +export default class KubernetesPodConverter { + static apiToModel(data) { + const res = new KubernetesPod(); + res.Id = data.metadata.uid; + res.Name = data.metadata.name; + res.Namespace = data.metadata.namespace; + res.Images = _.map(data.spec.containers, 'image'); + res.Status = computeStatus(data.status.containerStatuses); + res.Restarts = _.sumBy(data.status.containerStatuses, 'restartCount'); + res.Node = data.spec.nodeName; + res.CreationDate = data.status.startTime; + res.Containers = data.spec.containers; + res.Labels = data.metadata.labels; + res.Affinity = computeAffinity(data.spec.affinity); + res.NodeSelector = data.spec.nodeSelector; + res.Tolerations = computeTolerations(data.spec.tolerations); + return res; + } +} diff --git a/app/kubernetes/filters/podFilters.js b/app/kubernetes/pod/filters.js similarity index 100% rename from app/kubernetes/filters/podFilters.js rename to app/kubernetes/pod/filters.js diff --git a/app/kubernetes/pod/models/affinities.js b/app/kubernetes/pod/models/affinities.js new file mode 100644 index 000000000..e0bafb202 --- /dev/null +++ b/app/kubernetes/pod/models/affinities.js @@ -0,0 +1,65 @@ +export const KubernetesPodNodeAffinityNodeSelectorRequirementOperators = Object.freeze({ + IN: 'In', + NOT_IN: 'NotIn', + EXISTS: 'Exists', + DOES_NOT_EXIST: 'DoesNotExist', + GREATER_THAN: 'Gt', + LOWER_THAN: 'Lt', +}); + +/** + * KubernetesPodAffinity Model + */ +const _KubernetesPodAffinity = Object.freeze({ + NodeAffinity: {}, + // PodAffinity: {}, + // PodAntiAffinity: {}, +}); + +export class KubernetesPodAffinity { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodAffinity))); + } +} + +/** + * KubernetesPodNodeAffinity Model + */ +const _KubernetesPodNodeAffinity = Object.freeze({ + PreferredDuringSchedulingIgnoredDuringExecution: [], + RequiredDuringSchedulingIgnoredDuringExecution: {}, +}); + +export class KubernetesPodNodeAffinity { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodNodeAffinity))); + } +} + +/** + * KubernetesPodPodAffinity Model + */ +const _KubernetesPodPodAffinity = Object.freeze({ + PreferredDuringSchedulingIgnoredDuringExecution: [], + equiredDuringSchedulingIgnoredDuringExecution: [], +}); + +export class KubernetesPodPodAffinity { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodPodAffinity))); + } +} + +/** + * KubernetesPodPodAntiAffinity Model + */ +const _KubernetesPodPodAntiAffinity = Object.freeze({ + preferredDuringSchedulingIgnoredDuringExecution: [], + requiredDuringSchedulingIgnoredDuringExecution: [], +}); + +export class KubernetesPodPodAntiAffinity { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodPodAntiAffinity))); + } +} diff --git a/app/kubernetes/pod/models/index.js b/app/kubernetes/pod/models/index.js new file mode 100644 index 000000000..cf76e0386 --- /dev/null +++ b/app/kubernetes/pod/models/index.js @@ -0,0 +1,42 @@ +export * from './affinities'; + +/** + * KubernetesPod Model + */ +const _KubernetesPod = Object.freeze({ + Id: '', + Name: '', + Namespace: '', + Images: [], + Status: '', + Restarts: 0, + Node: '', + CreationDate: '', + Containers: [], + Labels: [], + Affinity: {}, // KubernetesPodAffinity + Tolerations: [], // KubernetesPodToleration[] +}); + +export class KubernetesPod { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPod))); + } +} + +/** + * KubernetesPodToleration Model + */ +const _KubernetesPodToleration = Object.freeze({ + Key: '', + Operator: '', + Value: '', + TolerationSeconds: 0, + Effect: '', +}); + +export class KubernetesPodToleration { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesPodToleration))); + } +} diff --git a/app/kubernetes/services/podService.js b/app/kubernetes/pod/service.js similarity index 95% rename from app/kubernetes/services/podService.js rename to app/kubernetes/pod/service.js index 49c733e62..90bca7be5 100644 --- a/app/kubernetes/services/podService.js +++ b/app/kubernetes/pod/service.js @@ -3,7 +3,7 @@ import angular from 'angular'; import PortainerError from 'Portainer/error'; import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; -import KubernetesPodConverter from 'Kubernetes/converters/pod'; +import KubernetesPodConverter from 'Kubernetes/pod/converter'; class KubernetesPodService { /* @ngInject */ @@ -21,7 +21,7 @@ class KubernetesPodService { async getAllAsync(namespace) { try { const data = await this.KubernetesPods(namespace).get().$promise; - return _.map(data.items, (item) => KubernetesPodConverter.apiToPod(item)); + return _.map(data.items, (item) => KubernetesPodConverter.apiToModel(item)); } catch (err) { throw new PortainerError('Unable to retrieve pods', err); } diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 40147d896..47eff6d7c 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -128,6 +128,24 @@ + Placement +
    + + The placement component helps you understand whether or not this application can be deployed on a specific node. +
    + +
    + + Events
    @@ -147,7 +165,7 @@ > - + YAML
    diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index f8706c265..0cfa9d206 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -1,9 +1,91 @@ import angular from 'angular'; -import _ from 'lodash-es'; +import * as _ from 'lodash-es'; +import * as JsonPatch from 'fast-json-patch'; import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes } from 'Kubernetes/models/application/models'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models'; + +function computeTolerations(nodes, application) { + const pod = application.Pods[0]; + _.forEach(nodes, (n) => { + n.AcceptsApplication = true; + n.Expanded = false; + if (!pod) { + return; + } + n.UnmetTaints = []; + _.forEach(n.Taints, (t) => { + const matchKeyMatchValueMatchEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Equal', Value: t.value, Effect: t.effect }); + const matchKeyAnyValueMatchEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Exists', Effect: t.effect }); + const matchKeyMatchValueAnyEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Equal', Value: t.value, Effect: '' }); + const matchKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: t.key, Operator: 'Exists', Effect: '' }); + const anyKeyAnyValueAnyEffect = _.find(pod.Tolerations, { Key: '', Operator: 'Exists', Effect: '' }); + + if (!matchKeyMatchValueMatchEffect && !matchKeyAnyValueMatchEffect && !matchKeyMatchValueAnyEffect && !matchKeyAnyValueAnyEffect && !anyKeyAnyValueAnyEffect) { + n.AcceptsApplication = false; + n.UnmetTaints.push(t); + } else { + n.AcceptsApplication = true; + } + }); + }); + return nodes; +} + +// For node requirement format depending on operator value +// see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#nodeselectorrequirement-v1-core +// Some operators require empty "values" field, some only one element in "values" field, etc + +function computeAffinities(nodes, application) { + const pod = application.Pods[0]; + _.forEach(nodes, (n) => { + if (pod.NodeSelector) { + const patch = JsonPatch.compare(n.Labels, pod.NodeSelector); + _.remove(patch, { op: 'remove' }); + n.UnmatchedNodeSelectorLabels = _.map(patch, (i) => { + return { key: _.trimStart(i.path, '/'), value: i.value }; + }); + if (n.UnmatchedNodeSelectorLabels.length) { + n.AcceptsApplication = false; + } + } + + if (pod.Affinity.NodeAffinity.requiredDuringSchedulingIgnoredDuringExecution) { + const unmatchedTerms = _.map(pod.Affinity.NodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms, (t) => { + const unmatchedExpressions = _.map(t.matchExpressions, (e) => { + const exists = {}.hasOwnProperty.call(n.Labels, e.key); + const isIn = exists && _.includes(e.values, n.Labels[e.key]); + if ( + (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.EXISTS && exists) || + (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.DOES_NOT_EXIST && !exists) || + (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.IN && isIn) || + (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.NOT_IN && !isIn) || + (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.GREATER_THAN && exists && parseInt(n.Labels[e.key]) > parseInt(e.values[0])) || + (e.operator === KubernetesPodNodeAffinityNodeSelectorRequirementOperators.LOWER_THAN && exists && parseInt(n.Labels[e.key]) < parseInt(e.values[0])) + ) { + return; + } + return e; + }); + return _.without(unmatchedExpressions, undefined); + }); + _.remove(unmatchedTerms, (i) => i.length === 0); + n.UnmatchedNodeAffinities = unmatchedTerms; + if (n.UnmatchedNodeAffinities.length) { + n.AcceptsApplication = false; + } + } + }); + return nodes; +} + +function computePlacements(nodes, application) { + nodes = computeTolerations(nodes, application); + nodes = computeAffinities(nodes, application); + return nodes; +} class KubernetesApplicationController { /* @ngInject */ @@ -18,6 +100,7 @@ class KubernetesApplicationController { KubernetesEventService, KubernetesStackService, KubernetesPodService, + KubernetesNodeService, KubernetesNamespaceHelper ) { this.$async = $async; @@ -31,6 +114,7 @@ class KubernetesApplicationController { this.KubernetesEventService = KubernetesEventService; this.KubernetesStackService = KubernetesStackService; this.KubernetesPodService = KubernetesPodService; + this.KubernetesNodeService = KubernetesNodeService; this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; @@ -103,7 +187,6 @@ class KubernetesApplicationController { /** * ROLLBACK */ - async rollbackApplicationAsync() { try { // await this.KubernetesApplicationService.rollback(this.application, this.formValues.SelectedRevision); @@ -196,7 +279,11 @@ class KubernetesApplicationController { async getApplicationAsync() { try { this.state.dataLoading = true; - this.application = await this.KubernetesApplicationService.get(this.state.params.namespace, this.state.params.name); + const [application, nodes] = await Promise.all([ + this.KubernetesApplicationService.get(this.state.params.namespace, this.state.params.name), + this.KubernetesNodeService.get(), + ]); + this.application = application; this.formValues.Note = this.application.Note; if (this.application.Note) { this.state.expandedNote = true; @@ -204,6 +291,8 @@ class KubernetesApplicationController { if (this.application.CurrentRevision) { this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision }); } + + this.placements = computePlacements(nodes, this.application); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve application details'); } finally { diff --git a/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js b/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js new file mode 100644 index 000000000..e35fbc74d --- /dev/null +++ b/app/kubernetes/views/applications/edit/components/placements-datatable/controller.js @@ -0,0 +1,71 @@ +import * as _ from 'lodash-es'; + +angular.module('portainer.docker').controller('KubernetesApplicationPlacementsDatatableController', function ($scope, $controller, DatatableService, Authentication) { + angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + this.state = Object.assign(this.state, { + expandedItems: [], + expandAll: false, + }); + + this.expandItem = function (item, expanded) { + if (!this.itemCanExpand(item)) { + return; + } + + item.Expanded = expanded; + if (!expanded) { + item.Highlighted = false; + } + }; + + this.itemCanExpand = function (item) { + return !item.AcceptsApplication; + }; + + this.hasExpandableItems = function () { + return _.filter(this.state.filteredDataSet, (item) => this.itemCanExpand(item)).length; + }; + + this.expandAll = function () { + this.state.expandAll = !this.state.expandAll; + _.forEach(this.state.filteredDataSet, (item) => { + if (this.itemCanExpand(item)) { + this.expandItem(item, this.state.expandAll); + } + }); + }; + + this.$onInit = function () { + this.isAdmin = Authentication.isAdmin(); + this.setDefaults(); + this.prepareTableFromDataset(); + + this.state.orderBy = this.orderBy; + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + + var textFilter = DatatableService.getDataTableTextFilters(this.tableKey); + if (textFilter !== null) { + this.state.textFilter = textFilter; + this.onTextFilterChange(); + } + + var storedFilters = DatatableService.getDataTableFilters(this.tableKey); + if (storedFilters !== null) { + this.filters = storedFilters; + } + if (this.filters && this.filters.state) { + this.filters.state.open = false; + } + + var storedSettings = DatatableService.getDataTableSettings(this.tableKey); + if (storedSettings !== null) { + this.settings = storedSettings; + this.settings.open = false; + } + this.onSettingsRepeaterChange(); + }; +}); diff --git a/app/kubernetes/views/applications/edit/components/placements-datatable/index.js b/app/kubernetes/views/applications/edit/components/placements-datatable/index.js new file mode 100644 index 000000000..21a02d6f3 --- /dev/null +++ b/app/kubernetes/views/applications/edit/components/placements-datatable/index.js @@ -0,0 +1,15 @@ +angular.module('portainer.kubernetes').component('kubernetesApplicationPlacementsDatatable', { + templateUrl: './template.html', + controller: 'KubernetesApplicationPlacementsDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + refreshCallback: '<', + loading: '<', + removeAction: '<', + }, +}); diff --git a/app/kubernetes/views/applications/edit/components/placements-datatable/template.html b/app/kubernetes/views/applications/edit/components/placements-datatable/template.html new file mode 100644 index 000000000..6b85f9e0c --- /dev/null +++ b/app/kubernetes/views/applications/edit/components/placements-datatable/template.html @@ -0,0 +1,184 @@ +
    + + +
    +
    {{ $ctrl.titleText }}
    +
    + + Table settings + + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + Node + + + +
    + + + + + + + {{ item.Name }} +
    + This application is missing a toleration for the taint {{ taint.key }}{{ taint.value ? '=' + taint.value : '' }}:{{ taint.effect }} +
    + Placement constraint not respected for that node. +
    + This application can only be scheduled on a node where the label {{ label.key }} is set to {{ label.value }} +
    + Placement label not respected for that node. +
    + This application can only be scheduled on nodes respecting one of the following labels combination: +
    + + {{ term.key }} {{ term.operator }} {{ term.values | kubernetesApplicationConstraintNodeAffinityValue: term.operator }} + + {{ $last ? '' : ' + ' }} +
    Loading...
    No node available.
    +
    + +
    +
    +
    From da143a7a22350d1afa2a0a26e2c4cce18ede65b7 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Thu, 30 Jul 2020 21:24:34 +0300 Subject: [PATCH 103/195] fix(docker/images): ignore pull image rejection (#4128) --- app/docker/services/imageService.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/docker/services/imageService.js b/app/docker/services/imageService.js index 56a13983e..2f04f8fb7 100644 --- a/app/docker/services/imageService.js +++ b/app/docker/services/imageService.js @@ -117,9 +117,13 @@ angular.module('portainer.docker').factory('ImageService', [ function pullImageAndIgnoreErrors(imageConfiguration) { var deferred = $q.defer(); - Image.create({}, imageConfiguration).$promise.finally(function final() { - deferred.resolve(); - }); + Image.create({}, imageConfiguration) + .$promise.catch(() => { + // left empty to ignore errors + }) + .finally(function final() { + deferred.resolve(); + }); return deferred.promise; } From 4d5836138bb75383d013543b207aebcccf86bfba Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 4 Aug 2020 01:18:53 +0300 Subject: [PATCH 104/195] feat(stacks): add the ability to stop a stack (#4042) * feat(stacks): add stack status * feat(stacks): add empty start/stop handlers * feat(stacks): show start/stop button * feat(stacks): implement stack stop * feat(stacks): implement start stack * feat(stacks): filter by active/inactive stacks * fix(stacks): update authorizations for stack start/stop * feat(stacks): assign default status on create * fix(bolt): fix import * fix(stacks): show external stacks * fix(stacks): reload on stop/start * feat(stacks): confirm before stop --- api/bolt/migrator/migrate_dbversion23.go | 20 ++++++ api/bolt/migrator/migrator.go | 5 ++ .../handler/stacks/create_compose_stack.go | 3 + api/http/handler/stacks/create_swarm_stack.go | 3 + api/http/handler/stacks/handler.go | 4 ++ api/http/handler/stacks/stack_start.go | 61 +++++++++++++++++++ api/http/handler/stacks/stack_stop.go | 61 +++++++++++++++++++ api/portainer.go | 11 ++++ .../stacks-datatable/stacksDatatable.html | 31 +++++++++- .../stacksDatatableController.js | 22 +++++++ app/portainer/models/stack.js | 1 + app/portainer/rest/stack.js | 2 + app/portainer/services/api/stackService.js | 10 +++ app/portainer/services/modalService.js | 7 +++ app/portainer/views/stacks/edit/stack.html | 30 ++++++++- .../views/stacks/edit/stackController.js | 59 ++++++++++++++++-- 16 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 api/http/handler/stacks/stack_start.go create mode 100644 api/http/handler/stacks/stack_stop.go diff --git a/api/bolt/migrator/migrate_dbversion23.go b/api/bolt/migrator/migrate_dbversion23.go index 856e77856..5d1c56904 100644 --- a/api/bolt/migrator/migrate_dbversion23.go +++ b/api/bolt/migrator/migrate_dbversion23.go @@ -1,5 +1,7 @@ package migrator +import portainer "github.com/portainer/portainer/api" + func (m *Migrator) updateSettingsToDB24() error { legacySettings, err := m.settingsService.Settings() if err != nil { @@ -12,3 +14,21 @@ func (m *Migrator) updateSettingsToDB24() error { return m.settingsService.UpdateSettings(legacySettings) } + +func (m *Migrator) updateStacksToDB24() error { + stacks, err := m.stackService.Stacks() + if err != nil { + return err + } + + for idx := range stacks { + stack := &stacks[idx] + stack.Status = portainer.StackStatusActive + err := m.stackService.UpdateStack(stack.ID, stack) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 598681f28..84f1e27e3 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -335,6 +335,11 @@ func (m *Migrator) Migrate() error { if err != nil { return err } + + err = m.updateStacksToDB24() + if err != nil { + return err + } } return m.versionService.StoreDBVersion(portainer.DBVersion) diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index aa47eacd7..6ceab991c 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -66,6 +66,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, EndpointID: endpoint.ID, EntryPoint: filesystem.ComposeFileDefaultName, Env: payload.Env, + Status: portainer.StackStatusActive, } stackFolder := strconv.Itoa(int(stack.ID)) @@ -151,6 +152,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite EndpointID: endpoint.ID, EntryPoint: payload.ComposeFilePathInRepository, Env: payload.Env, + Status: portainer.StackStatusActive, } projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID))) @@ -246,6 +248,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, EndpointID: endpoint.ID, EntryPoint: filesystem.ComposeFileDefaultName, Env: payload.Env, + Status: portainer.StackStatusActive, } stackFolder := strconv.Itoa(int(stack.ID)) diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index f9aac664a..0113e8a41 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -62,6 +62,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r EndpointID: endpoint.ID, EntryPoint: filesystem.ComposeFileDefaultName, Env: payload.Env, + Status: portainer.StackStatusActive, } stackFolder := strconv.Itoa(int(stack.ID)) @@ -151,6 +152,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, EndpointID: endpoint.ID, EntryPoint: payload.ComposeFilePathInRepository, Env: payload.Env, + Status: portainer.StackStatusActive, } projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID))) @@ -254,6 +256,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r EndpointID: endpoint.ID, EntryPoint: filesystem.ComposeFileDefaultName, Env: payload.Env, + Status: portainer.StackStatusActive, } stackFolder := strconv.Itoa(int(stack.ID)) diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index cf80b4ecd..b6210358f 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -54,6 +54,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet) h.Handle("/stacks/{id}/migrate", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost) + h.Handle("/stacks/{id}/start", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackStart))).Methods(http.MethodPost) + h.Handle("/stacks/{id}/stop", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackStop))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go new file mode 100644 index 000000000..5f473e93a --- /dev/null +++ b/api/http/handler/stacks/stack_start.go @@ -0,0 +1,61 @@ +package stacks + +import ( + "errors" + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" +) + +// POST request on /api/stacks/:id/start +func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} + } + + stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) + if err == bolterrors.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} + } + + if stack.Status == portainer.StackStatusActive { + return &httperror.HandlerError{http.StatusBadRequest, "Stack is already active", errors.New("Stack is already active")} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) + if err == bolterrors.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + err = handler.startStack(stack, endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err} + } + + stack.Status = portainer.StackStatusActive + err = handler.DataStore.Stack().UpdateStack(stack.ID, stack) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err} + } + + return response.JSON(w, stack) +} + +func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error { + switch stack.Type { + case portainer.DockerComposeStack: + return handler.ComposeStackManager.Up(stack, endpoint) + case portainer.DockerSwarmStack: + return handler.SwarmStackManager.Deploy(stack, true, endpoint) + } + return nil +} diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go new file mode 100644 index 000000000..e79b7c8dc --- /dev/null +++ b/api/http/handler/stacks/stack_stop.go @@ -0,0 +1,61 @@ +package stacks + +import ( + "errors" + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" + bolterrors "github.com/portainer/portainer/api/bolt/errors" +) + +// POST request on /api/stacks/:id/stop +func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} + } + + stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID)) + if err == bolterrors.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} + } + + if stack.Status == portainer.StackStatusInactive { + return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")} + } + + endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID) + if err == bolterrors.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + err = handler.stopStack(stack, endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err} + } + + stack.Status = portainer.StackStatusInactive + err = handler.DataStore.Stack().UpdateStack(stack.ID, stack) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err} + } + + return response.JSON(w, stack) +} + +func (handler *Handler) stopStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error { + switch stack.Type { + case portainer.DockerComposeStack: + return handler.ComposeStackManager.Down(stack, endpoint) + case portainer.DockerSwarmStack: + return handler.SwarmStackManager.Remove(stack, endpoint) + } + return nil +} diff --git a/api/portainer.go b/api/portainer.go index c5a0db217..dcbf25af3 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -547,12 +547,16 @@ type ( EntryPoint string `json:"EntryPoint"` Env []Pair `json:"Env"` ResourceControl *ResourceControl `json:"ResourceControl"` + Status StackStatus `json:"Status"` ProjectPath string } // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier) StackID int + // StackStatus represent a status for a stack + StackStatus int + // StackType represents the type of the stack (compose v2, stack deploy v3) StackType int @@ -1302,6 +1306,13 @@ const ( KubernetesStack ) +// StackStatus represents a status for a stack +const ( + _ StackStatus = iota + StackStatusActive + StackStatusInactive +) + const ( _ TemplateType = iota // ContainerTemplate represents a container template diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html index 7b642041b..1bd2c313d 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatable.html @@ -72,7 +72,7 @@ - - +
    + @@ -82,6 +82,32 @@ +
    + + Filter + + +
    +
    @@ -102,7 +128,7 @@
    @@ -112,6 +138,7 @@ {{ item.Name }} {{ item.Name }} + Inactive {{ item.Type === 1 ? 'Swarm' : 'Compose' }} diff --git a/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js b/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js index 75e8e2400..e5f8f1252 100644 --- a/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js +++ b/app/portainer/components/datatables/stacks-datatable/stacksDatatableController.js @@ -6,6 +6,15 @@ angular.module('portainer.app').controller('StacksDatatableController', [ function ($scope, $controller, DatatableService, Authentication) { angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); + this.filters = { + state: { + open: false, + enabled: false, + showActiveStacks: true, + showUnactiveStacks: true, + }, + }; + /** * Do not allow external items */ @@ -17,6 +26,19 @@ angular.module('portainer.app').controller('StacksDatatableController', [ return !(item.External && !this.isAdmin && !this.isEndpointAdmin); }; + this.applyFilters = applyFilters.bind(this); + function applyFilters(stack) { + const { showActiveStacks, showUnactiveStacks } = this.filters.state; + return (stack.Status === 1 && showActiveStacks) || (stack.Status === 2 && showUnactiveStacks) || stack.External; + } + + this.onFilterChange = onFilterChange.bind(this); + function onFilterChange() { + const { showActiveStacks, showUnactiveStacks } = this.filters.state; + this.filters.state.enabled = !showActiveStacks || !showUnactiveStacks; + DatatableService.setDataTableFilters(this.tableKey, this.filters); + } + this.$onInit = function () { this.isAdmin = Authentication.isAdmin(); this.isEndpointAdmin = Authentication.hasAuthorizations(['EndpointResourcesAccess']); diff --git a/app/portainer/models/stack.js b/app/portainer/models/stack.js index b21dc47b9..ef28f97e3 100644 --- a/app/portainer/models/stack.js +++ b/app/portainer/models/stack.js @@ -12,6 +12,7 @@ export function StackViewModel(data) { this.ResourceControl = new ResourceControlViewModel(data.ResourceControl); } this.External = false; + this.Status = data.Status; } export function ExternalStackViewModel(name, type) { diff --git a/app/portainer/rest/stack.js b/app/portainer/rest/stack.js index 7c4753025..d521c876c 100644 --- a/app/portainer/rest/stack.js +++ b/app/portainer/rest/stack.js @@ -15,6 +15,8 @@ angular.module('portainer.app').factory('Stack', [ remove: { method: 'DELETE', params: { id: '@id', external: '@external', endpointId: '@endpointId' } }, getStackFile: { method: 'GET', params: { id: '@id', action: 'file' } }, migrate: { method: 'POST', params: { id: '@id', action: 'migrate', endpointId: '@endpointId' }, ignoreLoadingBar: true }, + start: { method: 'POST', params: { id: '@id', action: 'start' } }, + stop: { method: 'POST', params: { id: '@id', action: 'stop' } }, } ); }, diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js index 47a9b6751..68bebbd56 100644 --- a/app/portainer/services/api/stackService.js +++ b/app/portainer/services/api/stackService.js @@ -349,6 +349,16 @@ angular.module('portainer.app').factory('StackService', [ return $async(kubernetesDeployAsync, endpointId, namespace, content, compose); }; + service.start = start; + function start(id) { + return Stack.start({ id }).$promise; + } + + service.stop = stop; + function stop(id) { + return Stack.stop({ id }).$promise; + } + return service; }, ]); diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 14cd30e0c..196483ba4 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -37,6 +37,13 @@ angular.module('portainer.app').factory('ModalService', [ }); }; + service.confirmAsync = confirmAsync; + function confirmAsync(options) { + return new Promise((resolve) => { + service.confirm({ ...options, callback: (confirmed) => resolve(confirmed) }); + }); + } + service.confirm = function (options) { var box = bootbox.confirm({ title: options.title, diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html index 7bc2c041c..114a1712f 100644 --- a/app/portainer/views/stacks/edit/stack.html +++ b/app/portainer/views/stacks/edit/stack.html @@ -47,6 +47,28 @@ Create template from stack + + + + diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js index 4187777a3..1ea4a8cb0 100644 --- a/app/portainer/views/stacks/edit/stackController.js +++ b/app/portainer/views/stacks/edit/stackController.js @@ -1,4 +1,5 @@ angular.module('portainer.app').controller('StackController', [ + '$async', '$q', '$scope', '$state', @@ -17,6 +18,7 @@ angular.module('portainer.app').controller('StackController', [ 'GroupService', 'ModalService', function ( + $async, $q, $scope, $state, @@ -187,6 +189,46 @@ angular.module('portainer.app').controller('StackController', [ $scope.stackFileContent = cm.getValue(); }; + $scope.stopStack = stopStack; + function stopStack() { + return $async(stopStackAsync); + } + async function stopStackAsync() { + const confirmed = await ModalService.confirmAsync({ + title: 'Are you sure?', + message: 'Are you sure you want to stop this stack?', + buttons: { confirm: { label: 'Stop', className: 'btn-danger' } }, + }); + if (!confirmed) { + return; + } + + $scope.state.actionInProgress = true; + try { + await StackService.stop($scope.stack.Id); + $state.reload(); + } catch (err) { + Notifications.error('Failure', err, 'Unable to stop stack'); + } + $scope.state.actionInProgress = false; + } + + $scope.startStack = startStack; + function startStack() { + return $async(startStackAsync); + } + async function startStackAsync() { + $scope.state.actionInProgress = true; + const id = $scope.stack.Id; + try { + await StackService.start(id); + $state.reload(); + } catch (err) { + Notifications.error('Failure', err, 'Unable to start stack'); + } + $scope.state.actionInProgress = false; + } + function loadStack(id) { var agentProxy = $scope.applicationState.endpoint.mode.agentProxy; @@ -207,17 +249,24 @@ angular.module('portainer.app').controller('StackController', [ $scope.groups = data.groups; $scope.stack = stack; + let resourcesPromise = Promise.resolve({}); + if (stack.Status === 1) { + resourcesPromise = stack.Type === 1 ? retrieveSwarmStackResources(stack.Name, agentProxy) : retrieveComposeStackResources(stack.Name); + } + return $q.all({ stackFile: StackService.getStackFile(id), - resources: stack.Type === 1 ? retrieveSwarmStackResources(stack.Name, agentProxy) : retrieveComposeStackResources(stack.Name), + resources: resourcesPromise, }); }) .then(function success(data) { $scope.stackFileContent = data.stackFile; - if ($scope.stack.Type === 1) { - assignSwarmStackResources(data.resources, agentProxy); - } else { - assignComposeStackResources(data.resources); + if ($scope.stack.Status === 1) { + if ($scope.stack.Type === 1) { + assignSwarmStackResources(data.resources, agentProxy); + } else { + assignComposeStackResources(data.resources); + } } }) .catch(function error(err) { From 490b7ad26fcb0eefd2f48dca3f37b2fceef100d1 Mon Sep 17 00:00:00 2001 From: itsconquest Date: Tue, 4 Aug 2020 11:14:59 +1200 Subject: [PATCH 105/195] fix(container-creation): allow resetting to unlimited (#4138) * fix(container-creation): allow resetting to unlimited * fix(container-creation): refactor for readability --- .../create/createContainerController.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 2d69e3fb2..2cfb41803 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -316,19 +316,21 @@ angular.module('portainer.docker').controller('CreateContainerController', [ function prepareResources(config) { // Memory Limit - Round to 0.125 - var memoryLimit = (Math.round($scope.formValues.MemoryLimit * 8) / 8).toFixed(3); - memoryLimit *= 1024 * 1024; - if (memoryLimit > 0) { + if ($scope.formValues.MemoryLimit >= 0) { + var memoryLimit = (Math.round($scope.formValues.MemoryLimit * 8) / 8).toFixed(3); + memoryLimit *= 1024 * 1024; config.HostConfig.Memory = memoryLimit; } + // Memory Resevation - Round to 0.125 - var memoryReservation = (Math.round($scope.formValues.MemoryReservation * 8) / 8).toFixed(3); - memoryReservation *= 1024 * 1024; - if (memoryReservation > 0) { + if ($scope.formValues.MemoryReservation >= 0) { + var memoryReservation = (Math.round($scope.formValues.MemoryReservation * 8) / 8).toFixed(3); + memoryReservation *= 1024 * 1024; config.HostConfig.MemoryReservation = memoryReservation; } + // CPU Limit - if ($scope.formValues.CpuLimit > 0) { + if ($scope.formValues.CpuLimit >= 0) { config.HostConfig.NanoCpus = $scope.formValues.CpuLimit * 1000000000; } } From bd7d7dcef56b6ed0a9020175125b57d080f5ff89 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Tue, 4 Aug 2020 03:44:17 +0300 Subject: [PATCH 106/195] feat(agent): add auto agent platform detection (#4132) * feat(endpoint): check endpoint type on creation * feat(edge): check edge endpoint type * feat(endpoint): send endpoint creation type * feat(endpoint): pass tls config * feat(endpoint): show connect errors * fix(endpoint): set correct endpoint type * feat(endpoint): support endpoint creation * style(endpoint): remove todo comment * feat(endpoint): set protocol for endpoint url * feat(endpoint): change scheme of url * fix(endpoint): toggle code block * feat(edge): report missing agent platform header * fix(api/endpoints): fix an issue with agent on kubernetes endpoint * feat(core/endpoints): minor UI update Co-authored-by: Anthony Lapenna --- api/http/handler/endpoints/endpoint_create.go | 115 +++++++++++++++--- .../endpoints/endpoint_status_inspect.go | 27 +++- api/portainer.go | 13 ++ app/portainer/models/endpoint/models.js | 11 ++ app/portainer/services/api/endpointService.js | 16 +-- app/portainer/services/fileUpload.js | 21 +++- .../create/createEndpointController.js | 53 +++++--- .../endpoints/create/createendpoint.html | 16 +-- .../views/endpoints/edit/endpoint.html | 16 +-- .../endpoints/edit/endpointController.js | 2 +- .../init/endpoint/initEndpointController.js | 12 +- 11 files changed, 226 insertions(+), 76 deletions(-) diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 210941fbc..f16e177c1 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -2,17 +2,20 @@ package endpoints import ( "errors" + "fmt" "net" "net/http" "net/url" "runtime" "strconv" "strings" + "time" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/internal/edge" ) @@ -20,7 +23,7 @@ import ( type endpointCreatePayload struct { Name string URL string - EndpointType int + EndpointCreationType endpointCreationEnum PublicURL string GroupID int TLS bool @@ -36,6 +39,17 @@ type endpointCreatePayload struct { EdgeCheckinInterval int } +type endpointCreationEnum int + +const ( + _ endpointCreationEnum = iota + localDockerEnvironment + agentEnvironment + azureEnvironment + edgeAgentEnvironment + localKubernetesEnvironment +) + func (payload *endpointCreatePayload) Validate(r *http.Request) error { name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { @@ -43,11 +57,11 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } payload.Name = name - endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false) - if err != nil || endpointType == 0 { - return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)") + endpointCreationType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointCreationType", false) + if err != nil || endpointCreationType == 0 { + return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge Agent environment) or 5 (Local Kubernetes environment)") } - payload.EndpointType = endpointType + payload.EndpointCreationType = endpointCreationEnum(endpointCreationType) groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true) if groupID == 0 { @@ -97,8 +111,8 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } } - switch portainer.EndpointType(payload.EndpointType) { - case portainer.AzureEnvironment: + switch payload.EndpointCreationType { + case azureEnvironment: azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false) if err != nil { return errors.New("Invalid Azure application ID") @@ -182,22 +196,34 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * } func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - switch portainer.EndpointType(payload.EndpointType) { - case portainer.AzureEnvironment: + switch payload.EndpointCreationType { + case azureEnvironment: return handler.createAzureEndpoint(payload) - case portainer.EdgeAgentOnDockerEnvironment: - return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnDockerEnvironment) + case edgeAgentEnvironment: + return handler.createEdgeAgentEndpoint(payload) - case portainer.KubernetesLocalEnvironment: + case localKubernetesEnvironment: return handler.createKubernetesEndpoint(payload) + } - case portainer.EdgeAgentOnKubernetesEnvironment: - return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnKubernetesEnvironment) + endpointType := portainer.DockerEnvironment + if payload.EndpointCreationType == agentEnvironment { + agentPlatform, err := handler.pingAndCheckPlatform(payload) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get endpoint type", err} + } + + if agentPlatform == portainer.AgentPlatformDocker { + endpointType = portainer.AgentOnDockerEnvironment + } else if agentPlatform == portainer.AgentPlatformKubernetes { + endpointType = portainer.AgentOnKubernetesEnvironment + payload.URL = strings.TrimPrefix(payload.URL, "tcp://") + } } if payload.TLS { - return handler.createTLSSecuredEndpoint(payload, portainer.EndpointType(payload.EndpointType)) + return handler.createTLSSecuredEndpoint(payload, endpointType) } return handler.createUnsecuredEndpoint(payload) } @@ -241,7 +267,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po return endpoint, nil } -func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) { +func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { endpointID := handler.DataStore.Endpoint().GetNextIdentifier() portainerURL, err := url.Parse(payload.URL) @@ -264,7 +290,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload, ID: portainer.EndpointID(endpointID), Name: payload.Name, URL: portainerHost, - Type: endpointType, + Type: portainer.EdgeAgentOnDockerEnvironment, GroupID: portainer.EndpointGroupID(payload.GroupID), TLSConfig: portainer.TLSConfiguration{ TLS: false, @@ -472,3 +498,58 @@ func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *end return nil } + +func (handler *Handler) pingAndCheckPlatform(payload *endpointCreatePayload) (portainer.AgentPlatform, error) { + httpCli := &http.Client{ + Timeout: 3 * time.Second, + } + + if payload.TLS { + tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify) + if err != nil { + return 0, err + } + + httpCli.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + } + + url, err := url.Parse(fmt.Sprintf("%s/ping", payload.URL)) + if err != nil { + return 0, err + } + + url.Scheme = "https" + + req, err := http.NewRequest(http.MethodGet, url.String(), nil) + if err != nil { + return 0, err + } + + resp, err := httpCli.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return 0, fmt.Errorf("Failed request with status %d", resp.StatusCode) + } + + agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform) + if agentPlatformHeader == "" { + return 0, errors.New("Agent Platform Header is missing") + } + + agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader) + if err != nil { + return 0, err + } + + if agentPlatformNumber == 0 { + return 0, errors.New("Agent platform is invalid") + } + + return portainer.AgentPlatform(agentPlatformNumber), nil +} diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index 8c1dacd62..787af6788 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -2,13 +2,15 @@ package endpoints import ( "encoding/base64" + "errors" "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/bolt/errors" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type stackStatusResponse struct { @@ -41,7 +43,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == errors.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -54,10 +56,27 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req if endpoint.EdgeID == "" { edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader) - endpoint.EdgeID = edgeIdentifier - err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) + agentPlatformHeader := r.Header.Get(portainer.HTTPResponseAgentPlatform) + if agentPlatformHeader == "" { + return &httperror.HandlerError{http.StatusInternalServerError, "Agent Platform Header is missing", errors.New("Agent Platform Header is missing")} + } + + agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse agent platform header", err} + } + + agentPlatform := portainer.AgentPlatform(agentPlatformNumber) + + if agentPlatform == portainer.AgentPlatformDocker { + endpoint.Type = portainer.EdgeAgentOnDockerEnvironment + } else if agentPlatform == portainer.AgentPlatformKubernetes { + endpoint.Type = portainer.EdgeAgentOnKubernetesEnvironment + } + + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err} } diff --git a/api/portainer.go b/api/portainer.go index dcbf25af3..fab4fdd46 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -11,6 +11,9 @@ type ( RoleID RoleID `json:"RoleId"` } + // AgentPlatform represents a platform type for an Agent + AgentPlatform int + // APIOperationAuthorizationRequest represent an request for the authorization to execute an API operation APIOperationAuthorizationRequest struct { Path string @@ -1141,6 +1144,8 @@ const ( PortainerAgentHeader = "Portainer-Agent" // PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster PortainerAgentEdgeIDHeader = "X-PortainerAgent-EdgeID" + // HTTPResponseAgentPlatform represents the name of the header containing the Agent platform + HTTPResponseAgentPlatform = "Portainer-Agent-Platform" // PortainerAgentTargetHeader represent the name of the header containing the target node name PortainerAgentTargetHeader = "X-PortainerAgent-Target" // PortainerAgentSignatureHeader represent the name of the header containing the digital signature @@ -1174,6 +1179,14 @@ const ( AuthenticationOAuth ) +const ( + _ AgentPlatform = iota + // AgentPlatformDocker represent the Docker platform (Standalone/Swarm) + AgentPlatformDocker + // AgentPlatformKubernetes represent the Kubernetes platform + AgentPlatformKubernetes +) + const ( _ EdgeJobLogsStatus = iota // EdgeJobLogsStatusIdle represents an idle log collection job diff --git a/app/portainer/models/endpoint/models.js b/app/portainer/models/endpoint/models.js index feae2b9ab..cdeb9a716 100644 --- a/app/portainer/models/endpoint/models.js +++ b/app/portainer/models/endpoint/models.js @@ -18,6 +18,17 @@ export const PortainerEndpointTypes = Object.freeze({ EdgeAgentOnKubernetesEnvironment: 7, }); +/** + * JS reference of endpoint_create.go#EndpointCreationType iota + */ +export const PortainerEndpointCreationTypes = Object.freeze({ + LocalDockerEnvironment: 1, + AgentEnvironment: 2, + AzureEnvironment: 3, + EdgeAgentEnvironment: 4, + LocalKubernetesEnvironment: 5, +}); + export const PortainerEndpointConnectionTypes = Object.freeze({ DOCKER_LOCAL: 1, KUBERNETES_LOCAL: 2, diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 3bbdf567f..19b05260a 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -1,4 +1,4 @@ -import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; +import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models'; angular.module('portainer.app').factory('EndpointService', [ '$q', @@ -59,7 +59,7 @@ angular.module('portainer.app').factory('EndpointService', [ service.createLocalEndpoint = function () { var deferred = $q.defer(); - FileUploadService.createEndpoint('local', PortainerEndpointTypes.DockerEnvironment, '', '', 1, [], false) + FileUploadService.createEndpoint('local', PortainerEndpointCreationTypes.LocalDockerEnvironment, '', '', 1, [], false) .then(function success(response) { deferred.resolve(response.data); }) @@ -72,7 +72,7 @@ angular.module('portainer.app').factory('EndpointService', [ service.createRemoteEndpoint = function ( name, - type, + creationType, URL, PublicURL, groupID, @@ -88,17 +88,13 @@ angular.module('portainer.app').factory('EndpointService', [ var deferred = $q.defer(); var endpointURL = URL; - if ( - type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment && - type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment && - type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment - ) { + if (creationType !== PortainerEndpointCreationTypes.EdgeAgentEnvironment) { endpointURL = 'tcp://' + URL; } FileUploadService.createEndpoint( name, - type, + creationType, endpointURL, PublicURL, groupID, @@ -124,7 +120,7 @@ angular.module('portainer.app').factory('EndpointService', [ service.createLocalKubernetesEndpoint = function () { var deferred = $q.defer(); - FileUploadService.createEndpoint('local', 5, '', '', 1, [], true, true, true) + FileUploadService.createEndpoint('local', PortainerEndpointCreationTypes.LocalKubernetesEnvironment, '', '', 1, [], true, true, true) .then(function success(response) { deferred.resolve(response.data); }) diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 69ebc7720..fe581645a 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -1,3 +1,4 @@ +import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models'; import { genericHandler, jsonObjectsToArrayHandler } from '../../docker/rest/response/handlers'; angular.module('portainer.app').factory('FileUploadService', [ @@ -112,12 +113,26 @@ angular.module('portainer.app').factory('FileUploadService', [ }); }; - service.createEndpoint = function (name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, checkinInterval) { + service.createEndpoint = function ( + name, + creationType, + URL, + PublicURL, + groupID, + tagIds, + TLS, + TLSSkipVerify, + TLSSkipClientVerify, + TLSCAFile, + TLSCertFile, + TLSKeyFile, + checkinInterval + ) { return Upload.upload({ url: 'api/endpoints', data: { Name: name, - EndpointType: type, + EndpointCreationType: creationType, URL: URL, PublicURL: PublicURL, GroupID: groupID, @@ -139,7 +154,7 @@ angular.module('portainer.app').factory('FileUploadService', [ url: 'api/endpoints', data: { Name: name, - EndpointType: 3, + EndpointCreationType: PortainerEndpointCreationTypes.Azure, GroupID: groupId, TagIds: Upload.json(tagIds), AzureApplicationID: applicationId, diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index 21b39af9d..c108f1725 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -1,4 +1,4 @@ -import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; +import { PortainerEndpointCreationTypes, PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel'; angular @@ -56,7 +56,7 @@ angular }; $scope.copyAgentCommand = function () { - if ($scope.state.deploymentTab === 0) { + if ($scope.state.deploymentTab === 1) { clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent'); } else { clipboard.copyText('curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml'); @@ -102,19 +102,31 @@ angular var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert; var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; - addEndpoint(name, PortainerEndpointTypes.DockerEnvironment, URL, publicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); + addEndpoint( + name, + PortainerEndpointCreationTypes.LocalDockerEnvironment, + URL, + publicURL, + groupId, + tagIds, + TLS, + TLSSkipVerify, + TLSSkipClientVerify, + TLSCAFile, + TLSCertFile, + TLSKeyFile + ); }; $scope.addAgentEndpoint = function () { var name = $scope.formValues.Name; - var URL = $filter('stripprotocol')($scope.formValues.URL); + // var URL = $filter('stripprotocol')($scope.formValues.URL); + var URL = $scope.formValues.URL; var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL; var groupId = $scope.formValues.GroupId; var tagIds = $scope.formValues.TagIds; - addEndpoint(name, PortainerEndpointTypes.AgentOnDockerEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); - // TODO: k8s merge - temporarily updated to AgentOnKubernetesEnvironment, breaking Docker agent support - // addEndpoint(name, PortainerEndpointTypes.AgentOnKubernetesEnvironment, URL, publicURL, groupId, tags, true, true, true, null, null, null); + addEndpoint(name, PortainerEndpointCreationTypes.AgentEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); }; $scope.addEdgeAgentEndpoint = function () { @@ -123,9 +135,7 @@ angular var tagIds = $scope.formValues.TagIds; var URL = $scope.formValues.URL; - addEndpoint(name, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); - // TODO: k8s merge - temporarily updated to EdgeAgentOnKubernetesEnvironment, breaking Docker Edge agent support - // addEndpoint(name, PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment, URL, "", groupId, tags, false, false, false, null, null, null); + addEndpoint(name, PortainerEndpointCreationTypes.EdgeAgentEnvironment, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); }; $scope.addAzureEndpoint = function () { @@ -154,11 +164,11 @@ angular }); } - function addEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) { + function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) { $scope.state.actionInProgress = true; EndpointService.createRemoteEndpoint( name, - type, + creationType, URL, PublicURL, groupId, @@ -171,14 +181,19 @@ angular TLSKeyFile, CheckinInterval ) - .then(function success(data) { + .then(function success(endpoint) { Notifications.success('Endpoint created', name); - if (type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { - $state.go('portainer.endpoints.endpoint', { id: data.Id }); - } else if (type === PortainerEndpointTypes.AgentOnKubernetesEnvironment) { - $state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: data.Id }); - } else { - $state.go('portainer.endpoints', {}, { reload: true }); + switch (endpoint.Type) { + case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment: + case PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment: + $state.go('portainer.endpoints.endpoint', { id: endpoint.Id }); + break; + case PortainerEndpointTypes.AgentOnKubernetesEnvironment: + $state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: endpoint.Id }); + break; + default: + $state.go('portainer.endpoints', {}, { reload: true }); + break; } }) .catch(function error(err) { diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 06a1c62dd..ee9e6e25a 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -73,19 +73,19 @@
    - Ensure that you have deployed the Portainer agent in your cluster first. You can use execute the following command on any manager node to deploy it. + Ensure that you have deployed the Portainer agent in your cluster first. Refer to the platform related command below to deploy it.
    - - curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml - + curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml - - - curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent - + + curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent
    diff --git a/app/portainer/views/endpoints/edit/endpoint.html b/app/portainer/views/endpoints/edit/endpoint.html index 48fabddf8..eaa590d98 100644 --- a/app/portainer/views/endpoints/edit/endpoint.html +++ b/app/portainer/views/endpoints/edit/endpoint.html @@ -28,23 +28,23 @@

    - Deploy the Edge agent on your remote Docker/Kubernetes environment using the following command(s) + Refer to the platform related command below to deploy the Edge agent in your remote cluster.

    The agent will communicate with Portainer via {{ edgeKeyDetails.instanceURL }} and tcp://{{ edgeKeyDetails.tunnelServerAddr }}

    - - {{ dockerCommands.standalone }} + + curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }} - + {{ dockerCommands.swarm }} - - - curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }} - + + {{ dockerCommands.standalone }}
    diff --git a/app/portainer/views/endpoints/edit/endpointController.js b/app/portainer/views/endpoints/edit/endpointController.js index 30f6c900d..5f96e2bfe 100644 --- a/app/portainer/views/endpoints/edit/endpointController.js +++ b/app/portainer/views/endpoints/edit/endpointController.js @@ -55,7 +55,7 @@ angular }; $scope.copyEdgeAgentDeploymentCommand = function () { - if ($scope.state.deploymentTab === 0) { + if ($scope.state.deploymentTab === 2) { clipboard.copyText( 'docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host --restart always -e EDGE=1 -e EDGE_ID=' + $scope.randomEdgeID + diff --git a/app/portainer/views/init/endpoint/initEndpointController.js b/app/portainer/views/init/endpoint/initEndpointController.js index e643a3679..15a2faa87 100644 --- a/app/portainer/views/init/endpoint/initEndpointController.js +++ b/app/portainer/views/init/endpoint/initEndpointController.js @@ -1,7 +1,7 @@ import _ from 'lodash-es'; import angular from 'angular'; import { PortainerEndpointInitFormValueEndpointSections, PortainerEndpointInitFormValues } from 'Portainer/models/endpoint/formValues'; -import { PortainerEndpointConnectionTypes, PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; +import { PortainerEndpointConnectionTypes, PortainerEndpointCreationTypes, PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; require('./includes/localDocker.html'); require('./includes/localKubernetes.html'); @@ -61,7 +61,7 @@ class InitEndpointController { case PortainerEndpointConnectionTypes.AGENT: return this.createAgentEndpoint(); default: - this.Notifications.error('Failure', 'Unable to determine wich action to do'); + this.Notifications.error('Failure', 'Unable to determine which action to do'); } } @@ -112,10 +112,10 @@ class InitEndpointController { const name = this.formValues.Name; const URL = this.formValues.URL; const PublicURL = URL.split(':')[0]; - // TODO: k8s merge - change type ID for agent on kube (6) or agent on swarm (2) + const endpoint = await this.EndpointService.createRemoteEndpoint( name, - PortainerEndpointTypes.AgentOnKubernetesEnvironment, + PortainerEndpointCreationTypes.AgentEnvironment, URL, PublicURL, 1, @@ -127,8 +127,8 @@ class InitEndpointController { null, null ); - // TODO: k8s merge - go on home whith agent on swarm (2) - this.$state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: endpoint.Id }); + const routeName = endpoint.Type === PortainerEndpointTypes.AgentOnKubernetesEnvironment ? 'portainer.endpoints.endpoint.kubernetesConfig' : 'portainer.home'; + this.$state.go(routeName, { id: endpoint.Id }); } catch (err) { this.Notifications.error('Failure', err, 'Unable to connect to the Docker environment'); } finally { From 909e1ef02c2a039c06cb564cde18bd14c7e2700e Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 4 Aug 2020 16:01:15 +1200 Subject: [PATCH 107/195] fix(k8s/user): remove username part from service account (#4147) --- api/http/proxy/factory/kubernetes/token.go | 6 +++--- api/http/proxy/factory/kubernetes/transport.go | 6 +++--- api/kubernetes/cli/naming.go | 4 ++-- api/kubernetes/cli/service_account.go | 8 ++++---- api/portainer.go | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/api/http/proxy/factory/kubernetes/token.go b/api/http/proxy/factory/kubernetes/token.go index 0e84f2d83..bfcc145d8 100644 --- a/api/http/proxy/factory/kubernetes/token.go +++ b/api/http/proxy/factory/kubernetes/token.go @@ -45,7 +45,7 @@ func (manager *tokenManager) getAdminServiceAccountToken() string { return manager.adminToken } -func (manager *tokenManager) getUserServiceAccountToken(userID int, username string) (string, error) { +func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) { manager.mutex.Lock() defer manager.mutex.Unlock() @@ -61,12 +61,12 @@ func (manager *tokenManager) getUserServiceAccountToken(userID int, username str teamIds = append(teamIds, int(membership.TeamID)) } - err = manager.kubecli.SetupUserServiceAccount(userID, username, teamIds) + err = manager.kubecli.SetupUserServiceAccount(userID, teamIds) if err != nil { return "", err } - serviceAccountToken, err := manager.kubecli.GetServiceAccountBearerToken(userID, username) + serviceAccountToken, err := manager.kubecli.GetServiceAccountBearerToken(userID) if err != nil { return "", err } diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go index 7837ce647..4fbacf590 100644 --- a/api/http/proxy/factory/kubernetes/transport.go +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -59,7 +59,7 @@ func (transport *localTransport) RoundTrip(request *http.Request) (*http.Respons if tokenData.Role == portainer.AdministratorRole { token = transport.tokenManager.getAdminServiceAccountToken() } else { - token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID)) if err != nil { return nil, err } @@ -94,7 +94,7 @@ func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Respons if tokenData.Role == portainer.AdministratorRole { token = transport.tokenManager.getAdminServiceAccountToken() } else { - token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID)) if err != nil { return nil, err } @@ -136,7 +136,7 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response if tokenData.Role == portainer.AdministratorRole { token = transport.tokenManager.getAdminServiceAccountToken() } else { - token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username) + token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID)) if err != nil { return nil, err } diff --git a/api/kubernetes/cli/naming.go b/api/kubernetes/cli/naming.go index 9c101e5bd..cbc0c4675 100644 --- a/api/kubernetes/cli/naming.go +++ b/api/kubernetes/cli/naming.go @@ -13,8 +13,8 @@ const ( portainerConfigMapAccessPoliciesKey = "NamespaceAccessPolicies" ) -func userServiceAccountName(userID int, username string) string { - return fmt.Sprintf("%s-%d-%s", portainerUserServiceAccountPrefix, userID, username) +func userServiceAccountName(userID int) string { + return fmt.Sprintf("%s-%d", portainerUserServiceAccountPrefix, userID) } func userServiceAccountTokenSecretName(serviceAccountName string) string { diff --git a/api/kubernetes/cli/service_account.go b/api/kubernetes/cli/service_account.go index 1af1b47f9..52e1b1fe5 100644 --- a/api/kubernetes/cli/service_account.go +++ b/api/kubernetes/cli/service_account.go @@ -8,8 +8,8 @@ import ( ) // GetServiceAccountBearerToken returns the ServiceAccountToken associated to the specified user. -func (kcl *KubeClient) GetServiceAccountBearerToken(userID int, username string) (string, error) { - serviceAccountName := userServiceAccountName(userID, username) +func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error) { + serviceAccountName := userServiceAccountName(userID) return kcl.getServiceAccountToken(serviceAccountName) } @@ -17,8 +17,8 @@ func (kcl *KubeClient) GetServiceAccountBearerToken(userID int, username string) // SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes // cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user. //It will also create required default RoleBinding and ClusterRoleBinding rules. -func (kcl *KubeClient) SetupUserServiceAccount(userID int, username string, teamIDs []int) error { - serviceAccountName := userServiceAccountName(userID, username) +func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error { + serviceAccountName := userServiceAccountName(userID) err := kcl.ensureRequiredResourcesExist() if err != nil { diff --git a/api/portainer.go b/api/portainer.go index fab4fdd46..b96cfdbe7 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -961,8 +961,8 @@ type ( // KubeClient represents a service used to query a Kubernetes environment KubeClient interface { - SetupUserServiceAccount(userID int, username string, teamIDs []int) error - GetServiceAccountBearerToken(userID int, username string) (string, error) + SetupUserServiceAccount(userID int, teamIDs []int) error + GetServiceAccountBearerToken(userID int) (string, error) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error } From 6756b04b67443aae58838c764f70f62ce2ae06ac Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Wed, 5 Aug 2020 00:08:11 +0200 Subject: [PATCH 108/195] feat(k8s/application): add the ability to set the auto-scale policy of an application (#4118) * feat(application): add horizontalpodautoscaler creation * feat(application): Add the ability to set the auto-scale policy of an application * feat(k8s/application): minor UI update * fix(application): set api version and prevent to use hpa with global deployment type * feat(settings): add a switch to enable features based on server metrics * feat(k8s/applications): minor UI update Co-authored-by: Anthony Lapenna --- api/kubernetes.go | 5 +- api/portainer.go | 5 +- app/kubernetes/converters/application.js | 1 + app/kubernetes/helpers/application/index.js | 15 +++ .../horizontal-pod-auto-scaler/converter.js | 112 ++++++++++++++++- .../horizontal-pod-auto-scaler/helper.js | 28 ++--- .../horizontal-pod-auto-scaler/models.js | 2 +- .../horizontal-pod-auto-scaler/payload.js | 86 +++++++++++++ .../horizontal-pod-auto-scaler/service.js | 114 ++++++++--------- .../models/application/formValues.js | 20 ++- app/kubernetes/services/applicationService.js | 38 +++++- .../create/createApplication.html | 118 +++++++++++++++++- .../create/createApplicationController.js | 41 +++++- .../views/applications/edit/application.html | 2 +- app/kubernetes/views/configure/configure.html | 27 +++- .../views/configure/configureController.js | 4 + 16 files changed, 534 insertions(+), 84 deletions(-) create mode 100644 app/kubernetes/horizontal-pod-auto-scaler/payload.js diff --git a/api/kubernetes.go b/api/kubernetes.go index 6ca8a3a78..a4d1beb4f 100644 --- a/api/kubernetes.go +++ b/api/kubernetes.go @@ -3,8 +3,9 @@ package portainer func KubernetesDefault() KubernetesData { return KubernetesData{ Configuration: KubernetesConfiguration{ - UseLoadBalancer: false, - StorageClasses: []KubernetesStorageClassConfig{}, + UseLoadBalancer: false, + UseServerMetrics: false, + StorageClasses: []KubernetesStorageClassConfig{}, }, Snapshots: []KubernetesSnapshot{}, } diff --git a/api/portainer.go b/api/portainer.go index b96cfdbe7..4cc3947fb 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -337,8 +337,9 @@ type ( // KubernetesConfiguration represents the configuration of a Kubernetes endpoint KubernetesConfiguration struct { - UseLoadBalancer bool `json:"UseLoadBalancer"` - StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` + UseLoadBalancer bool `json:"UseLoadBalancer"` + UseServerMetrics bool `json:"UseServerMetrics"` + StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` } // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 4491e2e4a..cd29a1160 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -260,6 +260,7 @@ class KubernetesApplicationConverter { res.EnvironmentVariables = KubernetesApplicationHelper.generateEnvVariablesFromEnv(app.Env); res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations); + res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler); if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) { res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER; diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index 4075b2e0a..8b0513de6 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -9,6 +9,7 @@ import { KubernetesApplicationConfigurationFormValueOverridenKey, KubernetesApplicationPersistedFolderFormValue, KubernetesApplicationPublishedPortFormValue, + KubernetesApplicationAutoScalerFormValue, } from 'Kubernetes/models/application/formValues'; import { KubernetesApplicationEnvConfigMapPayload, @@ -263,6 +264,20 @@ class KubernetesApplicationHelper { return finalRes; } + static generateAutoScalerFormValueFromHorizontalPodAutoScaler(autoScaler) { + const res = new KubernetesApplicationAutoScalerFormValue(); + if (autoScaler) { + res.IsUsed = true; + res.MinReplicas = autoScaler.MinReplicas; + res.MaxReplicas = autoScaler.MaxReplicas; + res.TargetCPUUtilization = autoScaler.TargetCPUUtilization; + res.ApiVersion = autoScaler.ApiVersion; + } else { + res.ApiVersion = 'apps/v1'; + } + return res; + } + /** * !APPLICATION TO FORMVALUES FUNCTIONS */ diff --git a/app/kubernetes/horizontal-pod-auto-scaler/converter.js b/app/kubernetes/horizontal-pod-auto-scaler/converter.js index 4c5ccea75..5ccfdf9c9 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/converter.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/converter.js @@ -1,4 +1,6 @@ +import * as JsonPatch from 'fast-json-patch'; import { KubernetesHorizontalPodAutoScaler } from './models'; +import { KubernetesHorizontalPodAutoScalerCreatePayload } from './payload'; export class KubernetesHorizontalPodAutoScalerConverter { /** @@ -11,7 +13,8 @@ export class KubernetesHorizontalPodAutoScalerConverter { res.Name = data.metadata.name; res.MinReplicas = data.spec.minReplicas; res.MaxReplicas = data.spec.maxReplicas; - res.TargetCPUUtilizationPercentage = data.spec.targetCPUUtilizationPercentage; + res.TargetCPUUtilization = data.spec.targetCPUUtilizationPercentage; + if (data.spec.scaleTargetRef) { res.TargetEntity.ApiVersion = data.spec.scaleTargetRef.apiVersion; res.TargetEntity.Kind = data.spec.scaleTargetRef.kind; @@ -20,4 +23,111 @@ export class KubernetesHorizontalPodAutoScalerConverter { res.Yaml = yaml ? yaml.data : ''; return res; } + + static createPayload(data) { + const payload = new KubernetesHorizontalPodAutoScalerCreatePayload(); + payload.metadata.namespace = data.Namespace; + payload.metadata.name = data.TargetEntity.Name; + payload.spec.minReplicas = data.MinReplicas; + payload.spec.maxReplicas = data.MaxReplicas; + payload.spec.targetCPUUtilizationPercentage = data.TargetCPUUtilization; + payload.spec.scaleTargetRef.apiVersion = data.TargetEntity.ApiVersion; + payload.spec.scaleTargetRef.kind = data.TargetEntity.Kind; + payload.spec.scaleTargetRef.name = data.TargetEntity.Name; + return payload; + } + + static patchPayload(oldScaler, newScaler) { + const oldPayload = KubernetesHorizontalPodAutoScalerConverter.createPayload(oldScaler); + const newPayload = KubernetesHorizontalPodAutoScalerConverter.createPayload(newScaler); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } + + static applicationFormValuesToModel(formValues, kind) { + const res = new KubernetesHorizontalPodAutoScaler(); + res.Name = formValues.Name; + res.Namespace = formValues.ResourcePool.Namespace.Name; + res.MinReplicas = formValues.AutoScaler.MinReplicas; + res.MaxReplicas = formValues.AutoScaler.MaxReplicas; + res.TargetCPUUtilization = formValues.AutoScaler.TargetCPUUtilization; + res.TargetEntity.Name = formValues.Name; + res.TargetEntity.Kind = kind; + res.TargetEntity.ApiVersion = formValues.AutoScaler.ApiVersion; + return res; + } + + /** + * Convertion functions to use with v2beta2 model + */ + + // static apiToModel(data, yaml) { + // const res = new KubernetesHorizontalPodAutoScaler(); + // res.Id = data.metadata.uid; + // res.Namespace = data.metadata.namespace; + // res.Name = data.metadata.name; + // res.MinReplicas = data.spec.minReplicas; + // res.MaxReplicas = data.spec.maxReplicas; + // res.TargetCPUUtilization = data.spec.targetCPUUtilization; + + // _.forEach(data.spec.metrics, (metric) => { + // if (metric.type === 'Resource') { + // if (metric.resource.name === 'cpu') { + // res.TargetCPUUtilization = metric.resource.target.averageUtilization; + // } + // if (metric.resource.name === 'memory') { + // res.TargetMemoryValue = parseFloat(metric.resource.target.averageValue) / 1000; + // } + // } + // }); + + // if (data.spec.scaleTargetRef) { + // res.TargetEntity.ApiVersion = data.spec.scaleTargetRef.apiVersion; + // res.TargetEntity.Kind = data.spec.scaleTargetRef.kind; + // res.TargetEntity.Name = data.spec.scaleTargetRef.name; + // } + // res.Yaml = yaml ? yaml.data : ''; + // return res; + // } + + // static createPayload(data) { + // const payload = new KubernetesHorizontalPodAutoScalerCreatePayload(); + // payload.metadata.namespace = data.Namespace; + // payload.metadata.name = data.TargetEntity.Name; + // payload.spec.minReplicas = data.MinReplicas; + // payload.spec.maxReplicas = data.MaxReplicas; + + // if (data.TargetMemoryValue) { + // const memoryMetric = new KubernetesHorizontalPodAutoScalerMemoryMetric(); + // memoryMetric.resource.target.averageValue = data.TargetMemoryValue; + // payload.spec.metrics.push(memoryMetric); + // } + + // if (data.TargetCPUUtilization) { + // const cpuMetric = new KubernetesHorizontalPodAutoScalerCPUMetric(); + // cpuMetric.resource.target.averageUtilization = data.TargetCPUUtilization; + // payload.spec.metrics.push(cpuMetric); + // } + + // payload.spec.scaleTargetRef.apiVersion = data.TargetEntity.ApiVersion; + // payload.spec.scaleTargetRef.kind = data.TargetEntity.Kind; + // payload.spec.scaleTargetRef.name = data.TargetEntity.Name; + + // return payload; + // } + + // static applicationFormValuesToModel(formValues, kind) { + // const res = new KubernetesHorizontalPodAutoScaler(); + // res.Name = formValues.Name; + // res.Namespace = formValues.ResourcePool.Namespace.Name; + // res.MinReplicas = formValues.AutoScaler.MinReplicas; + // res.MaxReplicas = formValues.AutoScaler.MaxReplicas; + // res.TargetCPUUtilization = formValues.AutoScaler.TargetCPUUtilization; + // if (formValues.AutoScaler.TargetMemoryValue) { + // res.TargetMemoryValue = formValues.AutoScaler.TargetMemoryValue + 'M'; + // } + // res.TargetEntity.Name = formValues.Name; + // res.TargetEntity.Kind = kind; + // return res; + // } } diff --git a/app/kubernetes/horizontal-pod-auto-scaler/helper.js b/app/kubernetes/horizontal-pod-auto-scaler/helper.js index af8061b85..663c3baf8 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/helper.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/helper.js @@ -5,22 +5,22 @@ import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; -function _getApplicationTypeString(app) { - if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT) || app instanceof KubernetesDeployment) { - return KubernetesApplicationTypeStrings.DEPLOYMENT; - } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET) || app instanceof KubernetesDaemonSet) { - return KubernetesApplicationTypeStrings.DAEMONSET; - } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) { - return KubernetesApplicationTypeStrings.STATEFULSET; - // } else if () { ---> TODO: refactor - handle bare pod type ! - } else { - throw new PortainerError('Unable to determine application type'); - } -} - export class KubernetesHorizontalPodAutoScalerHelper { static findApplicationBoundScaler(sList, app) { - const kind = _getApplicationTypeString(app); + const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app); return _.find(sList, (item) => item.TargetEntity.Kind === kind && item.TargetEntity.Name === app.Name); } + + static getApplicationTypeString(app) { + if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT) || app instanceof KubernetesDeployment) { + return KubernetesApplicationTypeStrings.DEPLOYMENT; + } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET) || app instanceof KubernetesDaemonSet) { + return KubernetesApplicationTypeStrings.DAEMONSET; + } else if ((app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET) || app instanceof KubernetesStatefulSet) { + return KubernetesApplicationTypeStrings.STATEFULSET; + // } else if () { ---> TODO: refactor - handle bare pod type ! + } else { + throw new PortainerError('Unable to determine application type'); + } + } } diff --git a/app/kubernetes/horizontal-pod-auto-scaler/models.js b/app/kubernetes/horizontal-pod-auto-scaler/models.js index 1cba85d7d..1e4bf9122 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/models.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/models.js @@ -7,7 +7,7 @@ const _KubernetesHorizontalPodAutoScaler = Object.freeze({ Name: '', MinReplicas: 1, MaxReplicas: 1, - TargetCPUUtilizationPercentage: undefined, + TargetCPUUtilization: 0, TargetEntity: { ApiVersion: '', Kind: '', diff --git a/app/kubernetes/horizontal-pod-auto-scaler/payload.js b/app/kubernetes/horizontal-pod-auto-scaler/payload.js new file mode 100644 index 000000000..d6c67edab --- /dev/null +++ b/app/kubernetes/horizontal-pod-auto-scaler/payload.js @@ -0,0 +1,86 @@ +/** + * KubernetesHorizontalPodAutoScaler Create Payload Model + */ +const _KubernetesHorizontalPodAutoScalerCreatePayload = Object.freeze({ + metadata: { + namespace: '', + name: '', + }, + spec: { + maxReplicas: 0, + minReplicas: 0, + targetCPUUtilizationPercentage: 0, + scaleTargetRef: { + kind: '', + name: '', + }, + }, +}); + +export class KubernetesHorizontalPodAutoScalerCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScalerCreatePayload))); + } +} + +/** + * KubernetesHorizontalPodAutoScaler Create Payload Model for v2beta2 + * Include support of memory usage + */ + +// const _KubernetesHorizontalPodAutoScalerCreatePayload = Object.freeze({ +// metadata: { +// namespace: '', +// name: '' +// }, +// spec: { +// maxReplicas: 0, +// minReplicas: 0, +// targetCPUUtilizationPercentage: 0, +// scaleTargetRef: { +// kind: '', +// name: '' +// }, +// metrics: [] +// } +// }); + +// export class KubernetesHorizontalPodAutoScalerCreatePayload { +// constructor() { +// Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScalerCreatePayload))); +// } +// } + +// const _KubernetesHorizontalPodAutoScalerCPUMetric = Object.freeze({ +// type: 'Resource', +// resource: { +// name: 'cpu', +// target: { +// type: 'Utilization', +// averageUtilization: 0 +// } +// } +// }); + +// export class KubernetesHorizontalPodAutoScalerCPUMetric { +// constructor() { +// Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScalerCPUMetric))); +// } +// } + +// const _KubernetesHorizontalPodAutoScalerMemoryMetric = Object.freeze({ +// type: 'Resource', +// resource: { +// name: 'memory', +// target: { +// type: 'AverageValue', +// averageValue: '' +// } +// } +// }); + +// export class KubernetesHorizontalPodAutoScalerMemoryMetric { +// constructor() { +// Object.assign(this, JSON.parse(JSON.stringify(_KubernetesHorizontalPodAutoScalerMemoryMetric))); +// } +// } diff --git a/app/kubernetes/horizontal-pod-auto-scaler/service.js b/app/kubernetes/horizontal-pod-auto-scaler/service.js index df33457ce..d9ab6f50f 100644 --- a/app/kubernetes/horizontal-pod-auto-scaler/service.js +++ b/app/kubernetes/horizontal-pod-auto-scaler/service.js @@ -12,10 +12,10 @@ class KubernetesHorizontalPodAutoScalerService { this.getAsync = this.getAsync.bind(this); this.getAllAsync = this.getAllAsync.bind(this); - // this.createAsync = this.createAsync.bind(this); - // this.patchAsync = this.patchAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); // this.rollbackAsync = this.rollbackAsync.bind(this); - // this.deleteAsync = this.deleteAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); } /** @@ -53,65 +53,65 @@ class KubernetesHorizontalPodAutoScalerService { return this.$async(this.getAllAsync, namespace); } - // /** - // * CREATE - // */ - // async createAsync(horizontalPodAutoScaler) { - // try { - // const params = {}; - // const payload = KubernetesHorizontalPodAutoScalerConverter.createPayload(horizontalPodAutoScaler); - // const namespace = payload.metadata.namespace; - // const data = await this.KubernetesHorizontalPodAutoScalers(namespace).create(params, payload).$promise; - // return data; - // } catch (err) { - // throw new PortainerError('Unable to create horizontalPodAutoScaler', err); - // } - // } + /** + * CREATE + */ + async createAsync(horizontalPodAutoScaler) { + try { + const params = {}; + const payload = KubernetesHorizontalPodAutoScalerConverter.createPayload(horizontalPodAutoScaler); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesHorizontalPodAutoScalers(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create horizontalPodAutoScaler', err); + } + } - // create(horizontalPodAutoScaler) { - // return this.$async(this.createAsync, horizontalPodAutoScaler); - // } + create(horizontalPodAutoScaler) { + return this.$async(this.createAsync, horizontalPodAutoScaler); + } - // /** - // * PATCH - // */ - // async patchAsync(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { - // try { - // const params = new KubernetesCommonParams(); - // params.id = newHorizontalPodAutoScaler.Name; - // const namespace = newHorizontalPodAutoScaler.Namespace; - // const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); - // if (!payload.length) { - // return; - // } - // const data = await this.KubernetesHorizontalPodAutoScalers(namespace).patch(params, payload).$promise; - // return data; - // } catch (err) { - // throw new PortainerError('Unable to patch horizontalPodAutoScaler', err); - // } - // } + /** + * PATCH + */ + async patchAsync(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { + try { + const params = new KubernetesCommonParams(); + params.id = newHorizontalPodAutoScaler.Name; + const namespace = newHorizontalPodAutoScaler.Namespace; + const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); + if (!payload.length) { + return; + } + const data = await this.KubernetesHorizontalPodAutoScalers(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch horizontalPodAutoScaler', err); + } + } - // patch(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { - // return this.$async(this.patchAsync, oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); - // } + patch(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) { + return this.$async(this.patchAsync, oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler); + } - // /** - // * DELETE - // */ - // async deleteAsync(horizontalPodAutoScaler) { - // try { - // const params = new KubernetesCommonParams(); - // params.id = horizontalPodAutoScaler.Name; - // const namespace = horizontalPodAutoScaler.Namespace; - // await this.KubernetesHorizontalPodAutoScalers(namespace).delete(params).$promise; - // } catch (err) { - // throw new PortainerError('Unable to remove horizontalPodAutoScaler', err); - // } - // } + /** + * DELETE + */ + async deleteAsync(horizontalPodAutoScaler) { + try { + const params = new KubernetesCommonParams(); + params.id = horizontalPodAutoScaler.Name; + const namespace = horizontalPodAutoScaler.Namespace; + await this.KubernetesHorizontalPodAutoScalers(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to remove horizontalPodAutoScaler', err); + } + } - // delete(horizontalPodAutoScaler) { - // return this.$async(this.deleteAsync, horizontalPodAutoScaler); - // } + delete(horizontalPodAutoScaler) { + return this.$async(this.deleteAsync, horizontalPodAutoScaler); + } // /** // * ROLLBACK diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 72af5bbf2..49d3cc827 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -1,4 +1,4 @@ -import { KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes, KubernetesApplicationDataAccessPolicies } from './models'; +import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationPublishingTypes } from './models'; /** * KubernetesApplicationFormValues Model @@ -21,6 +21,7 @@ const _KubernetesApplicationFormValues = Object.freeze({ PublishingType: KubernetesApplicationPublishingTypes.INTERNAL, DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED, Configurations: [], // KubernetesApplicationConfigurationFormValue list + AutoScaler: {}, }); export class KubernetesApplicationFormValues { @@ -116,3 +117,20 @@ export class KubernetesApplicationPublishedPortFormValue { Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPublishedPortFormValue))); } } + +/** + * KubernetesApplicationAutoScalerFormValue Model + */ +const _KubernetesApplicationAutoScalerFormValue = Object.freeze({ + MinReplicas: 0, + MaxReplicas: 0, + TargetCPUUtilization: 50, + ApiVersion: '', + IsUsed: false, +}); + +export class KubernetesApplicationAutoScalerFormValue { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationAutoScalerFormValue))); + } +} diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index d4172f606..a6bfdd2f7 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -12,6 +12,7 @@ import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; import { KubernetesApplication } from 'Kubernetes/models/application/models'; import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper'; +import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter'; class KubernetesApplicationService { /* @ngInject */ @@ -141,13 +142,14 @@ class KubernetesApplicationService { const res = await Promise.all( _.map(namespaces, async (ns) => { - const [deployments, daemonSets, statefulSets, services, pods, ingresses] = await Promise.all([ + const [deployments, daemonSets, statefulSets, services, pods, ingresses, autoScalers] = await Promise.all([ this.KubernetesDeploymentService.get(ns), this.KubernetesDaemonSetService.get(ns), this.KubernetesStatefulSetService.get(ns), this.KubernetesServiceService.get(ns), this.KubernetesPodService.get(ns), this.KubernetesIngressService.get(ns), + this.KubernetesHorizontalPodAutoScalerService.get(ns), ]); const deploymentApplications = _.map(deployments, (item) => @@ -160,7 +162,15 @@ class KubernetesApplicationService { convertToApplication(item, KubernetesApplicationConverter.apiStatefulSetToapplication, services, pods, ingresses) ); - return _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications); + const applications = _.concat(deploymentApplications, daemonSetApplications, statefulSetApplications); + await Promise.all( + _.forEach(applications, async (application) => { + const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application); + const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined; + application.AutoScaler = scaler; + }) + ); + return applications; }) ); return _.flatten(res); @@ -206,6 +216,12 @@ class KubernetesApplicationService { await Promise.all(_.without(claimPromises, undefined)); } + if (formValues.AutoScaler.IsUsed) { + const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app); + const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind); + await this.KubernetesHorizontalPodAutoScalerService.create(autoScaler); + } + await apiService.create(app); } catch (err) { throw err; @@ -257,6 +273,20 @@ class KubernetesApplicationService { } else if (oldService && !newService) { await this.KubernetesServiceService.delete(oldService); } + + const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp); + const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind); + if (_.isEmpty(oldFormValues.AutoScaler)) { + await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler); + } else { + const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp); + const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind); + if (newFormValues.AutoScaler.IsUsed) { + await this.KubernetesHorizontalPodAutoScalerService.patch(oldAutoScaler, newAutoScaler); + } else { + await this.KubernetesHorizontalPodAutoScalerService.delete(oldAutoScaler); + } + } } catch (err) { throw err; } @@ -319,6 +349,10 @@ class KubernetesApplicationService { if (application.ServiceType) { await this.KubernetesServiceService.delete(servicePayload); } + + if (!_.isEmpty(application.AutoScaler)) { + await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler); + } } catch (err) { throw err; } diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index d9421b904..4dfaf0d00 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -635,7 +635,13 @@
    - +
    + +
    + Auto-scaling +
    + +
    +
    + + +
    +
    + +
    +
    +

    + This feature is currently disabled and must be enabled by an administrator user. +

    +

    + Server metrics features must be enabled in the + endpoint configuration view. +

    +
    +
    + +
    + + + + + + + + + + + + + +
    Minimum instancesMaximum instances + Target CPU usage (%) + + +
    +
    + +
    +
    +
    + +

    Minimum instances is required.

    +

    Minimum instances must be greater than 0.

    +

    Minimum instances must be smaller than maximum instances.

    +
    +
    +
    +
    +
    + +
    +
    +
    + +

    Maximum instances is required.

    +

    Maximum instances must be greater than minimum instances.

    +
    +
    +
    +
    +
    + +
    +
    +
    + +

    Target CPU usage is required.

    +

    Target CPU usage must be greater than 0.

    +

    Target CPU usage must be smaller than 100.

    +
    +
    +
    +
    + +
    +
    + + This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy. +
    +
    +
    + +
    Publishing the application
    diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index 86679639f..c91ee8031 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -22,6 +22,7 @@ import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHel import KubernetesApplicationConverter from 'Kubernetes/converters/application'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; +import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index'; class KubernetesCreateApplicationController { /* @ngInject */ @@ -80,6 +81,16 @@ class KubernetesCreateApplicationController { this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication); } + /** + * AUTO SCALER UI MANAGEMENT + */ + + unselectAutoScaler() { + if (this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL) { + this.formValues.AutoScaler.IsUsed = false; + } + } + /** * CONFIGURATION UI MANAGEMENT */ @@ -319,6 +330,24 @@ class KubernetesCreateApplicationController { return false; } + autoScalerOverflow() { + const instances = this.formValues.AutoScaler.MaxReplicas; + const cpu = this.formValues.CpuLimit; + const maxCpu = this.state.sliders.cpu.max; + const memory = this.formValues.MemoryLimit; + const maxMemory = this.state.sliders.memory.max; + + if (cpu * instances > maxCpu) { + return true; + } + + if (memory * instances > maxMemory) { + return true; + } + + return false; + } + publishViaLoadBalancerEnabled() { return this.state.useLoadBalancer; } @@ -345,11 +374,12 @@ class KubernetesCreateApplicationController { isDeployUpdateButtonDisabled() { const overflow = this.resourceReservationsOverflow(); + const autoScalerOverflow = this.autoScalerOverflow(); const inProgress = this.state.actionInProgress; const invalid = !this.isValid(); const hasNoChanges = this.isEditAndNoChangesMade(); const nonScalable = this.isNonScalable(); - const res = overflow || inProgress || invalid || hasNoChanges || nonScalable; + const res = overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable; return res; } @@ -549,6 +579,7 @@ class KubernetesCreateApplicationController { this.state = { actionInProgress: false, useLoadBalancer: false, + useServerMetrics: false, sliders: { cpu: { min: 0, @@ -580,6 +611,8 @@ class KubernetesCreateApplicationController { }, }; + this.isAdmin = this.Authentication.isAdmin(); + this.editChanges = []; if (this.$transition$.params().namespace && this.$transition$.params().name) { @@ -587,8 +620,10 @@ class KubernetesCreateApplicationController { } const endpoint = this.EndpointProvider.currentEndpoint(); + this.endpoint = endpoint; this.storageClasses = endpoint.Kubernetes.Configuration.StorageClasses; this.state.useLoadBalancer = endpoint.Kubernetes.Configuration.UseLoadBalancer; + this.state.useServerMetrics = endpoint.Kubernetes.Configuration.UseServerMetrics; this.formValues = new KubernetesApplicationFormValues(); @@ -611,6 +646,10 @@ class KubernetesCreateApplicationController { this.formValues = KubernetesApplicationConverter.applicationToFormValues(this.application, this.resourcePools, this.configurations, this.persistentVolumeClaims); this.savedFormValues = angular.copy(this.formValues); delete this.formValues.ApplicationType; + } else { + this.formValues.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(); + this.formValues.AutoScaler.MinReplicas = this.formValues.ReplicaCount; + this.formValues.AutoScaler.MaxReplicas = this.formValues.ReplicaCount; } await this.updateSliders(); diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 47eff6d7c..0a656635b 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -355,7 +355,7 @@
    {{ ctrl.application.AutoScaler.MinReplicas }} {{ ctrl.application.AutoScaler.MaxReplicas }}{{ ctrl.application.AutoScaler.TargetCPUUtilizationPercentage }}%{{ ctrl.application.AutoScaler.TargetCPUUtilization }}%
    diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html index 697c19e6c..e50f5a2b2 100644 --- a/app/kubernetes/views/configure/configure.html +++ b/app/kubernetes/views/configure/configure.html @@ -13,10 +13,11 @@
    Expose applications over external IP addresses
    +
    Enabling this feature will allow users to expose application they deploy over an external IP address assigned by cloud provider. -

    +

    Ensure that your cloud provider allows you to create load balancers if you want to use this feature. Might incur costs.

    @@ -31,6 +32,30 @@
    +
    + Metrics +
    + +
    + + Enabling this feature will allow users to use specific features that leverage the server metrics component. +

    + + Ensure that server metrics is + running inside your cluster. +

    +
    +
    + +
    +
    + + +
    +
    +
    Available storage options
    diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index 3a16083ce..7e2430bb6 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -50,12 +50,14 @@ class KubernetesConfigureController { this.endpoint.Kubernetes.Configuration.StorageClasses = classes; this.endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; + this.endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; await this.EndpointService.updateEndpoint(this.endpoint.Id, this.endpoint); const endpoints = this.EndpointProvider.endpoints(); const modifiedEndpoint = _.find(endpoints, (item) => item.Id === this.endpoint.Id); if (modifiedEndpoint) { modifiedEndpoint.Kubernetes.Configuration.StorageClasses = classes; modifiedEndpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; + modifiedEndpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; this.EndpointProvider.setEndpoints(endpoints); } this.Notifications.success('Configuration successfully applied'); @@ -80,6 +82,7 @@ class KubernetesConfigureController { this.formValues = { UseLoadBalancer: false, + UseServerMetrics: false, }; try { @@ -100,6 +103,7 @@ class KubernetesConfigureController { }); this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer; + this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve storage classes'); } finally { From 148ccd1bc41fe8a278b33b56f056d8962630aa3d Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Wed, 5 Aug 2020 02:15:17 +0200 Subject: [PATCH 109/195] feat(node): Show which IP address / port the cluster API is listening on (#4134) * feat(cluster): add kubernetes endpoint resource * feat(cluster): add kubernetes endpoint service * feat(node): Show which IP address / port the cluster API is listening on * fix(cluster): support multi-master clusters * fix(cluster): support multi-master clusters * feat(k8s/cluster): minor UI update * refactor(k8s/cluster): rename variable * refactor(k8s/endpoints): refactor KubernetesEndpointsFactory Co-authored-by: Anthony Lapenna --- .../nodes-datatable/nodesDatatable.html | 5 ++- app/kubernetes/endpoint/converter.js | 12 +++++- app/kubernetes/endpoint/models.js | 12 ++++++ app/kubernetes/node/models.js | 2 + app/kubernetes/rest/endpoint.js | 20 ++++++++++ app/kubernetes/views/cluster/cluster.html | 4 +- .../views/cluster/clusterController.js | 13 ++++++- app/kubernetes/views/cluster/node/node.html | 11 +++++- .../views/cluster/node/nodeController.js | 39 ++++++++++++++++++- 9 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 app/kubernetes/rest/endpoint.js diff --git a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html index c6526a30e..8b976a235 100644 --- a/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html +++ b/app/kubernetes/components/datatables/nodes-datatable/nodesDatatable.html @@ -114,7 +114,10 @@ dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" > - {{ item.Name }} + + {{ item.Name }} + + api {{ item.Name }} {{ item.Role }} diff --git a/app/kubernetes/endpoint/converter.js b/app/kubernetes/endpoint/converter.js index 780a9118c..6dd728785 100644 --- a/app/kubernetes/endpoint/converter.js +++ b/app/kubernetes/endpoint/converter.js @@ -1,4 +1,4 @@ -import { KubernetesEndpoint, KubernetesEndpointAnnotationLeader } from 'Kubernetes/endpoint/models'; +import { KubernetesEndpoint, KubernetesEndpointAnnotationLeader, KubernetesEndpointSubset } from 'Kubernetes/endpoint/models'; import _ from 'lodash-es'; class KubernetesEndpointConverter { @@ -13,6 +13,16 @@ class KubernetesEndpointConverter { const split = _.split(parsedJson.holderIdentity, '_'); res.HolderIdentity = split[0]; } + + if (data.subsets) { + res.Subsets = _.map(data.subsets, (item) => { + const subset = new KubernetesEndpointSubset(); + subset.Ips = _.map(item.addresses, 'ip'); + const port = _.find(item.ports, { name: 'https' }); + subset.Port = port ? port.port : undefined; + return subset; + }); + } return res; } } diff --git a/app/kubernetes/endpoint/models.js b/app/kubernetes/endpoint/models.js index bd493adfd..506573af3 100644 --- a/app/kubernetes/endpoint/models.js +++ b/app/kubernetes/endpoint/models.js @@ -8,6 +8,7 @@ const _KubernetesEndpoint = Object.freeze({ Name: '', Namespace: '', HolderIdentity: '', + Subsets: [], }); export class KubernetesEndpoint { @@ -15,3 +16,14 @@ export class KubernetesEndpoint { Object.assign(this, JSON.parse(JSON.stringify(_KubernetesEndpoint))); } } + +const _KubernetesEndpointSubset = Object.freeze({ + Ips: [], + Port: 0, +}); + +export class KubernetesEndpointSubset { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesEndpointSubset))); + } +} diff --git a/app/kubernetes/node/models.js b/app/kubernetes/node/models.js index 8b77bfece..f1c547969 100644 --- a/app/kubernetes/node/models.js +++ b/app/kubernetes/node/models.js @@ -11,7 +11,9 @@ const _KubernetesNode = Object.freeze({ Memory: '', Version: '', IPAddress: '', + Api: false, Taints: [], + Port: 0, }); export class KubernetesNode { diff --git a/app/kubernetes/rest/endpoint.js b/app/kubernetes/rest/endpoint.js new file mode 100644 index 000000000..7fce22e41 --- /dev/null +++ b/app/kubernetes/rest/endpoint.js @@ -0,0 +1,20 @@ +angular.module('portainer.kubernetes').factory('KubernetesEndpoints', function KubernetesEndpointsFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/api/v1' + (namespace ? '/namespaces/:namespace' : '') + '/endpoints/:id'; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, + }, + } + ); + }; +}); diff --git a/app/kubernetes/views/cluster/cluster.html b/app/kubernetes/views/cluster/cluster.html index ad95331bf..47a198c6e 100644 --- a/app/kubernetes/views/cluster/cluster.html +++ b/app/kubernetes/views/cluster/cluster.html @@ -51,7 +51,7 @@ -
    +
    Leader status
    @@ -62,7 +62,7 @@ Component Leader node - + {{ ep.Name }} diff --git a/app/kubernetes/views/cluster/clusterController.js b/app/kubernetes/views/cluster/clusterController.js index a8ec935c0..7726834d2 100644 --- a/app/kubernetes/views/cluster/clusterController.js +++ b/app/kubernetes/views/cluster/clusterController.js @@ -51,8 +51,17 @@ class KubernetesClusterController { async getEndpointsAsync() { try { - const endpoints = await this.KubernetesEndpointService.get('kube-system'); - this.endpoints = _.filter(endpoints, (ep) => ep.HolderIdentity); + const endpoints = await this.KubernetesEndpointService.get(); + const systemEndpoints = _.filter(endpoints, { Namespace: 'kube-system' }); + this.systemEndpoints = _.filter(systemEndpoints, (ep) => ep.HolderIdentity); + + const kubernetesEndpoint = _.find(endpoints, { Name: 'kubernetes' }); + if (kubernetesEndpoint && kubernetesEndpoint.Subsets) { + const ips = _.flatten(_.map(kubernetesEndpoint.Subsets, 'Ips')); + _.forEach(this.nodes, (node) => { + node.Api = _.includes(ips, node.IPAddress); + }); + } } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve endpoints'); } diff --git a/app/kubernetes/views/cluster/node/node.html b/app/kubernetes/views/cluster/node/node.html index 6fabaaa77..cc503348f 100644 --- a/app/kubernetes/views/cluster/node/node.html +++ b/app/kubernetes/views/cluster/node/node.html @@ -18,7 +18,16 @@ Hostname - {{ ctrl.node.Name }} + + {{ ctrl.node.Name }} + api + + + + + Kubernetes API + + {{ ctrl.node.IPAddress }}:{{ ctrl.node.Port }} Role diff --git a/app/kubernetes/views/cluster/node/nodeController.js b/app/kubernetes/views/cluster/node/nodeController.js index eb4bb7327..1f803965a 100644 --- a/app/kubernetes/views/cluster/node/nodeController.js +++ b/app/kubernetes/views/cluster/node/nodeController.js @@ -6,7 +6,17 @@ import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; class KubernetesNodeController { /* @ngInject */ - constructor($async, $state, Notifications, LocalStorage, KubernetesNodeService, KubernetesEventService, KubernetesPodService, KubernetesApplicationService) { + constructor( + $async, + $state, + Notifications, + LocalStorage, + KubernetesNodeService, + KubernetesEventService, + KubernetesPodService, + KubernetesApplicationService, + KubernetesEndpointService + ) { this.$async = $async; this.$state = $state; this.Notifications = Notifications; @@ -15,18 +25,44 @@ class KubernetesNodeController { this.KubernetesEventService = KubernetesEventService; this.KubernetesPodService = KubernetesPodService; this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesEndpointService = KubernetesEndpointService; this.onInit = this.onInit.bind(this); this.getNodeAsync = this.getNodeAsync.bind(this); this.getEvents = this.getEvents.bind(this); this.getEventsAsync = this.getEventsAsync.bind(this); this.getApplicationsAsync = this.getApplicationsAsync.bind(this); + this.getEndpointsAsync = this.getEndpointsAsync.bind(this); } selectTab(index) { this.LocalStorage.storeActiveTab('node', index); } + async getEndpointsAsync() { + try { + const endpoints = await this.KubernetesEndpointService.get(); + this.endpoint = _.find(endpoints, { Name: 'kubernetes' }); + if (this.endpoint && this.endpoint.Subsets) { + _.forEach(this.endpoint.Subsets, (subset) => { + return _.forEach(subset.Ips, (ip) => { + if (ip === this.node.IPAddress) { + this.node.Api = true; + this.node.Port = subset.Port; + return false; + } + }); + }); + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve endpoints'); + } + } + + getEndpoints() { + return this.$async(this.getEndpointsAsync); + } + async getNodeAsync() { try { this.state.dataLoading = true; @@ -118,6 +154,7 @@ class KubernetesNodeController { await this.getNode(); await this.getEvents(); await this.getApplications(); + await this.getEndpoints(); this.state.viewReady = true; } From 00f4fe00395567af67c24740454fcdd66a325890 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 5 Aug 2020 11:36:46 +0300 Subject: [PATCH 110/195] feat(auth): integrate oauth extension (#4152) * refactor(oauth): move oauth client code * feat(oauth): move extension code into server code * feat(oauth): enable oauth without extension * refactor(oauth): make it easier to remove providers --- api/cmd/portainer/main.go | 10 +- api/go.mod | 1 + api/http/handler/auth/authenticate_oauth.go | 63 ++------- api/http/handler/auth/handler.go | 1 + api/http/server.go | 2 + api/oauth/oauth.go | 130 ++++++++++++++++++ api/portainer.go | 5 + app/extensions/_module.js | 2 +- app/extensions/oauth/__module.js | 1 - .../oauth-providers-selector.html | 49 ------- app/portainer/__module.js | 2 +- app/portainer/oauth/__module.js | 1 + .../oauth-provider-selector-controller.js | 14 +- .../oauth-providers-selector.html | 19 +++ .../oauth-providers-selector.js | 2 +- .../oauth-settings-controller.js | 2 +- .../oauth-settings/oauth-settings.html | 0 .../oauth-settings/oauth-settings.js | 2 +- .../oauth/services/rest/oauth.js | 2 +- .../settingsAuthentication.html | 19 +-- .../settingsAuthenticationController.js | 9 +- 21 files changed, 201 insertions(+), 135 deletions(-) create mode 100644 api/oauth/oauth.go delete mode 100644 app/extensions/oauth/__module.js delete mode 100644 app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html create mode 100644 app/portainer/oauth/__module.js rename app/{extensions => portainer}/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js (77%) create mode 100644 app/portainer/oauth/components/oauth-providers-selector/oauth-providers-selector.html rename app/{extensions => portainer}/oauth/components/oauth-providers-selector/oauth-providers-selector.js (65%) rename app/{extensions => portainer}/oauth/components/oauth-settings/oauth-settings-controller.js (96%) rename app/{extensions => portainer}/oauth/components/oauth-settings/oauth-settings.html (100%) rename app/{extensions => portainer}/oauth/components/oauth-settings/oauth-settings.js (65%) rename app/{extensions => portainer}/oauth/services/rest/oauth.js (85%) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 830a8aede..e3b2d0da9 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -6,7 +6,7 @@ import ( "strings" "time" - portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt" "github.com/portainer/portainer/api/chisel" "github.com/portainer/portainer/api/cli" @@ -24,6 +24,7 @@ import ( kubecli "github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/ldap" "github.com/portainer/portainer/api/libcompose" + "github.com/portainer/portainer/api/oauth" ) func initCLI() *portainer.CLIFlags { @@ -108,6 +109,10 @@ func initLDAPService() portainer.LDAPService { return &ldap.Service{} } +func initOAuthService() portainer.OAuthService { + return oauth.NewService() +} + func initGitService() portainer.GitService { return git.NewService() } @@ -354,6 +359,8 @@ func main() { ldapService := initLDAPService() + oauthService := initOAuthService() + gitService := initGitService() cryptoService := initCryptoService() @@ -467,6 +474,7 @@ func main() { JWTService: jwtService, FileService: fileService, LDAPService: ldapService, + OAuthService: oauthService, GitService: gitService, SignatureService: digitalSignatureService, SnapshotService: snapshotService, diff --git a/api/go.mod b/api/go.mod index 650af2de9..fcee3b6a8 100644 --- a/api/go.mod +++ b/api/go.mod @@ -30,6 +30,7 @@ require ( github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/src-d/go-git.v4 v4.13.1 k8s.io/api v0.17.2 diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index f0d6d59a2..c0e3e8bda 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -1,9 +1,7 @@ package auth import ( - "encoding/json" "errors" - "io/ioutil" "log" "net/http" @@ -27,52 +25,22 @@ func (payload *oauthPayload) Validate(r *http.Request) error { return nil } -func (handler *Handler) authenticateThroughExtension(code, licenseKey string, settings *portainer.OAuthSettings) (string, error) { - extensionURL := handler.ProxyManager.GetExtensionURL(portainer.OAuthAuthenticationExtension) +func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) { + if code == "" { + return "", errors.New("Invalid OAuth authorization code") + } - encodedConfiguration, err := json.Marshal(settings) + if settings == nil { + return "", errors.New("Invalid OAuth configuration") + } + + username, err := handler.OAuthService.Authenticate(code, settings) if err != nil { + log.Printf("[DEBUG] - Unable to authenticate user via OAuth: %v", err) return "", nil } - req, err := http.NewRequest("GET", extensionURL+"/validate", nil) - if err != nil { - return "", err - } - - client := &http.Client{} - req.Header.Set("X-OAuth-Config", string(encodedConfiguration)) - req.Header.Set("X-OAuth-Code", code) - req.Header.Set("X-PortainerExtension-License", licenseKey) - - resp, err := client.Do(req) - if err != nil { - return "", err - } - - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - - type extensionResponse struct { - Username string `json:"Username,omitempty"` - Err string `json:"err,omitempty"` - Details string `json:"details,omitempty"` - } - - var extResp extensionResponse - err = json.Unmarshal(body, &extResp) - if err != nil { - return "", err - } - - if resp.StatusCode != http.StatusOK { - return "", errors.New(extResp.Err + ":" + extResp.Details) - } - - return extResp.Username, nil + return username, nil } func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { @@ -91,14 +59,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")} } - extension, err := handler.DataStore.Extension().Extension(portainer.OAuthAuthenticationExtension) - if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} - } - - username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings) + username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings) if err != nil { log.Printf("[DEBUG] - OAuth authentication error: %s", err) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized} diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index 9bc98f834..120e5980e 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -19,6 +19,7 @@ type Handler struct { CryptoService portainer.CryptoService JWTService portainer.JWTService LDAPService portainer.LDAPService + OAuthService portainer.OAuthService ProxyManager *proxy.Manager AuthorizationService *authorization.Service KubernetesTokenCacheManager *kubernetes.TokenCacheManager diff --git a/api/http/server.go b/api/http/server.go index fb62c842e..f75aaf86e 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -61,6 +61,7 @@ type Server struct { GitService portainer.GitService JWTService portainer.JWTService LDAPService portainer.LDAPService + OAuthService portainer.OAuthService SwarmStackManager portainer.SwarmStackManager Handler *handler.Handler SSL bool @@ -90,6 +91,7 @@ func (server *Server) Start() error { authHandler.ProxyManager = proxyManager authHandler.AuthorizationService = authorizationService authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager + authHandler.OAuthService = server.OAuthService var roleHandler = roles.NewHandler(requestBouncer) roleHandler.DataStore = server.DataStore diff --git a/api/oauth/oauth.go b/api/oauth/oauth.go new file mode 100644 index 000000000..7b6866c90 --- /dev/null +++ b/api/oauth/oauth.go @@ -0,0 +1,130 @@ +package oauth + +import ( + "context" + "encoding/json" + "fmt" + "golang.org/x/oauth2" + "io/ioutil" + "log" + "mime" + "net/http" + "net/url" + + "github.com/portainer/portainer/api" +) + +// Service represents a service used to authenticate users against an authorization server +type Service struct{} + +// NewService returns a pointer to a new instance of this service +func NewService() *Service { + return &Service{} +} + +// Authenticate takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint. +// On success, it will then return the username associated to authenticated user by fetching this information +// from the resource server and matching it with the user identifier setting. +func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) { + token, err := getAccessToken(code, configuration) + if err != nil { + log.Printf("[DEBUG] - Failed retrieving access token: %v", err) + return "", err + } + + return getUsername(token, configuration) +} + +func getAccessToken(code string, configuration *portainer.OAuthSettings) (string, error) { + unescapedCode, err := url.QueryUnescape(code) + if err != nil { + return "", err + } + + config := buildConfig(configuration) + token, err := config.Exchange(context.Background(), unescapedCode) + if err != nil { + return "", err + } + + return token.AccessToken, nil +} + +func getUsername(token string, configuration *portainer.OAuthSettings) (string, error) { + req, err := http.NewRequest("GET", configuration.ResourceURI, nil) + if err != nil { + return "", err + } + + client := &http.Client{} + req.Header.Set("Authorization", "Bearer "+token) + resp, err := client.Do(req) + if err != nil { + return "", err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", &oauth2.RetrieveError{ + Response: resp, + Body: body, + } + } + + content, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return "", err + } + + if content == "application/x-www-form-urlencoded" || content == "text/plain" { + values, err := url.ParseQuery(string(body)) + if err != nil { + return "", err + } + + username := values.Get(configuration.UserIdentifier) + return username, nil + } + + var datamap map[string]interface{} + if err = json.Unmarshal(body, &datamap); err != nil { + return "", err + } + + username, ok := datamap[configuration.UserIdentifier].(string) + if ok && username != "" { + return username, nil + } + + if !ok { + username, ok := datamap[configuration.UserIdentifier].(float64) + if ok && username != 0 { + return fmt.Sprint(int(username)), nil + } + } + + return "", &oauth2.RetrieveError{ + Response: resp, + Body: body, + } +} + +func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config { + endpoint := oauth2.Endpoint{ + AuthURL: configuration.AuthorizationURI, + TokenURL: configuration.AccessTokenURI, + } + + return &oauth2.Config{ + ClientID: configuration.ClientID, + ClientSecret: configuration.ClientSecret, + Endpoint: endpoint, + RedirectURL: configuration.RedirectURI, + Scopes: []string{configuration.Scopes}, + } +} diff --git a/api/portainer.go b/api/portainer.go index 4cc3947fb..4226bb778 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -984,6 +984,11 @@ type ( GetUserGroups(username string, settings *LDAPSettings) ([]string, error) } + // OAuthService represents a service used to authenticate users using OAuth + OAuthService interface { + Authenticate(code string, configuration *OAuthSettings) (string, error) + } + // RegistryService represents a service for managing registry data RegistryService interface { Registry(ID RegistryID) (*Registry, error) diff --git a/app/extensions/_module.js b/app/extensions/_module.js index ebf2143cd..47dfa09a3 100644 --- a/app/extensions/_module.js +++ b/app/extensions/_module.js @@ -1 +1 @@ -angular.module('portainer.extensions', ['portainer.extensions.registrymanagement', 'portainer.extensions.oauth', 'portainer.extensions.rbac']); +angular.module('portainer.extensions', ['portainer.extensions.registrymanagement', 'portainer.extensions.rbac']); diff --git a/app/extensions/oauth/__module.js b/app/extensions/oauth/__module.js deleted file mode 100644 index d9e386521..000000000 --- a/app/extensions/oauth/__module.js +++ /dev/null @@ -1 +0,0 @@ -angular.module('portainer.extensions.oauth', ['ngResource']).constant('API_ENDPOINT_OAUTH', 'api/auth/oauth'); diff --git a/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html b/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html deleted file mode 100644 index a01ec3d41..000000000 --- a/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html +++ /dev/null @@ -1,49 +0,0 @@ -
    - Provider -
    - -
    -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 5f3907272..9bb8fb74b 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -25,7 +25,7 @@ function initAnalytics(Analytics, $rootScope) { }); } -angular.module('portainer.app', []).config([ +angular.module('portainer.app', ['portainer.oauth']).config([ '$stateRegistryProvider', function ($stateRegistryProvider) { 'use strict'; diff --git a/app/portainer/oauth/__module.js b/app/portainer/oauth/__module.js new file mode 100644 index 000000000..f6b119235 --- /dev/null +++ b/app/portainer/oauth/__module.js @@ -0,0 +1 @@ +angular.module('portainer.oauth', ['ngResource']).constant('API_ENDPOINT_OAUTH', 'api/auth/oauth'); diff --git a/app/extensions/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js b/app/portainer/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js similarity index 77% rename from app/extensions/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js rename to app/portainer/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js index 3adc8d39a..6d7afc099 100644 --- a/app/extensions/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js +++ b/app/portainer/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js @@ -1,4 +1,4 @@ -angular.module('portainer.extensions.oauth').controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() { +angular.module('portainer.oauth').controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() { var ctrl = this; this.providers = [ @@ -9,6 +9,9 @@ angular.module('portainer.extensions.oauth').controller('OAuthProviderSelectorCo userIdentifier: 'userPrincipalName', scopes: 'id,email,name', name: 'microsoft', + label: 'Microsoft', + description: 'Microsoft OAuth provider', + icon: 'fab fa-microsoft', }, { authUrl: 'https://accounts.google.com/o/oauth2/auth', @@ -17,6 +20,9 @@ angular.module('portainer.extensions.oauth').controller('OAuthProviderSelectorCo userIdentifier: 'email', scopes: 'profile email', name: 'google', + label: 'Google', + description: 'Google OAuth provider', + icon: 'fab fa-google', }, { authUrl: 'https://github.com/login/oauth/authorize', @@ -25,6 +31,9 @@ angular.module('portainer.extensions.oauth').controller('OAuthProviderSelectorCo userIdentifier: 'login', scopes: 'id email name', name: 'github', + label: 'Github', + description: 'Github OAuth provider', + icon: 'fab fa-github', }, { authUrl: '', @@ -33,6 +42,9 @@ angular.module('portainer.extensions.oauth').controller('OAuthProviderSelectorCo userIdentifier: '', scopes: '', name: 'custom', + label: 'Custom', + description: 'Custom OAuth provider', + icon: 'fa fa-user-check', }, ]; diff --git a/app/portainer/oauth/components/oauth-providers-selector/oauth-providers-selector.html b/app/portainer/oauth/components/oauth-providers-selector/oauth-providers-selector.html new file mode 100644 index 000000000..17dffd726 --- /dev/null +++ b/app/portainer/oauth/components/oauth-providers-selector/oauth-providers-selector.html @@ -0,0 +1,19 @@ +
    + Provider +
    + +
    +
    +
    +
    + + +
    +
    +
    diff --git a/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.js b/app/portainer/oauth/components/oauth-providers-selector/oauth-providers-selector.js similarity index 65% rename from app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.js rename to app/portainer/oauth/components/oauth-providers-selector/oauth-providers-selector.js index 2ce2941e2..d0a1dca08 100644 --- a/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.js +++ b/app/portainer/oauth/components/oauth-providers-selector/oauth-providers-selector.js @@ -1,4 +1,4 @@ -angular.module('portainer.extensions.oauth').component('oauthProvidersSelector', { +angular.module('portainer.oauth').component('oauthProvidersSelector', { templateUrl: './oauth-providers-selector.html', bindings: { onSelect: '<', diff --git a/app/extensions/oauth/components/oauth-settings/oauth-settings-controller.js b/app/portainer/oauth/components/oauth-settings/oauth-settings-controller.js similarity index 96% rename from app/extensions/oauth/components/oauth-settings/oauth-settings-controller.js rename to app/portainer/oauth/components/oauth-settings/oauth-settings-controller.js index 085715982..ba1424956 100644 --- a/app/extensions/oauth/components/oauth-settings/oauth-settings-controller.js +++ b/app/portainer/oauth/components/oauth-settings/oauth-settings-controller.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; -angular.module('portainer.extensions.oauth').controller('OAuthSettingsController', function OAuthSettingsController() { +angular.module('portainer.oauth').controller('OAuthSettingsController', function OAuthSettingsController() { var ctrl = this; this.state = { diff --git a/app/extensions/oauth/components/oauth-settings/oauth-settings.html b/app/portainer/oauth/components/oauth-settings/oauth-settings.html similarity index 100% rename from app/extensions/oauth/components/oauth-settings/oauth-settings.html rename to app/portainer/oauth/components/oauth-settings/oauth-settings.html diff --git a/app/extensions/oauth/components/oauth-settings/oauth-settings.js b/app/portainer/oauth/components/oauth-settings/oauth-settings.js similarity index 65% rename from app/extensions/oauth/components/oauth-settings/oauth-settings.js rename to app/portainer/oauth/components/oauth-settings/oauth-settings.js index 818fc69c9..ffe6b1739 100644 --- a/app/extensions/oauth/components/oauth-settings/oauth-settings.js +++ b/app/portainer/oauth/components/oauth-settings/oauth-settings.js @@ -1,4 +1,4 @@ -angular.module('portainer.extensions.oauth').component('oauthSettings', { +angular.module('portainer.oauth').component('oauthSettings', { templateUrl: './oauth-settings.html', bindings: { settings: '=', diff --git a/app/extensions/oauth/services/rest/oauth.js b/app/portainer/oauth/services/rest/oauth.js similarity index 85% rename from app/extensions/oauth/services/rest/oauth.js rename to app/portainer/oauth/services/rest/oauth.js index d3db66b2f..b221f7d23 100644 --- a/app/extensions/oauth/services/rest/oauth.js +++ b/app/portainer/oauth/services/rest/oauth.js @@ -1,4 +1,4 @@ -angular.module('portainer.extensions.oauth').factory('OAuth', [ +angular.module('portainer.oauth').factory('OAuth', [ '$resource', 'API_ENDPOINT_OAUTH', function OAuthFactory($resource, API_ENDPOINT_OAUTH) { diff --git a/app/portainer/views/settings/authentication/settingsAuthentication.html b/app/portainer/views/settings/authentication/settingsAuthentication.html index be58b78da..0ecab33e1 100644 --- a/app/portainer/views/settings/authentication/settingsAuthentication.html +++ b/app/portainer/views/settings/authentication/settingsAuthentication.html @@ -57,7 +57,7 @@

    LDAP authentication

    -
    +
    diff --git a/app/portainer/views/settings/authentication/settingsAuthenticationController.js b/app/portainer/views/settings/authentication/settingsAuthenticationController.js index 97eb2a5dc..f03b88f46 100644 --- a/app/portainer/views/settings/authentication/settingsAuthenticationController.js +++ b/app/portainer/views/settings/authentication/settingsAuthenticationController.js @@ -6,8 +6,7 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [ 'SettingsService', 'FileUploadService', 'TeamService', - 'ExtensionService', - function ($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService, ExtensionService) { + function ($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService) { $scope.state = { successfulConnectivityCheck: false, failedConnectivityCheck: false, @@ -68,10 +67,6 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [ }, }; - $scope.goToOAuthExtensionView = function () { - $state.go('portainer.extensions.extension', { id: 2 }); - }; - $scope.isOauthEnabled = function isOauthEnabled() { return $scope.settings && $scope.settings.AuthenticationMethod === 3; }; @@ -167,7 +162,6 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [ $q.all({ settings: SettingsService.settings(), teams: TeamService.teams(), - oauthAuthentication: ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.OAUTH_AUTHENTICATION), }) .then(function success(data) { var settings = data.settings; @@ -176,7 +170,6 @@ angular.module('portainer.app').controller('SettingsAuthenticationController', [ $scope.formValues.LDAPSettings = settings.LDAPSettings; $scope.OAuthSettings = settings.OAuthSettings; $scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert; - $scope.oauthAuthenticationAvailable = data.oauthAuthentication; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings'); From ff250a202a0fd5926103407e347c2b24a73048b3 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 5 Aug 2020 13:13:23 +0300 Subject: [PATCH 111/195] feat(extensions): remove oauth extension (#4156) * feat(oauth): remove oauth providers * feat(extensions): remove references to oauth extension --- api/exec/extension.go | 7 +-- api/http/proxy/factory/factory.go | 5 +- api/portainer.go | 2 +- .../oauth-provider-selector-controller.js | 33 ---------- .../oauth-settings-controller.js | 60 +------------------ .../oauth-settings/oauth-settings.html | 46 +++----------- 6 files changed, 15 insertions(+), 138 deletions(-) diff --git a/api/exec/extension.go b/api/exec/extension.go index d70cd98bb..036cfc01a 100644 --- a/api/exec/extension.go +++ b/api/exec/extension.go @@ -26,9 +26,8 @@ var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/" var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`) var extensionBinaryMap = map[portainer.ExtensionID]string{ - portainer.RegistryManagementExtension: "extension-registry-management", - portainer.OAuthAuthenticationExtension: "extension-oauth-authentication", - portainer.RBACExtension: "extension-rbac", + portainer.RegistryManagementExtension: "extension-registry-management", + portainer.RBACExtension: "extension-rbac", } // ExtensionManager represents a service used to @@ -114,8 +113,6 @@ func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension switch extension.ID { case portainer.RegistryManagementExtension: extension.Name = "Registry Manager" - case portainer.OAuthAuthenticationExtension: - extension.Name = "External Authentication" case portainer.RBACExtension: extension.Name = "Role-Based Access Control" } diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go index e3e4c1e3b..3068a8cea 100644 --- a/api/http/proxy/factory/factory.go +++ b/api/http/proxy/factory/factory.go @@ -17,9 +17,8 @@ import ( const azureAPIBaseURL = "https://management.azure.com" var extensionPorts = map[portainer.ExtensionID]string{ - portainer.RegistryManagementExtension: "7001", - portainer.OAuthAuthenticationExtension: "7002", - portainer.RBACExtension: "7003", + portainer.RegistryManagementExtension: "7001", + portainer.RBACExtension: "7003", } type ( diff --git a/api/portainer.go b/api/portainer.go index 4226bb778..b87666e17 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1257,7 +1257,7 @@ const ( _ ExtensionID = iota // RegistryManagementExtension represents the registry management extension RegistryManagementExtension - // OAuthAuthenticationExtension represents the OAuth authentication extension + // OAuthAuthenticationExtension represents the OAuth authentication extension (Deprecated) OAuthAuthenticationExtension // RBACExtension represents the RBAC extension RBACExtension diff --git a/app/portainer/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js b/app/portainer/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js index 6d7afc099..32a527120 100644 --- a/app/portainer/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js +++ b/app/portainer/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js @@ -2,39 +2,6 @@ angular.module('portainer.oauth').controller('OAuthProviderSelectorController', var ctrl = this; this.providers = [ - { - authUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', - accessTokenUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/token', - resourceUrl: 'https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', - userIdentifier: 'userPrincipalName', - scopes: 'id,email,name', - name: 'microsoft', - label: 'Microsoft', - description: 'Microsoft OAuth provider', - icon: 'fab fa-microsoft', - }, - { - authUrl: 'https://accounts.google.com/o/oauth2/auth', - accessTokenUrl: 'https://accounts.google.com/o/oauth2/token', - resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json', - userIdentifier: 'email', - scopes: 'profile email', - name: 'google', - label: 'Google', - description: 'Google OAuth provider', - icon: 'fab fa-google', - }, - { - authUrl: 'https://github.com/login/oauth/authorize', - accessTokenUrl: 'https://github.com/login/oauth/access_token', - resourceUrl: 'https://api.github.com/user', - userIdentifier: 'login', - scopes: 'id email name', - name: 'github', - label: 'Github', - description: 'Github OAuth provider', - icon: 'fab fa-github', - }, { authUrl: '', accessTokenUrl: '', diff --git a/app/portainer/oauth/components/oauth-settings/oauth-settings-controller.js b/app/portainer/oauth/components/oauth-settings/oauth-settings-controller.js index ba1424956..84b8634f9 100644 --- a/app/portainer/oauth/components/oauth-settings/oauth-settings-controller.js +++ b/app/portainer/oauth/components/oauth-settings/oauth-settings-controller.js @@ -1,75 +1,19 @@ -import _ from 'lodash-es'; - angular.module('portainer.oauth').controller('OAuthSettingsController', function OAuthSettingsController() { var ctrl = this; this.state = { provider: {}, - overrideConfiguration: false, - microsoftTenantID: '', }; - this.$onInit = onInit; - this.onSelectProvider = onSelectProvider; - this.onMicrosoftTenantIDChange = onMicrosoftTenantIDChange; - this.useDefaultProviderConfiguration = useDefaultProviderConfiguration; + this.$onInit = $onInit; - function onMicrosoftTenantIDChange() { - var tenantID = ctrl.state.microsoftTenantID; - - ctrl.settings.AuthorizationURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', 'TENANT_ID', tenantID); - ctrl.settings.AccessTokenURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/token', 'TENANT_ID', tenantID); - ctrl.settings.ResourceURI = _.replace('https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', 'TENANT_ID', tenantID); - } - - function useDefaultProviderConfiguration() { - ctrl.settings.AuthorizationURI = ctrl.state.provider.authUrl; - ctrl.settings.AccessTokenURI = ctrl.state.provider.accessTokenUrl; - ctrl.settings.ResourceURI = ctrl.state.provider.resourceUrl; - ctrl.settings.UserIdentifier = ctrl.state.provider.userIdentifier; - ctrl.settings.Scopes = ctrl.state.provider.scopes; - - if (ctrl.state.provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') { - onMicrosoftTenantIDChange(); - } - } - - function useExistingConfiguration() { - var provider = ctrl.state.provider; - ctrl.settings.AuthorizationURI = ctrl.settings.AuthorizationURI === '' ? provider.authUrl : ctrl.settings.AuthorizationURI; - ctrl.settings.AccessTokenURI = ctrl.settings.AccessTokenURI === '' ? provider.accessTokenUrl : ctrl.settings.AccessTokenURI; - ctrl.settings.ResourceURI = ctrl.settings.ResourceURI === '' ? provider.resourceUrl : ctrl.settings.ResourceURI; - ctrl.settings.UserIdentifier = ctrl.settings.UserIdentifier === '' ? provider.userIdentifier : ctrl.settings.UserIdentifier; - ctrl.settings.Scopes = ctrl.settings.Scopes === '' ? provider.scopes : ctrl.settings.Scopes; - - if (provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') { - onMicrosoftTenantIDChange(); - } - } - - function onSelectProvider(provider, overrideConfiguration) { - ctrl.state.provider = provider; - - if (overrideConfiguration) { - useDefaultProviderConfiguration(); - } else { - useExistingConfiguration(); - } - } - - function onInit() { + function $onInit() { if (ctrl.settings.RedirectURI === '') { ctrl.settings.RedirectURI = window.location.origin; } if (ctrl.settings.AuthorizationURI !== '') { ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI; - - if (ctrl.settings.AuthorizationURI.indexOf('login.microsoftonline.com') > -1) { - var tenantID = ctrl.settings.AuthorizationURI.match(/login.microsoftonline.com\/(.*?)\//)[1]; - ctrl.state.microsoftTenantID = tenantID; - onMicrosoftTenantIDChange(); - } } } }); diff --git a/app/portainer/oauth/components/oauth-settings/oauth-settings.html b/app/portainer/oauth/components/oauth-settings/oauth-settings.html index 0050835e9..f6703c812 100644 --- a/app/portainer/oauth/components/oauth-settings/oauth-settings.html +++ b/app/portainer/oauth/components/oauth-settings/oauth-settings.html @@ -39,30 +39,11 @@
    - -
    OAuth Configuration
    -
    - -
    - -
    -
    -
    @@ -72,14 +53,14 @@
    -
    +
    -
    +
    -
    +
    -
    +
    - From 7e90bf11b7a576e3d7bf98d723c156d53765c3cf Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 5 Aug 2020 13:14:28 +0300 Subject: [PATCH 112/195] fix(datatables): deselect row (#4122) * fix(datatables): deselect row * fix(datatables): enable batch select * fix(registry): select registry items --- .../components/datatables/genericDatatableController.js | 4 ++-- .../views/registries/create/createRegistryController.js | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/portainer/components/datatables/genericDatatableController.js b/app/portainer/components/datatables/genericDatatableController.js index 62f4c960a..19e04f5b2 100644 --- a/app/portainer/components/datatables/genericDatatableController.js +++ b/app/portainer/components/datatables/genericDatatableController.js @@ -57,7 +57,7 @@ angular.module('portainer.app').controller('GenericDatatableController', [ const itemsInRange = _.filter(this.state.filteredDataSet, (item, index) => { return isBetween(index, firstItemIndex, lastItemIndex); }); - const value = item.Checked; + const value = this.state.firstClickedItem.Checked; _.forEach(itemsInRange, (i) => { if (!this.allowSelection(i)) { @@ -67,7 +67,7 @@ angular.module('portainer.app').controller('GenericDatatableController', [ }); this.state.firstClickedItem = item; } else if (event) { - item.Checked = true; + item.Checked = !item.Checked; this.state.firstClickedItem = item; } this.state.selectedItems = this.state.filteredDataSet.filter((i) => i.Checked); diff --git a/app/portainer/views/registries/create/createRegistryController.js b/app/portainer/views/registries/create/createRegistryController.js index 50cdb265b..4cab30dc2 100644 --- a/app/portainer/views/registries/create/createRegistryController.js +++ b/app/portainer/views/registries/create/createRegistryController.js @@ -21,7 +21,12 @@ angular.module('portainer.app').controller('CreateRegistryController', [ $scope.state = { actionInProgress: false, overrideConfiguration: false, - gitlab: {}, + gitlab: { + get selectedItemCount() { + return this.selectedItems.length || 0; + }, + selectedItems: [], + }, }; function selectQuayRegistry() { From 82064152ecc5af66410e87bb6853ffd1ac6e4149 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 5 Aug 2020 13:23:19 +0300 Subject: [PATCH 113/195] feat(registries): remove registry extension (#4155) * feat(registries): remove client extension code * feat(registry): remove server registry code * refactor(registry): remove extension related code * feat(extensions): remove registry extension type --- api/exec/extension.go | 5 +- api/http/handler/registries/handler.go | 4 - api/http/handler/registries/proxy.go | 85 ---- .../registries/proxy_management_gitlab.go | 68 --- api/http/proxy/factory/factory.go | 3 +- api/portainer.go | 2 +- .../porImageRegistryController.js | 2 +- app/docker/helpers/imageHelper.js | 2 +- app/extensions/_module.js | 2 +- app/extensions/registry-management/_module.js | 55 --- .../registryRepositoriesDatatable.html | 76 ---- .../registryRepositoriesDatatable.js | 14 - ...registryRepositoriesDatatableController.js | 41 -- .../registriesRepositoryTagsDatatable.html | 108 ----- .../registriesRepositoryTagsDatatable.js | 17 - ...stryRepositoriesTagsDatatableController.js | 37 -- .../helpers/localRegistryHelper.js | 35 -- .../models/registryImageDetails.js | 14 - .../models/registryImageLayer.js | 8 - .../models/repositoryTag.js | 22 - .../registry-management/rest/catalog.js | 34 -- .../rest/manifestJquery.js | 90 ---- .../registry-management/rest/tags.js | 20 - .../rest/transform/linkGetResponse.js | 13 - .../services/genericAsyncGenerator.js | 34 -- .../services/registryServiceSelector.js | 84 ---- .../services/registryV2Service.js | 310 ------------- .../configure/configureRegistryController.js | 75 ---- .../views/configure/configureregistry.html | 170 ------- .../progression-modal/progressionModal.html | 11 - .../progression-modal/progressionModal.js | 6 - .../repositories/edit/registryRepository.html | 134 ------ .../edit/registryRepositoryController.js | 423 ------------------ .../repositories/registryRepositories.html | 45 -- .../registryRepositoriesController.js | 68 --- .../tag/registryRepositoryTag.html | 177 -------- .../tag/registryRepositoryTagController.js | 56 --- .../registriesDatatable.html | 15 - .../registriesDatatable.js | 1 - .../registry-form-gitlab.html | 4 - .../models/gitlabRegistry.js | 0 app/portainer/models/registry.js | 2 +- .../models/registryRepository.js | 0 .../models/registryTypes.js | 0 .../rest/gitlab.js | 2 +- .../rest/transform/gitlabResponseGetLink.js | 0 app/portainer/services/api/registryService.js | 2 +- .../services/registryGitlabService.js | 2 +- .../create/createRegistryController.js | 6 +- .../views/registries/registries.html | 1 - .../views/registries/registriesController.js | 5 +- 51 files changed, 13 insertions(+), 2377 deletions(-) delete mode 100644 api/http/handler/registries/proxy.go delete mode 100644 api/http/handler/registries/proxy_management_gitlab.go delete mode 100644 app/extensions/registry-management/_module.js delete mode 100644 app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html delete mode 100644 app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.js delete mode 100644 app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatableController.js delete mode 100644 app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.html delete mode 100644 app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.js delete mode 100644 app/extensions/registry-management/components/registries-repository-tags-datatable/registryRepositoriesTagsDatatableController.js delete mode 100644 app/extensions/registry-management/helpers/localRegistryHelper.js delete mode 100644 app/extensions/registry-management/models/registryImageDetails.js delete mode 100644 app/extensions/registry-management/models/registryImageLayer.js delete mode 100644 app/extensions/registry-management/models/repositoryTag.js delete mode 100644 app/extensions/registry-management/rest/catalog.js delete mode 100644 app/extensions/registry-management/rest/manifestJquery.js delete mode 100644 app/extensions/registry-management/rest/tags.js delete mode 100644 app/extensions/registry-management/rest/transform/linkGetResponse.js delete mode 100644 app/extensions/registry-management/services/genericAsyncGenerator.js delete mode 100644 app/extensions/registry-management/services/registryServiceSelector.js delete mode 100644 app/extensions/registry-management/services/registryV2Service.js delete mode 100644 app/extensions/registry-management/views/configure/configureRegistryController.js delete mode 100644 app/extensions/registry-management/views/configure/configureregistry.html delete mode 100644 app/extensions/registry-management/views/repositories/edit/progression-modal/progressionModal.html delete mode 100644 app/extensions/registry-management/views/repositories/edit/progression-modal/progressionModal.js delete mode 100644 app/extensions/registry-management/views/repositories/edit/registryRepository.html delete mode 100644 app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js delete mode 100644 app/extensions/registry-management/views/repositories/registryRepositories.html delete mode 100644 app/extensions/registry-management/views/repositories/registryRepositoriesController.js delete mode 100644 app/extensions/registry-management/views/repositories/tag/registryRepositoryTag.html delete mode 100644 app/extensions/registry-management/views/repositories/tag/registryRepositoryTagController.js rename app/{extensions/registry-management => portainer}/models/gitlabRegistry.js (100%) rename app/{extensions/registry-management => portainer}/models/registryRepository.js (100%) rename app/{extensions/registry-management => portainer}/models/registryTypes.js (100%) rename app/{extensions/registry-management => portainer}/rest/gitlab.js (92%) rename app/{extensions/registry-management => portainer}/rest/transform/gitlabResponseGetLink.js (100%) rename app/{extensions/registry-management => portainer}/services/registryGitlabService.js (96%) diff --git a/api/exec/extension.go b/api/exec/extension.go index 036cfc01a..049bf9851 100644 --- a/api/exec/extension.go +++ b/api/exec/extension.go @@ -26,8 +26,7 @@ var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/" var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`) var extensionBinaryMap = map[portainer.ExtensionID]string{ - portainer.RegistryManagementExtension: "extension-registry-management", - portainer.RBACExtension: "extension-rbac", + portainer.RBACExtension: "extension-rbac", } // ExtensionManager represents a service used to @@ -111,8 +110,6 @@ func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension } switch extension.ID { - case portainer.RegistryManagementExtension: - extension.Name = "Registry Manager" case portainer.RBACExtension: extension.Name = "Role-Based Access Control" } diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go index 74a933586..035385346 100644 --- a/api/http/handler/registries/handler.go +++ b/api/http/handler/registries/handler.go @@ -43,10 +43,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { bouncer.AdminAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost) h.Handle("/registries/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete) - h.PathPrefix("/registries/{id}/v2").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI))) - h.PathPrefix("/registries/{id}/proxies/gitlab").Handler( - bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithRegistry))) h.PathPrefix("/registries/proxies/gitlab").Handler( bouncer.AdminAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithoutRegistry))) return h diff --git a/api/http/handler/registries/proxy.go b/api/http/handler/registries/proxy.go deleted file mode 100644 index d452e8e66..000000000 --- a/api/http/handler/registries/proxy.go +++ /dev/null @@ -1,85 +0,0 @@ -package registries - -import ( - "encoding/json" - "net/http" - "strconv" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" - bolterrors "github.com/portainer/portainer/api/bolt/errors" - "github.com/portainer/portainer/api/http/errors" -) - -// request on /api/registries/:id/v2 -func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - registryID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err} - } - - registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} - } - - err = handler.requestBouncer.RegistryAccess(r, registry) - if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied} - } - - extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension) - if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} - } - - var proxy http.Handler - proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension) - if proxy == nil { - proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err} - } - } - - managementConfiguration := registry.ManagementConfiguration - if managementConfiguration == nil { - managementConfiguration = createDefaultManagementConfiguration(registry) - } - - encodedConfiguration, err := json.Marshal(managementConfiguration) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err} - } - - id := strconv.Itoa(int(registryID)) - r.Header.Set("X-RegistryManagement-Key", id) - r.Header.Set("X-RegistryManagement-URI", registry.URL) - r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration)) - r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey) - - http.StripPrefix("/registries/"+id, proxy).ServeHTTP(w, r) - return nil -} - -func createDefaultManagementConfiguration(registry *portainer.Registry) *portainer.RegistryManagementConfiguration { - config := &portainer.RegistryManagementConfiguration{ - Type: registry.Type, - TLSConfig: portainer.TLSConfiguration{ - TLS: false, - }, - } - - if registry.Authentication { - config.Authentication = true - config.Username = registry.Username - config.Password = registry.Password - } - - return config -} diff --git a/api/http/handler/registries/proxy_management_gitlab.go b/api/http/handler/registries/proxy_management_gitlab.go deleted file mode 100644 index 6a8656c38..000000000 --- a/api/http/handler/registries/proxy_management_gitlab.go +++ /dev/null @@ -1,68 +0,0 @@ -package registries - -import ( - "encoding/json" - "net/http" - "strconv" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" - bolterrors "github.com/portainer/portainer/api/bolt/errors" - "github.com/portainer/portainer/api/http/errors" -) - -// request on /api/registries/{id}/proxies/gitlab -func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - registryID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err} - } - - registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID)) - if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err} - } - - err = handler.requestBouncer.RegistryAccess(r, registry) - if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied} - } - - extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension) - if err == bolterrors.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} - } - - var proxy http.Handler - proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension) - if proxy == nil { - proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err} - } - } - - config := &portainer.RegistryManagementConfiguration{ - Type: portainer.GitlabRegistry, - Password: registry.Password, - } - - encodedConfiguration, err := json.Marshal(config) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err} - } - - id := strconv.Itoa(int(registryID)) - r.Header.Set("X-RegistryManagement-Key", id+"-gitlab") - r.Header.Set("X-RegistryManagement-URI", registry.Gitlab.InstanceURL) - r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration)) - r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey) - - http.StripPrefix("/registries/"+id+"/proxies/gitlab", proxy).ServeHTTP(w, r) - return nil -} diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go index 3068a8cea..5beaba1f6 100644 --- a/api/http/proxy/factory/factory.go +++ b/api/http/proxy/factory/factory.go @@ -17,8 +17,7 @@ import ( const azureAPIBaseURL = "https://management.azure.com" var extensionPorts = map[portainer.ExtensionID]string{ - portainer.RegistryManagementExtension: "7001", - portainer.RBACExtension: "7003", + portainer.RBACExtension: "7003", } type ( diff --git a/api/portainer.go b/api/portainer.go index b87666e17..367061816 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1255,7 +1255,7 @@ const ( const ( _ ExtensionID = iota - // RegistryManagementExtension represents the registry management extension + // RegistryManagementExtension represents the registry management extension (removed) RegistryManagementExtension // OAuthAuthenticationExtension represents the OAuth authentication extension (Deprecated) OAuthAuthenticationExtension diff --git a/app/docker/components/imageRegistry/porImageRegistryController.js b/app/docker/components/imageRegistry/porImageRegistryController.js index f16902c9d..2c2ff5e53 100644 --- a/app/docker/components/imageRegistry/porImageRegistryController.js +++ b/app/docker/components/imageRegistry/porImageRegistryController.js @@ -1,7 +1,7 @@ import angular from 'angular'; import _ from 'lodash-es'; import { DockerHubViewModel } from 'Portainer/models/dockerhub'; -import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; +import { RegistryTypes } from '@/portainer/models/registryTypes'; class porImageRegistryController { /* @ngInject */ diff --git a/app/docker/helpers/imageHelper.js b/app/docker/helpers/imageHelper.js index ea28976de..ff149b939 100644 --- a/app/docker/helpers/imageHelper.js +++ b/app/docker/helpers/imageHelper.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; +import { RegistryTypes } from '@/portainer/models/registryTypes'; angular.module('portainer.docker').factory('ImageHelper', [ function ImageHelperFactory() { diff --git a/app/extensions/_module.js b/app/extensions/_module.js index 47dfa09a3..9afe56617 100644 --- a/app/extensions/_module.js +++ b/app/extensions/_module.js @@ -1 +1 @@ -angular.module('portainer.extensions', ['portainer.extensions.registrymanagement', 'portainer.extensions.rbac']); +angular.module('portainer.extensions', ['portainer.extensions.rbac']); diff --git a/app/extensions/registry-management/_module.js b/app/extensions/registry-management/_module.js deleted file mode 100644 index b471aee44..000000000 --- a/app/extensions/registry-management/_module.js +++ /dev/null @@ -1,55 +0,0 @@ -angular.module('portainer.extensions.registrymanagement', []).config([ - '$stateRegistryProvider', - function ($stateRegistryProvider) { - 'use strict'; - - var registryConfiguration = { - name: 'portainer.registries.registry.configure', - url: '/configure', - views: { - 'content@': { - templateUrl: './views/configure/configureregistry.html', - controller: 'ConfigureRegistryController', - }, - }, - }; - - var registryRepositories = { - name: 'portainer.registries.registry.repositories', - url: '/repositories', - views: { - 'content@': { - templateUrl: './views/repositories/registryRepositories.html', - controller: 'RegistryRepositoriesController', - }, - }, - }; - - var registryRepositoryTags = { - name: 'portainer.registries.registry.repository', - url: '/:repository', - views: { - 'content@': { - templateUrl: './views/repositories/edit/registryRepository.html', - controller: 'RegistryRepositoryController', - }, - }, - }; - var registryRepositoryTag = { - name: 'portainer.registries.registry.repository.tag', - url: '/:tag', - views: { - 'content@': { - templateUrl: './views/repositories/tag/registryRepositoryTag.html', - controller: 'RegistryRepositoryTagController', - controllerAs: 'ctrl', - }, - }, - }; - - $stateRegistryProvider.register(registryConfiguration); - $stateRegistryProvider.register(registryRepositories); - $stateRegistryProvider.register(registryRepositoryTags); - $stateRegistryProvider.register(registryRepositoryTag); - }, -]); diff --git a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html deleted file mode 100644 index 72e632518..000000000 --- a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.html +++ /dev/null @@ -1,76 +0,0 @@ -
    - - -
    -
    {{ $ctrl.titleText }}
    -
    - -
    - - - - - - - - - - - - - - - - - - - -
    - - Repository - - - - - Tags count -
    - {{ item.Name }} - {{ item.TagsCount }}
    Loading...
    No repository available.
    -
    - -
    -
    -
    diff --git a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.js b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.js deleted file mode 100644 index 8dde205ab..000000000 --- a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatable.js +++ /dev/null @@ -1,14 +0,0 @@ -angular.module('portainer.extensions.registrymanagement').component('registryRepositoriesDatatable', { - templateUrl: './registryRepositoriesDatatable.html', - controller: 'RegistryRepositoriesDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - paginationAction: '<', - loading: '<', - }, -}); diff --git a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatableController.js b/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatableController.js deleted file mode 100644 index de4769daa..000000000 --- a/app/extensions/registry-management/components/registries-repositories-datatable/registryRepositoriesDatatableController.js +++ /dev/null @@ -1,41 +0,0 @@ -import _ from 'lodash-es'; - -angular.module('portainer.app').controller('RegistryRepositoriesDatatableController', [ - '$scope', - '$controller', - function ($scope, $controller) { - var ctrl = this; - - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - this.state.orderBy = this.orderBy; - - function areDifferent(a, b) { - if (!a || !b) { - return true; - } - var namesA = a - .map(function (x) { - return x.Name; - }) - .sort(); - var namesB = b - .map(function (x) { - return x.Name; - }) - .sort(); - return namesA.join(',') !== namesB.join(','); - } - - $scope.$watch( - function () { - return ctrl.state.filteredDataSet; - }, - function (newValue, oldValue) { - if (newValue && areDifferent(oldValue, newValue)) { - ctrl.paginationAction(_.filter(newValue, { TagsCount: 0 })); - } - }, - true - ); - }, -]); diff --git a/app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.html b/app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.html deleted file mode 100644 index 5602100bc..000000000 --- a/app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.html +++ /dev/null @@ -1,108 +0,0 @@ -
    - - -
    -
    {{ $ctrl.titleText }}
    -
    -
    - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - Name - - - - Os/ArchitectureImage IDCompressed sizeActions
    - - - - - {{ item.Name }} - {{ item.Os }}/{{ item.Architecture }}{{ item.ImageId | trimshasum }}{{ item.Size | humansize }} - - - Retag - - - - - - - - -
    Loading...
    No tag available.
    -
    - -
    -
    -
    diff --git a/app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.js b/app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.js deleted file mode 100644 index 9ee817654..000000000 --- a/app/extensions/registry-management/components/registries-repository-tags-datatable/registriesRepositoryTagsDatatable.js +++ /dev/null @@ -1,17 +0,0 @@ -angular.module('portainer.extensions.registrymanagement').component('registriesRepositoryTagsDatatable', { - templateUrl: './registriesRepositoryTagsDatatable.html', - controller: 'RegistryRepositoriesTagsDatatableController', - bindings: { - titleText: '@', - titleIcon: '@', - dataset: '<', - tableKey: '@', - orderBy: '@', - reverseOrder: '<', - removeAction: '<', - retagAction: '<', - advancedFeaturesAvailable: '<', - paginationAction: '<', - loading: '<', - }, -}); diff --git a/app/extensions/registry-management/components/registries-repository-tags-datatable/registryRepositoriesTagsDatatableController.js b/app/extensions/registry-management/components/registries-repository-tags-datatable/registryRepositoriesTagsDatatableController.js deleted file mode 100644 index cc21dc902..000000000 --- a/app/extensions/registry-management/components/registries-repository-tags-datatable/registryRepositoriesTagsDatatableController.js +++ /dev/null @@ -1,37 +0,0 @@ -import _ from 'lodash-es'; - -angular.module('portainer.app').controller('RegistryRepositoriesTagsDatatableController', [ - '$scope', - '$controller', - function ($scope, $controller) { - angular.extend(this, $controller('GenericDatatableController', { $scope: $scope })); - var ctrl = this; - this.state.orderBy = this.orderBy; - - function diff(item) { - return item.Name + item.ImageDigest; - } - - function areDifferent(a, b) { - if (!a || !b) { - return true; - } - var namesA = _.sortBy(_.map(a, diff)); - var namesB = _.sortBy(_.map(b, diff)); - return namesA.join(',') !== namesB.join(','); - } - - $scope.$watch( - function () { - return ctrl.state.filteredDataSet; - }, - function (newValue, oldValue) { - if (newValue && newValue.length && areDifferent(oldValue, newValue)) { - ctrl.paginationAction(_.filter(newValue, { ImageId: '' })); - ctrl.resetSelectionState(); - } - }, - true - ); - }, -]); diff --git a/app/extensions/registry-management/helpers/localRegistryHelper.js b/app/extensions/registry-management/helpers/localRegistryHelper.js deleted file mode 100644 index 851f4b62a..000000000 --- a/app/extensions/registry-management/helpers/localRegistryHelper.js +++ /dev/null @@ -1,35 +0,0 @@ -import _ from 'lodash-es'; -import { RepositoryTagViewModel } from '../models/repositoryTag'; - -angular.module('portainer.extensions.registrymanagement').factory('RegistryV2Helper', [ - function RegistryV2HelperFactory() { - 'use strict'; - - var helper = {}; - - function historyRawToParsed(rawHistory) { - return _.map(rawHistory, (item) => angular.fromJson(item.v1Compatibility)); - } - - helper.manifestsToTag = function (manifests) { - var v1 = manifests.v1; - var v2 = manifests.v2; - - var history = historyRawToParsed(v1.history); - var name = v1.tag; - var os = history[0].os; - var arch = v1.architecture; - var size = v2.layers.reduce(function (a, b) { - return { - size: a.size + b.size, - }; - }).size; - var imageId = v2.config.digest; - var imageDigest = v2.digest; - - return new RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2, history); - }; - - return helper; - }, -]); diff --git a/app/extensions/registry-management/models/registryImageDetails.js b/app/extensions/registry-management/models/registryImageDetails.js deleted file mode 100644 index 9a80adedc..000000000 --- a/app/extensions/registry-management/models/registryImageDetails.js +++ /dev/null @@ -1,14 +0,0 @@ -export function RegistryImageDetailsViewModel(data) { - this.Id = data.id; - this.Parent = data.parent; - this.Created = data.created; - this.DockerVersion = data.docker_version; - this.Os = data.os; - this.Architecture = data.architecture; - this.Author = data.author; - this.Command = data.config.Cmd; - this.Entrypoint = data.container_config.Entrypoint ? data.container_config.Entrypoint : ''; - this.ExposedPorts = data.container_config.ExposedPorts ? Object.keys(data.container_config.ExposedPorts) : []; - this.Volumes = data.container_config.Volumes ? Object.keys(data.container_config.Volumes) : []; - this.Env = data.container_config.Env ? data.container_config.Env : []; -} diff --git a/app/extensions/registry-management/models/registryImageLayer.js b/app/extensions/registry-management/models/registryImageLayer.js deleted file mode 100644 index e2ec76f87..000000000 --- a/app/extensions/registry-management/models/registryImageLayer.js +++ /dev/null @@ -1,8 +0,0 @@ -import _ from 'lodash-es'; - -export function RegistryImageLayerViewModel(order, data) { - this.Order = order; - this.Id = data.id; - this.Created = data.created; - this.CreatedBy = _.join(data.container_config.Cmd, ' '); -} diff --git a/app/extensions/registry-management/models/repositoryTag.js b/app/extensions/registry-management/models/repositoryTag.js deleted file mode 100644 index b4682438d..000000000 --- a/app/extensions/registry-management/models/repositoryTag.js +++ /dev/null @@ -1,22 +0,0 @@ -export function RepositoryTagViewModel(name, os, arch, size, imageDigest, imageId, v2, history) { - this.Name = name; - this.Os = os || ''; - this.Architecture = arch || ''; - this.Size = size || 0; - this.ImageDigest = imageDigest || ''; - this.ImageId = imageId || ''; - this.ManifestV2 = v2 || {}; - this.History = history || []; -} - -export function RepositoryShortTag(name, imageId, imageDigest, manifest) { - this.Name = name; - this.ImageId = imageId; - this.ImageDigest = imageDigest; - this.ManifestV2 = manifest; -} - -export function RepositoryAddTagPayload(tag, manifest) { - this.Tag = tag; - this.Manifest = manifest; -} diff --git a/app/extensions/registry-management/rest/catalog.js b/app/extensions/registry-management/rest/catalog.js deleted file mode 100644 index dfe11b044..000000000 --- a/app/extensions/registry-management/rest/catalog.js +++ /dev/null @@ -1,34 +0,0 @@ -import linkGetResponse from './transform/linkGetResponse'; - -angular.module('portainer.extensions.registrymanagement').factory('RegistryCatalog', [ - '$resource', - 'API_ENDPOINT_REGISTRIES', - function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) { - 'use strict'; - return $resource( - API_ENDPOINT_REGISTRIES + '/:id/v2/:action', - {}, - { - get: { - method: 'GET', - params: { id: '@id', action: '_catalog' }, - transformResponse: linkGetResponse, - }, - ping: { - method: 'GET', - params: { id: '@id' }, - timeout: 3500, - }, - pingWithForceNew: { - method: 'GET', - params: { id: '@id' }, - timeout: 3500, - headers: { 'X-RegistryManagement-ForceNew': '1' }, - }, - }, - { - stripTrailingSlashes: false, - } - ); - }, -]); diff --git a/app/extensions/registry-management/rest/manifestJquery.js b/app/extensions/registry-management/rest/manifestJquery.js deleted file mode 100644 index a0f4fb082..000000000 --- a/app/extensions/registry-management/rest/manifestJquery.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * This service has been created to request the docker registry API - * without triggering AngularJS digest cycles - * For more information, see https://github.com/portainer/portainer/pull/2648#issuecomment-505644913 - */ - -import $ from 'jquery'; - -angular.module('portainer.extensions.registrymanagement').factory('RegistryManifestsJquery', [ - 'API_ENDPOINT_REGISTRIES', - function RegistryManifestsJqueryFactory(API_ENDPOINT_REGISTRIES) { - 'use strict'; - - function buildUrl(params) { - return API_ENDPOINT_REGISTRIES + '/' + params.id + '/v2/' + params.repository + '/manifests/' + params.tag; - } - - function _get(params) { - return new Promise((resolve, reject) => { - $.ajax({ - type: 'GET', - dataType: 'JSON', - url: buildUrl(params), - headers: { - 'Cache-Control': 'no-cache', - 'If-Modified-Since': 'Mon, 26 Jul 1997 05:00:00 GMT', - }, - success: (result) => resolve(result), - error: (error) => reject(error), - }); - }); - } - - function _getV2(params) { - return new Promise((resolve, reject) => { - $.ajax({ - type: 'GET', - dataType: 'JSON', - url: buildUrl(params), - headers: { - Accept: 'application/vnd.docker.distribution.manifest.v2+json', - 'Cache-Control': 'no-cache', - 'If-Modified-Since': 'Mon, 26 Jul 1997 05:00:00 GMT', - }, - success: (result, status, request) => { - result.digest = request.getResponseHeader('Docker-Content-Digest'); - resolve(result); - }, - error: (error) => reject(error), - }); - }); - } - - function _put(params, data) { - const transformRequest = (d) => { - return angular.toJson(d, 3); - }; - return new Promise((resolve, reject) => { - $.ajax({ - type: 'PUT', - url: buildUrl(params), - headers: { - 'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json', - }, - data: transformRequest(data), - success: (result) => resolve(result), - error: (error) => reject(error), - }); - }); - } - - function _delete(params) { - return new Promise((resolve, reject) => { - $.ajax({ - type: 'DELETE', - url: buildUrl(params), - success: (result) => resolve(result), - error: (error) => reject(error), - }); - }); - } - - return { - get: _get, - getV2: _getV2, - put: _put, - delete: _delete, - }; - }, -]); diff --git a/app/extensions/registry-management/rest/tags.js b/app/extensions/registry-management/rest/tags.js deleted file mode 100644 index 6087d4eb5..000000000 --- a/app/extensions/registry-management/rest/tags.js +++ /dev/null @@ -1,20 +0,0 @@ -import linkGetResponse from './transform/linkGetResponse'; - -angular.module('portainer.extensions.registrymanagement').factory('RegistryTags', [ - '$resource', - 'API_ENDPOINT_REGISTRIES', - function RegistryTagsFactory($resource, API_ENDPOINT_REGISTRIES) { - 'use strict'; - return $resource( - API_ENDPOINT_REGISTRIES + '/:id/v2/:repository/tags/list', - {}, - { - get: { - method: 'GET', - params: { id: '@id', repository: '@repository' }, - transformResponse: linkGetResponse, - }, - } - ); - }, -]); diff --git a/app/extensions/registry-management/rest/transform/linkGetResponse.js b/app/extensions/registry-management/rest/transform/linkGetResponse.js deleted file mode 100644 index 6362c7a0f..000000000 --- a/app/extensions/registry-management/rest/transform/linkGetResponse.js +++ /dev/null @@ -1,13 +0,0 @@ -export default function linkGetResponse(data, headers) { - var response = angular.fromJson(data); - var link = headers('link'); - if (link) { - var queryString = link.substring(link.indexOf('?') + 1).split('>;')[0]; - var queries = queryString.split('&'); - for (var i = 0; i < queries.length; i++) { - var kv = queries[i].split('='); - response[kv[0]] = kv[1]; - } - } - return response; -} diff --git a/app/extensions/registry-management/services/genericAsyncGenerator.js b/app/extensions/registry-management/services/genericAsyncGenerator.js deleted file mode 100644 index 49978b41c..000000000 --- a/app/extensions/registry-management/services/genericAsyncGenerator.js +++ /dev/null @@ -1,34 +0,0 @@ -import _ from 'lodash-es'; - -function findBestStep(length) { - let step = Math.trunc(length / 10); - if (step < 10) { - step = 10; - } else if (step > 100) { - step = 100; - } - return step; -} - -export default async function* genericAsyncGenerator($q, list, func, params) { - const step = findBestStep(list.length); - let start = 0; - let end = start + step; - let results = []; - while (start < list.length) { - const batch = _.slice(list, start, end); - const promises = []; - for (let i = 0; i < batch.length; i++) { - promises.push(func(...params, batch[i])); - } - yield start; - const res = await $q.all(promises); - for (let i = 0; i < res.length; i++) { - results.push(res[i]); - } - start = end; - end += step; - } - yield list.length; - yield results; -} diff --git a/app/extensions/registry-management/services/registryServiceSelector.js b/app/extensions/registry-management/services/registryServiceSelector.js deleted file mode 100644 index f51d89e7e..000000000 --- a/app/extensions/registry-management/services/registryServiceSelector.js +++ /dev/null @@ -1,84 +0,0 @@ -import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; - -angular.module('portainer.extensions.registrymanagement').factory('RegistryServiceSelector', [ - '$q', - 'RegistryV2Service', - 'RegistryGitlabService', - function RegistryServiceSelector($q, RegistryV2Service, RegistryGitlabService) { - 'use strict'; - const service = {}; - - service.ping = ping; - service.repositories = repositories; - service.getRepositoriesDetails = getRepositoriesDetails; - service.tags = tags; - service.getTagsDetails = getTagsDetails; - service.tag = tag; - service.addTag = addTag; - service.deleteManifest = deleteManifest; - - service.shortTagsWithProgress = shortTagsWithProgress; - service.deleteTagsWithProgress = deleteTagsWithProgress; - service.retagWithProgress = retagWithProgress; - - function ping(registry, forceNewConfig) { - let service = RegistryV2Service; - return service.ping(registry, forceNewConfig); - } - - function repositories(registry) { - let service = RegistryV2Service; - if (registry.Type === RegistryTypes.GITLAB) { - service = RegistryGitlabService; - } - return service.repositories(registry); - } - - function getRepositoriesDetails(registry, repositories) { - let service = RegistryV2Service; - return service.getRepositoriesDetails(registry, repositories); - } - - function tags(registry, repository) { - let service = RegistryV2Service; - return service.tags(registry, repository); - } - - function getTagsDetails(registry, repository, tags) { - let service = RegistryV2Service; - return service.getTagsDetails(registry, repository, tags); - } - - function tag(registry, repository, tag) { - let service = RegistryV2Service; - return service.tag(registry, repository, tag); - } - - function addTag(registry, repository, tag, manifest) { - let service = RegistryV2Service; - return service.addTag(registry, repository, tag, manifest); - } - - function deleteManifest(registry, repository, digest) { - let service = RegistryV2Service; - return service.deleteManifest(registry, repository, digest); - } - - function shortTagsWithProgress(registry, repository, tagsList) { - let service = RegistryV2Service; - return service.shortTagsWithProgress(registry, repository, tagsList); - } - - function deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) { - let service = RegistryV2Service; - return service.deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags); - } - - function retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags) { - let service = RegistryV2Service; - return service.retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags); - } - - return service; - }, -]); diff --git a/app/extensions/registry-management/services/registryV2Service.js b/app/extensions/registry-management/services/registryV2Service.js deleted file mode 100644 index 242ca364b..000000000 --- a/app/extensions/registry-management/services/registryV2Service.js +++ /dev/null @@ -1,310 +0,0 @@ -import _ from 'lodash-es'; -import { RepositoryAddTagPayload, RepositoryShortTag } from '../models/repositoryTag'; -import { RegistryRepositoryViewModel } from '../models/registryRepository'; -import genericAsyncGenerator from './genericAsyncGenerator'; - -angular.module('portainer.extensions.registrymanagement').factory('RegistryV2Service', [ - '$q', - '$async', - 'RegistryCatalog', - 'RegistryTags', - 'RegistryManifestsJquery', - 'RegistryV2Helper', - function RegistryV2ServiceFactory($q, $async, RegistryCatalog, RegistryTags, RegistryManifestsJquery, RegistryV2Helper) { - 'use strict'; - var service = {}; - - /** - * PING - */ - function ping(registry, forceNewConfig) { - const id = registry.Id; - if (forceNewConfig) { - return RegistryCatalog.pingWithForceNew({ id: id }).$promise; - } - return RegistryCatalog.ping({ id: id }).$promise; - } - - /** - * END PING - */ - - /** - * REPOSITORIES - */ - - function _getCatalogPage(params, deferred, repositories) { - RegistryCatalog.get(params).$promise.then(function (data) { - repositories = _.concat(repositories, data.repositories); - if (data.last && data.n) { - _getCatalogPage({ id: params.id, n: data.n, last: data.last }, deferred, repositories); - } else { - deferred.resolve(repositories); - } - }); - } - - function _getCatalog(id) { - var deferred = $q.defer(); - var repositories = []; - - _getCatalogPage({ id: id }, deferred, repositories); - return deferred.promise; - } - - function repositories(registry) { - const deferred = $q.defer(); - const id = registry.Id; - - _getCatalog(id) - .then(function success(data) { - const repositories = _.map(data, (repositoryName) => new RegistryRepositoryViewModel(repositoryName)); - deferred.resolve(repositories); - }) - .catch(function error(err) { - deferred.reject({ - msg: 'Unable to retrieve repositories', - err: err, - }); - }); - - return deferred.promise; - } - - function getRepositoriesDetails(registry, repositories) { - const deferred = $q.defer(); - const promises = _.map(repositories, (repository) => tags(registry, repository.Name)); - - $q.all(promises) - .then(function success(data) { - var repositories = data.map(function (item) { - return new RegistryRepositoryViewModel(item); - }); - repositories = _.without(repositories, undefined); - deferred.resolve(repositories); - }) - .catch(function error(err) { - deferred.reject({ - msg: 'Unable to retrieve repositories', - err: err, - }); - }); - - return deferred.promise; - } - - /** - * END REPOSITORIES - */ - - /** - * TAGS - */ - - function _getTagsPage(params, deferred, previousTags) { - RegistryTags.get(params) - .$promise.then(function (data) { - previousTags.name = data.name; - previousTags.tags = _.concat(previousTags.tags, data.tags); - if (data.last && data.n) { - _getTagsPage({ id: params.id, repository: params.repository, n: data.n, last: data.last }, deferred, previousTags); - } else { - deferred.resolve(previousTags); - } - }) - .catch(function error(err) { - deferred.reject({ - msg: 'Unable to retrieve tags', - err: err, - }); - }); - } - - function tags(registry, repository) { - const deferred = $q.defer(); - const id = registry.Id; - - _getTagsPage({ id: id, repository: repository }, deferred, { tags: [] }); - return deferred.promise; - } - - function getTagsDetails(registry, repository, tags) { - const promises = _.map(tags, (t) => tag(registry, repository, t.Name)); - - return $q.all(promises); - } - - function tag(registry, repository, tag) { - const deferred = $q.defer(); - const id = registry.Id; - - var promises = { - v1: RegistryManifestsJquery.get({ - id: id, - repository: repository, - tag: tag, - }), - v2: RegistryManifestsJquery.getV2({ - id: id, - repository: repository, - tag: tag, - }), - }; - $q.all(promises) - .then(function success(data) { - var tag = RegistryV2Helper.manifestsToTag(data); - deferred.resolve(tag); - }) - .catch(function error(err) { - deferred.reject({ - msg: 'Unable to retrieve tag ' + tag, - err: err, - }); - }); - - return deferred.promise; - } - - /** - * END TAGS - */ - - /** - * ADD TAG - */ - - // tag: RepositoryAddTagPayload - function _addTagFromGenerator(registry, repository, tag) { - return addTag(registry, repository, tag.Tag, tag.Manifest); - } - - function addTag(registry, repository, tag, manifest) { - const id = registry.Id; - delete manifest.digest; - return RegistryManifestsJquery.put( - { - id: id, - repository: repository, - tag: tag, - }, - manifest - ); - } - - async function* _addTagsWithProgress(registry, repository, tagsList, progression = 0) { - for await (const partialResult of genericAsyncGenerator($q, tagsList, _addTagFromGenerator, [registry, repository])) { - if (typeof partialResult === 'number') { - yield progression + partialResult; - } else { - yield partialResult; - } - } - } - - /** - * END ADD TAG - */ - - /** - * DELETE MANIFEST - */ - - function deleteManifest(registry, repository, imageDigest) { - const id = registry.Id; - return RegistryManifestsJquery.delete({ - id: id, - repository: repository, - tag: imageDigest, - }); - } - - async function* _deleteManifestsWithProgress(registry, repository, manifests) { - for await (const partialResult of genericAsyncGenerator($q, manifests, deleteManifest, [registry, repository])) { - yield partialResult; - } - } - - /** - * END DELETE MANIFEST - */ - - /** - * SHORT TAG - */ - - function _shortTagFromGenerator(id, repository, tag) { - return new Promise((resolve, reject) => { - RegistryManifestsJquery.getV2({ id: id, repository: repository, tag: tag }) - .then((data) => resolve(new RepositoryShortTag(tag, data.config.digest, data.digest, data))) - .catch((err) => reject(err)); - }); - } - - async function* shortTagsWithProgress(registry, repository, tagsList) { - const id = registry.Id; - yield* genericAsyncGenerator($q, tagsList, _shortTagFromGenerator, [id, repository]); - } - - /** - * END SHORT TAG - */ - - /** - * RETAG - */ - async function* retagWithProgress(registry, repository, modifiedTags, modifiedDigests, impactedTags) { - yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests); - - const newTags = _.map(impactedTags, (item) => { - const tagFromTable = _.find(modifiedTags, { Name: item.Name }); - const name = tagFromTable && tagFromTable.Name !== tagFromTable.NewName ? tagFromTable.NewName : item.Name; - return new RepositoryAddTagPayload(name, item.ManifestV2); - }); - - yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length); - } - - /** - * END RETAG - */ - - /** - * DELETE TAGS - */ - - async function* deleteTagsWithProgress(registry, repository, modifiedDigests, impactedTags) { - yield* _deleteManifestsWithProgress(registry, repository, modifiedDigests); - - const newTags = _.map(impactedTags, (item) => new RepositoryAddTagPayload(item.Name, item.ManifestV2)); - - yield* _addTagsWithProgress(registry, repository, newTags, modifiedDigests.length); - } - - /** - * END DELETE TAGS - */ - - /** - * SERVICE FUNCTIONS DECLARATION - */ - - service.ping = ping; - - service.repositories = repositories; - service.getRepositoriesDetails = getRepositoriesDetails; - - service.tags = tags; - service.tag = tag; - service.getTagsDetails = getTagsDetails; - - service.shortTagsWithProgress = shortTagsWithProgress; - - service.addTag = addTag; - service.deleteManifest = deleteManifest; - - service.deleteTagsWithProgress = deleteTagsWithProgress; - service.retagWithProgress = retagWithProgress; - - return service; - }, -]); diff --git a/app/extensions/registry-management/views/configure/configureRegistryController.js b/app/extensions/registry-management/views/configure/configureRegistryController.js deleted file mode 100644 index 588f26876..000000000 --- a/app/extensions/registry-management/views/configure/configureRegistryController.js +++ /dev/null @@ -1,75 +0,0 @@ -import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; -import { RegistryManagementConfigurationDefaultModel } from '../../../../portainer/models/registry'; - -angular.module('portainer.extensions.registrymanagement').controller('ConfigureRegistryController', [ - '$scope', - '$state', - '$transition$', - 'RegistryService', - 'RegistryServiceSelector', - 'Notifications', - function ($scope, $state, $transition$, RegistryService, RegistryServiceSelector, Notifications) { - $scope.state = { - testInProgress: false, - updateInProgress: false, - validConfiguration: false, - }; - - $scope.testConfiguration = testConfiguration; - $scope.updateConfiguration = updateConfiguration; - - function testConfiguration() { - $scope.state.testInProgress = true; - - RegistryService.configureRegistry($scope.registry.Id, $scope.model) - .then(function success() { - return RegistryServiceSelector.ping($scope.registry, true); - }) - .then(function success() { - Notifications.success('Success', 'Valid management configuration'); - $scope.state.validConfiguration = true; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Invalid management configuration'); - }) - .finally(function final() { - $scope.state.testInProgress = false; - }); - } - - function updateConfiguration() { - $scope.state.updateInProgress = true; - - RegistryService.configureRegistry($scope.registry.Id, $scope.model) - .then(function success() { - Notifications.success('Success', 'Registry management configuration updated'); - $state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, { reload: true }); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to update registry management configuration'); - }) - .finally(function final() { - $scope.state.updateInProgress = false; - }); - } - - function initView() { - var registryId = $transition$.params().id; - $scope.RegistryTypes = RegistryTypes; - - RegistryService.registry(registryId) - .then(function success(data) { - var registry = data; - var model = new RegistryManagementConfigurationDefaultModel(registry); - - $scope.registry = registry; - $scope.model = model; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve registry details'); - }); - } - - initView(); - }, -]); diff --git a/app/extensions/registry-management/views/configure/configureregistry.html b/app/extensions/registry-management/views/configure/configureregistry.html deleted file mode 100644 index d515830c0..000000000 --- a/app/extensions/registry-management/views/configure/configureregistry.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - Registries > {{ registry.Name }} > Management configuration - - - -
    -
    - - -
    -
    - Information -
    -
    - - The following configuration will be used to access this registry API to provide Portainer - management features. - -
    -
    - Registry details -
    - -
    - -
    - -
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    - -
    - -
    -
    - - -
    - -
    - -
    -
    - -
    - - -
    - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    - Required TLS files -
    - -
    - -
    - -
    - - - {{ model.TLSCACertFile.name }} - - - -
    -
    - - -
    - -
    - - - {{ model.TLSCertFile.name }} - - - -
    -
    - - -
    - -
    - - - {{ model.TLSKeyFile.name }} - - - -
    -
    - -
    -
    - -
    - Actions -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    diff --git a/app/extensions/registry-management/views/repositories/edit/progression-modal/progressionModal.html b/app/extensions/registry-management/views/repositories/edit/progression-modal/progressionModal.html deleted file mode 100644 index 7bb5b7c2d..000000000 --- a/app/extensions/registry-management/views/repositories/edit/progression-modal/progressionModal.html +++ /dev/null @@ -1,11 +0,0 @@ - - - -

    - - {{ $ctrl.resolve.message }} -

    -
    -   {{ $ctrl.resolve.progressLabel }} : {{ $ctrl.resolve.context.progression }}% - {{ $ctrl.resolve.context.elapsedTime | number: 0 }}s -
    -
    diff --git a/app/extensions/registry-management/views/repositories/edit/progression-modal/progressionModal.js b/app/extensions/registry-management/views/repositories/edit/progression-modal/progressionModal.js deleted file mode 100644 index bc4df86fb..000000000 --- a/app/extensions/registry-management/views/repositories/edit/progression-modal/progressionModal.js +++ /dev/null @@ -1,6 +0,0 @@ -angular.module('portainer.extensions.registrymanagement').component('progressionModal', { - templateUrl: './progressionModal.html', - bindings: { - resolve: '<', - }, -}); diff --git a/app/extensions/registry-management/views/repositories/edit/registryRepository.html b/app/extensions/registry-management/views/repositories/edit/registryRepository.html deleted file mode 100644 index 23ae7a37f..000000000 --- a/app/extensions/registry-management/views/repositories/edit/registryRepository.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - Registries > {{ registry.Name }} > - {{ repository.Name }} - - - -
    - - -

    - - Portainer needs to retrieve additional information to enable tags modifications (addition, removal, rename) and repository removal features.
    - As this repository contains more than {{ state.tagsRetrieval.limit }} tags, the additional retrieval wasn't started automatically.
    - Once started you can still navigate this page, leaving the page will cancel the retrieval process.
    -
    - Note: on very large repositories or high latency environments the retrieval process can take a few minutes. -

    - - -
    - -   Retrieval progress : {{ state.tagsRetrieval.progression }}% - {{ state.tagsRetrieval.elapsedTime | number: 0 }}s - - - Retrieval completed in {{ state.tagsRetrieval.elapsedTime | number: 0 }}s - -
    -
    - -
    -
    - - - - - - - - - - - - - - - - - - - - - -
    Repository - {{ repository.Name }} - - -
    Tags count{{ repository.Tags.length }}
    Images count{{ short.Images.length }}
    -
    -
    -
    - -
    - - - -
    -
    - -
    - -
    -
    -
    - - - - {{ $select.selected | trimshasum }} - - - {{ image | trimshasum }} - - -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - -
    -
    diff --git a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js b/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js deleted file mode 100644 index e8ab74f09..000000000 --- a/app/extensions/registry-management/views/repositories/edit/registryRepositoryController.js +++ /dev/null @@ -1,423 +0,0 @@ -import _ from 'lodash-es'; -import { RepositoryShortTag, RepositoryTagViewModel } from '../../../models/repositoryTag'; - -angular.module('portainer.app').controller('RegistryRepositoryController', [ - '$q', - '$async', - '$scope', - '$uibModal', - '$interval', - '$transition$', - '$state', - 'RegistryServiceSelector', - 'RegistryService', - 'ModalService', - 'Notifications', - 'ImageHelper', - function ($q, $async, $scope, $uibModal, $interval, $transition$, $state, RegistryServiceSelector, RegistryService, ModalService, Notifications, ImageHelper) { - $scope.state = { - actionInProgress: false, - loading: false, - tagsRetrieval: { - auto: true, - running: false, - limit: 100, - progression: 0, - elapsedTime: 0, - asyncGenerator: null, - clock: null, - }, - tagsRetag: { - running: false, - progression: 0, - elapsedTime: 0, - asyncGenerator: null, - clock: null, - }, - tagsDelete: { - running: false, - progression: 0, - elapsedTime: 0, - asyncGenerator: null, - clock: null, - }, - }; - $scope.formValues = { - Tag: '', // new tag name on add feature - }; - $scope.tags = []; // RepositoryTagViewModel (for datatable) - $scope.short = { - Tags: [], // RepositoryShortTag - Images: [], // strings extracted from short.Tags - }; - $scope.repository = { - Name: '', - Tags: [], // string list - }; - - function toSeconds(time) { - return time / 1000; - } - function toPercent(progress, total) { - return ((progress / total) * 100).toFixed(); - } - - function openModal(resolve) { - return $uibModal.open({ - component: 'progressionModal', - backdrop: 'static', - keyboard: false, - resolve: resolve, - }); - } - - $scope.paginationAction = function (tags) { - $scope.state.loading = true; - RegistryServiceSelector.getTagsDetails($scope.registry, $scope.repository.Name, tags) - .then(function success(data) { - for (var i = 0; i < data.length; i++) { - var idx = _.findIndex($scope.tags, { Name: data[i].Name }); - if (idx !== -1) { - $scope.tags[idx] = data[i]; - } - } - $scope.state.loading = false; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve tags details'); - }); - }; - - /** - * RETRIEVAL SECTION - */ - function updateRetrievalClock(startTime) { - $scope.state.tagsRetrieval.elapsedTime = toSeconds(Date.now() - startTime); - } - - function createRetrieveAsyncGenerator() { - $scope.state.tagsRetrieval.asyncGenerator = RegistryServiceSelector.shortTagsWithProgress($scope.registry, $scope.repository.Name, $scope.repository.Tags); - } - - function resetTagsRetrievalState() { - $scope.state.tagsRetrieval.running = false; - $scope.state.tagsRetrieval.progression = 0; - $scope.state.tagsRetrieval.elapsedTime = 0; - $scope.state.tagsRetrieval.clock = null; - } - - function computeImages() { - const images = _.map($scope.short.Tags, 'ImageId'); - $scope.short.Images = _.without(_.uniq(images), ''); - } - - $scope.startStopRetrieval = function () { - if ($scope.state.tagsRetrieval.running) { - $scope.state.tagsRetrieval.asyncGenerator.return(); - $interval.cancel($scope.state.tagsRetrieval.clock); - } else { - retrieveTags().then(() => { - createRetrieveAsyncGenerator(); - if ($scope.short.Tags.length === 0) { - resetTagsRetrievalState(); - } else { - computeImages(); - } - }); - } - }; - - function retrieveTags() { - return $async(retrieveTagsAsync); - } - - async function retrieveTagsAsync() { - $scope.state.tagsRetrieval.running = true; - const startTime = Date.now(); - $scope.state.tagsRetrieval.clock = $interval(updateRetrievalClock, 1000, 0, true, startTime); - for await (const partialResult of $scope.state.tagsRetrieval.asyncGenerator) { - if (typeof partialResult === 'number') { - $scope.state.tagsRetrieval.progression = toPercent(partialResult, $scope.repository.Tags.length); - } else { - $scope.short.Tags = _.sortBy(partialResult, 'Name'); - } - } - $scope.state.tagsRetrieval.running = false; - $interval.cancel($scope.state.tagsRetrieval.clock); - } - /** - * !END RETRIEVAL SECTION - */ - - /** - * ADD TAG SECTION - */ - - async function addTagAsync() { - try { - $scope.state.actionInProgress = true; - if (!ImageHelper.isValidTag($scope.formValues.Tag)) { - throw { msg: 'Invalid tag pattern, see info for more details on format.' }; - } - const tag = $scope.short.Tags.find((item) => item.ImageId === $scope.formValues.SelectedImage); - const manifest = tag.ManifestV2; - await RegistryServiceSelector.addTag($scope.registry, $scope.repository.Name, $scope.formValues.Tag, manifest); - - Notifications.success('Success', 'Tag successfully added'); - $scope.short.Tags.push(new RepositoryShortTag($scope.formValues.Tag, tag.ImageId, tag.ImageDigest, tag.ManifestV2)); - - await loadRepositoryDetails(); - $scope.formValues.Tag = ''; - delete $scope.formValues.SelectedImage; - } catch (err) { - Notifications.error('Failure', err, 'Unable to add tag'); - } finally { - $scope.state.actionInProgress = false; - } - } - - $scope.addTag = function () { - return $async(addTagAsync); - }; - /** - * !END ADD TAG SECTION - */ - - /** - * RETAG SECTION - */ - function updateRetagClock(startTime) { - $scope.state.tagsRetag.elapsedTime = toSeconds(Date.now() - startTime); - } - - function createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags) { - $scope.state.tagsRetag.asyncGenerator = RegistryServiceSelector.retagWithProgress($scope.registry, $scope.repository.Name, modifiedTags, modifiedDigests, impactedTags); - } - - async function retagActionAsync() { - let modal = null; - try { - $scope.state.tagsRetag.running = true; - - const modifiedTags = _.filter($scope.tags, (item) => item.Modified === true); - for (const tag of modifiedTags) { - if (!ImageHelper.isValidTag(tag.NewName)) { - throw { msg: 'Invalid tag pattern, see info for more details on format.' }; - } - } - modal = await openModal({ - message: () => 'Retag is in progress! Closing your browser or refreshing the page while this operation is in progress will result in loss of tags.', - progressLabel: () => 'Retag progress', - context: () => $scope.state.tagsRetag, - }); - const modifiedDigests = _.uniq(_.map(modifiedTags, 'ImageDigest')); - const impactedTags = _.filter($scope.short.Tags, (item) => _.includes(modifiedDigests, item.ImageDigest)); - - const totalOps = modifiedDigests.length + impactedTags.length; - - createRetagAsyncGenerator(modifiedTags, modifiedDigests, impactedTags); - - const startTime = Date.now(); - $scope.state.tagsRetag.clock = $interval(updateRetagClock, 1000, 0, true, startTime); - for await (const partialResult of $scope.state.tagsRetag.asyncGenerator) { - if (typeof partialResult === 'number') { - $scope.state.tagsRetag.progression = toPercent(partialResult, totalOps); - } - } - - _.map(modifiedTags, (item) => { - const idx = _.findIndex($scope.short.Tags, (i) => i.Name === item.Name); - $scope.short.Tags[idx].Name = item.NewName; - }); - - Notifications.success('Success', 'Tags successfully renamed'); - - await loadRepositoryDetails(); - } catch (err) { - Notifications.error('Failure', err, 'Unable to rename tags'); - } finally { - $interval.cancel($scope.state.tagsRetag.clock); - $scope.state.tagsRetag.running = false; - if (modal) { - modal.close(); - } - } - } - - $scope.retagAction = function () { - return $async(retagActionAsync); - }; - /** - * !END RETAG SECTION - */ - - /** - * REMOVE TAGS SECTION - */ - - function updateDeleteClock(startTime) { - $scope.state.tagsDelete.elapsedTime = toSeconds(Date.now() - startTime); - } - - function createDeleteAsyncGenerator(modifiedDigests, impactedTags) { - $scope.state.tagsDelete.asyncGenerator = RegistryServiceSelector.deleteTagsWithProgress($scope.registry, $scope.repository.Name, modifiedDigests, impactedTags); - } - - async function removeTagsAsync(selectedTags) { - let modal = null; - try { - $scope.state.tagsDelete.running = true; - modal = await openModal({ - message: () => 'Tag delete is in progress! Closing your browser or refreshing the page while this operation is in progress will result in loss of tags.', - progressLabel: () => 'Deletion progress', - context: () => $scope.state.tagsDelete, - }); - - const deletedTagNames = _.map(selectedTags, 'Name'); - const deletedShortTags = _.filter($scope.short.Tags, (item) => _.includes(deletedTagNames, item.Name)); - const modifiedDigests = _.uniq(_.map(deletedShortTags, 'ImageDigest')); - const impactedTags = _.filter($scope.short.Tags, (item) => _.includes(modifiedDigests, item.ImageDigest)); - const tagsToKeep = _.without(impactedTags, ...deletedShortTags); - - const totalOps = modifiedDigests.length + tagsToKeep.length; - - createDeleteAsyncGenerator(modifiedDigests, tagsToKeep); - - const startTime = Date.now(); - $scope.state.tagsDelete.clock = $interval(updateDeleteClock, 1000, 0, true, startTime); - for await (const partialResult of $scope.state.tagsDelete.asyncGenerator) { - if (typeof partialResult === 'number') { - $scope.state.tagsDelete.progression = toPercent(partialResult, totalOps); - } - } - - _.pull($scope.short.Tags, ...deletedShortTags); - $scope.short.Images = _.map(_.uniqBy($scope.short.Tags, 'ImageId'), 'ImageId'); - - Notifications.success('Success', 'Tags successfully deleted'); - - if ($scope.short.Tags.length === 0) { - $state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, { reload: true }); - } - await loadRepositoryDetails(); - } catch (err) { - Notifications.error('Failure', err, 'Unable to delete tags'); - } finally { - $interval.cancel($scope.state.tagsDelete.clock); - $scope.state.tagsDelete.running = false; - modal.close(); - } - } - - $scope.removeTags = function (selectedItems) { - ModalService.confirmDeletion('Are you sure you want to remove the selected tags ?', (confirmed) => { - if (!confirmed) { - return; - } - return $async(removeTagsAsync, selectedItems); - }); - }; - /** - * !END REMOVE TAGS SECTION - */ - - /** - * REMOVE REPOSITORY SECTION - */ - async function removeRepositoryAsync() { - try { - const digests = _.uniqBy($scope.short.Tags, 'ImageDigest'); - const promises = []; - _.map(digests, (item) => promises.push(RegistryServiceSelector.deleteManifest($scope.registry, $scope.repository.Name, item.ImageDigest))); - await Promise.all(promises); - Notifications.success('Success', 'Repository sucessfully removed'); - $state.go('portainer.registries.registry.repositories', { id: $scope.registry.Id }, { reload: true }); - } catch (err) { - Notifications.error('Failure', err, 'Unable to delete repository'); - } - } - - $scope.removeRepository = function () { - ModalService.confirmDeletion( - 'This action will only remove the manifests linked to this repository. You need to manually trigger a garbage collector pass on your registry to remove orphan layers and really remove the images content. THIS ACTION CAN NOT BE UNDONE', - function onConfirm(confirmed) { - if (!confirmed) { - return; - } - return $async(removeRepositoryAsync); - } - ); - }; - /** - * !END REMOVE REPOSITORY SECTION - */ - - /** - * INIT SECTION - */ - async function loadRepositoryDetails() { - try { - const registry = $scope.registry; - const repository = $scope.repository.Name; - const tags = await RegistryServiceSelector.tags(registry, repository); - $scope.tags = []; - $scope.repository.Tags = []; - $scope.repository.Tags = _.sortBy(_.concat($scope.repository.Tags, _.without(tags.tags, null))); - _.map($scope.repository.Tags, (item) => $scope.tags.push(new RepositoryTagViewModel(item))); - } catch (err) { - Notifications.error('Failure', err, 'Unable to retrieve tags details'); - } - } - - async function initView() { - try { - const registryId = $transition$.params().id; - $scope.repository.Name = $transition$.params().repository; - $scope.state.loading = true; - - $scope.registry = await RegistryService.registry(registryId); - await loadRepositoryDetails(); - if ($scope.repository.Tags.length > $scope.state.tagsRetrieval.limit) { - $scope.state.tagsRetrieval.auto = false; - } - createRetrieveAsyncGenerator(); - } catch (err) { - Notifications.error('Failure', err, 'Unable to retrieve repository information'); - } finally { - $scope.state.loading = false; - } - } - - $scope.$on('$destroy', () => { - if ($scope.state.tagsRetrieval.asyncGenerator) { - $scope.state.tagsRetrieval.asyncGenerator.return(); - } - if ($scope.state.tagsRetrieval.clock) { - $interval.cancel($scope.state.tagsRetrieval.clock); - } - if ($scope.state.tagsRetag.asyncGenerator) { - $scope.state.tagsRetag.asyncGenerator.return(); - } - if ($scope.state.tagsRetag.clock) { - $interval.cancel($scope.state.tagsRetag.clock); - } - if ($scope.state.tagsDelete.asyncGenerator) { - $scope.state.tagsDelete.asyncGenerator.return(); - } - if ($scope.state.tagsDelete.clock) { - $interval.cancel($scope.state.tagsDelete.clock); - } - }); - - this.$onInit = function () { - return $async(initView).then(() => { - if ($scope.state.tagsRetrieval.auto) { - $scope.startStopRetrieval(); - } - }); - }; - /** - * !END INIT SECTION - */ - }, -]); diff --git a/app/extensions/registry-management/views/repositories/registryRepositories.html b/app/extensions/registry-management/views/repositories/registryRepositories.html deleted file mode 100644 index 191089dad..000000000 --- a/app/extensions/registry-management/views/repositories/registryRepositories.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - Registries > - {{ registry.Name }}{{ registry.Name }} > Repositories - - - -
    - - -

    - - Portainer was not able to use this registry management features. You might need to update the configuration used by Portainer to access this registry. -

    -

    Note: Portainer registry management features are only supported with registries exposing the - v2 registry API.

    - -
    -
    -
    - -
    -
    - - -
    -
    diff --git a/app/extensions/registry-management/views/repositories/registryRepositoriesController.js b/app/extensions/registry-management/views/repositories/registryRepositoriesController.js deleted file mode 100644 index 00ad8b0cb..000000000 --- a/app/extensions/registry-management/views/repositories/registryRepositoriesController.js +++ /dev/null @@ -1,68 +0,0 @@ -import _ from 'lodash-es'; - -import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; - -angular.module('portainer.extensions.registrymanagement').controller('RegistryRepositoriesController', [ - '$transition$', - '$scope', - 'RegistryService', - 'RegistryServiceSelector', - 'Notifications', - 'Authentication', - function ($transition$, $scope, RegistryService, RegistryServiceSelector, Notifications, Authentication) { - $scope.state = { - displayInvalidConfigurationMessage: false, - loading: false, - }; - - $scope.paginationAction = function (repositories) { - if ($scope.registry.Type === RegistryTypes.GITLAB) { - return; - } - $scope.state.loading = true; - RegistryServiceSelector.getRepositoriesDetails($scope.registry, repositories) - .then(function success(data) { - for (var i = 0; i < data.length; i++) { - var idx = _.findIndex($scope.repositories, { Name: data[i].Name }); - if (idx !== -1) { - if (data[i].TagsCount === 0) { - $scope.repositories.splice(idx, 1); - } else { - $scope.repositories[idx].TagsCount = data[i].TagsCount; - } - } - } - $scope.state.loading = false; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve repositories details'); - }); - }; - - function initView() { - const registryId = $transition$.params().id; - - $scope.isAdmin = Authentication.isAdmin(); - - RegistryService.registry(registryId) - .then(function success(data) { - $scope.registry = data; - RegistryServiceSelector.ping($scope.registry, false) - .then(function success() { - return RegistryServiceSelector.repositories($scope.registry); - }) - .then(function success(data) { - $scope.repositories = data; - }) - .catch(function error() { - $scope.state.displayInvalidConfigurationMessage = true; - }); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve registry details'); - }); - } - - initView(); - }, -]); diff --git a/app/extensions/registry-management/views/repositories/tag/registryRepositoryTag.html b/app/extensions/registry-management/views/repositories/tag/registryRepositoryTag.html deleted file mode 100644 index 881649b9d..000000000 --- a/app/extensions/registry-management/views/repositories/tag/registryRepositoryTag.html +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - - Registries > {{ ctrl.registry.Name }} > - {{ ctrl.context.repository }} > - {{ ctrl.context.tag }} - - - -
    -
    - - - -
    -
    -
    -
    -
    - {{ tag }} -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ID - {{ ctrl.details.Id }} -
    Parent{{ ctrl.details.Parent }}
    Created{{ ctrl.details.Created | getisodate }}
    BuildDocker {{ ctrl.details.DockerVersion }} on {{ ctrl.details.Os }}, {{ ctrl.details.Architecture }}
    Author{{ ctrl.details.Author }}
    -
    -
    -
    -
    - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    CMD{{ ctrl.details.Command | command }}
    ENTRYPOINT{{ ctrl.details.Entrypoint | command }}
    EXPOSE - - {{ port }} - -
    VOLUME - - {{ volume }} - -
    ENV - - - - - -
    {{ var|key: '=' }}{{ var|value: '=' }}
    -
    -
    -
    -
    -
    - -
    -
    - - - - - - - - - - - - - - -
    - - Order - - - - - - Layer - - - -
    - {{ layer.Order }} - -
    - - - {{ layer.CreatedBy | truncate: 130 }} - - - - - - -
    -
    - - {{ layer.CreatedBy }} - -
    -
    -
    -
    -
    -
    diff --git a/app/extensions/registry-management/views/repositories/tag/registryRepositoryTagController.js b/app/extensions/registry-management/views/repositories/tag/registryRepositoryTagController.js deleted file mode 100644 index d2a878a74..000000000 --- a/app/extensions/registry-management/views/repositories/tag/registryRepositoryTagController.js +++ /dev/null @@ -1,56 +0,0 @@ -import _ from 'lodash-es'; -import angular from 'angular'; -import { RegistryImageLayerViewModel } from 'Extensions/registry-management/models/registryImageLayer'; -import { RegistryImageDetailsViewModel } from 'Extensions/registry-management/models/registryImageDetails'; - -class RegistryRepositoryTagController { - /* @ngInject */ - constructor($transition$, $async, Notifications, RegistryService, RegistryServiceSelector, imagelayercommandFilter) { - this.$transition$ = $transition$; - this.$async = $async; - this.Notifications = Notifications; - this.RegistryService = RegistryService; - this.RegistryServiceSelector = RegistryServiceSelector; - this.imagelayercommandFilter = imagelayercommandFilter; - - this.context = {}; - this.onInit = this.onInit.bind(this); - } - - toggleLayerCommand(layerId) { - $('#layer-command-expander' + layerId + ' span').toggleClass('glyphicon-plus-sign glyphicon-minus-sign'); - $('#layer-command-' + layerId + '-short').toggle(); - $('#layer-command-' + layerId + '-full').toggle(); - } - - order(sortType) { - this.Sort.Reverse = this.Sort.Type === sortType ? !this.Sort.Reverse : false; - this.Sort.Type = sortType; - } - - async onInit() { - this.context.registryId = this.$transition$.params().id; - this.context.repository = this.$transition$.params().repository; - this.context.tag = this.$transition$.params().tag; - this.Sort = { - Type: 'Order', - Reverse: false, - }; - try { - this.registry = await this.RegistryService.registry(this.context.registryId); - this.tag = await this.RegistryServiceSelector.tag(this.registry, this.context.repository, this.context.tag); - const length = this.tag.History.length; - this.history = _.map(this.tag.History, (layer, idx) => new RegistryImageLayerViewModel(length - idx, layer)); - _.forEach(this.history, (item) => (item.CreatedBy = this.imagelayercommandFilter(item.CreatedBy))); - this.details = new RegistryImageDetailsViewModel(this.tag.History[0]); - } catch (error) { - this.Notifications.error('Failure', error, 'Unable to retrieve tag'); - } - } - - $onInit() { - return this.$async(this.onInit); - } -} -export default RegistryRepositoryTagController; -angular.module('portainer.extensions.registrymanagement').controller('RegistryRepositoryTagController', RegistryRepositoryTagController); diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html index bde459875..342102e5e 100644 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.html +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.html @@ -66,21 +66,6 @@ Manage access - - Browse - - - Browse (extension) - diff --git a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js index 98b342f07..f4df3c5a8 100644 --- a/app/portainer/components/datatables/registries-datatable/registriesDatatable.js +++ b/app/portainer/components/datatables/registries-datatable/registriesDatatable.js @@ -10,7 +10,6 @@ angular.module('portainer.app').component('registriesDatatable', { reverseOrder: '<', accessManagement: '<', removeAction: '<', - registryManagement: '<', canBrowse: '<', }, }); diff --git a/app/portainer/components/forms/registry-form-gitlab/registry-form-gitlab.html b/app/portainer/components/forms/registry-form-gitlab/registry-form-gitlab.html index f540d3add..2b64020d4 100644 --- a/app/portainer/components/forms/registry-form-gitlab/registry-form-gitlab.html +++ b/app/portainer/components/forms/registry-form-gitlab/registry-form-gitlab.html @@ -8,10 +8,6 @@ For information on how to generate a Gitlab Personal Access Token, follow the gitlab guide.

    -

    - You must provide a token with api scope. Failure to do so - will mean you can only push/pull from your registry but not manage it using the registry management (extension). -

    diff --git a/app/extensions/registry-management/models/gitlabRegistry.js b/app/portainer/models/gitlabRegistry.js similarity index 100% rename from app/extensions/registry-management/models/gitlabRegistry.js rename to app/portainer/models/gitlabRegistry.js diff --git a/app/portainer/models/registry.js b/app/portainer/models/registry.js index 640271294..54bf040c2 100644 --- a/app/portainer/models/registry.js +++ b/app/portainer/models/registry.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; +import { RegistryTypes } from '@/portainer/models/registryTypes'; export function RegistryViewModel(data) { this.Id = data.Id; diff --git a/app/extensions/registry-management/models/registryRepository.js b/app/portainer/models/registryRepository.js similarity index 100% rename from app/extensions/registry-management/models/registryRepository.js rename to app/portainer/models/registryRepository.js diff --git a/app/extensions/registry-management/models/registryTypes.js b/app/portainer/models/registryTypes.js similarity index 100% rename from app/extensions/registry-management/models/registryTypes.js rename to app/portainer/models/registryTypes.js diff --git a/app/extensions/registry-management/rest/gitlab.js b/app/portainer/rest/gitlab.js similarity index 92% rename from app/extensions/registry-management/rest/gitlab.js rename to app/portainer/rest/gitlab.js index 5616cba6d..1418a4cb9 100644 --- a/app/extensions/registry-management/rest/gitlab.js +++ b/app/portainer/rest/gitlab.js @@ -1,6 +1,6 @@ import gitlabResponseGetLink from './transform/gitlabResponseGetLink'; -angular.module('portainer.extensions.registrymanagement').factory('Gitlab', [ +angular.module('portainer.app').factory('Gitlab', [ '$resource', 'API_ENDPOINT_REGISTRIES', function GitlabFactory($resource, API_ENDPOINT_REGISTRIES) { diff --git a/app/extensions/registry-management/rest/transform/gitlabResponseGetLink.js b/app/portainer/rest/transform/gitlabResponseGetLink.js similarity index 100% rename from app/extensions/registry-management/rest/transform/gitlabResponseGetLink.js rename to app/portainer/rest/transform/gitlabResponseGetLink.js diff --git a/app/portainer/services/api/registryService.js b/app/portainer/services/api/registryService.js index b59280d50..d1468c91e 100644 --- a/app/portainer/services/api/registryService.js +++ b/app/portainer/services/api/registryService.js @@ -1,6 +1,6 @@ import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; -import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; +import { RegistryTypes } from '@/portainer/models/registryTypes'; import { RegistryCreateRequest, RegistryViewModel } from '../../models/registry'; angular.module('portainer.app').factory('RegistryService', [ diff --git a/app/extensions/registry-management/services/registryGitlabService.js b/app/portainer/services/registryGitlabService.js similarity index 96% rename from app/extensions/registry-management/services/registryGitlabService.js rename to app/portainer/services/registryGitlabService.js index 657579eb9..f1115cbf2 100644 --- a/app/extensions/registry-management/services/registryGitlabService.js +++ b/app/portainer/services/registryGitlabService.js @@ -2,7 +2,7 @@ import _ from 'lodash-es'; import { RegistryGitlabProject } from '../models/gitlabRegistry'; import { RegistryRepositoryGitlabViewModel } from '../models/registryRepository'; -angular.module('portainer.extensions.registrymanagement').factory('RegistryGitlabService', [ +angular.module('portainer.app').factory('RegistryGitlabService', [ '$async', 'Gitlab', function RegistryGitlabServiceFactory($async, Gitlab) { diff --git a/app/portainer/views/registries/create/createRegistryController.js b/app/portainer/views/registries/create/createRegistryController.js index 4cab30dc2..af7b1c181 100644 --- a/app/portainer/views/registries/create/createRegistryController.js +++ b/app/portainer/views/registries/create/createRegistryController.js @@ -1,4 +1,4 @@ -import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes'; +import { RegistryTypes } from '@/portainer/models/registryTypes'; import { RegistryDefaultModel } from '../../../models/registry'; angular.module('portainer.app').controller('CreateRegistryController', [ @@ -7,8 +7,7 @@ angular.module('portainer.app').controller('CreateRegistryController', [ 'RegistryService', 'Notifications', 'RegistryGitlabService', - 'ExtensionService', - function ($scope, $state, RegistryService, Notifications, RegistryGitlabService, ExtensionService) { + function ($scope, $state, RegistryService, Notifications, RegistryGitlabService) { $scope.selectQuayRegistry = selectQuayRegistry; $scope.selectAzureRegistry = selectAzureRegistry; $scope.selectCustomRegistry = selectCustomRegistry; @@ -106,7 +105,6 @@ angular.module('portainer.app').controller('CreateRegistryController', [ function initView() { $scope.RegistryTypes = RegistryTypes; $scope.model = new RegistryDefaultModel(); - ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.REGISTRY_MANAGEMENT).then((data) => ($scope.registryExtensionEnabled = data)); } initView(); diff --git a/app/portainer/views/registries/registries.html b/app/portainer/views/registries/registries.html index 22f89ade9..451146be2 100644 --- a/app/portainer/views/registries/registries.html +++ b/app/portainer/views/registries/registries.html @@ -81,7 +81,6 @@ order-by="Name" access-management="isAdmin" remove-action="removeAction" - registry-management="registryManagementAvailable" can-browse="canBrowse" >
    diff --git a/app/portainer/views/registries/registriesController.js b/app/portainer/views/registries/registriesController.js index 72e9e51eb..c0e40acf8 100644 --- a/app/portainer/views/registries/registriesController.js +++ b/app/portainer/views/registries/registriesController.js @@ -8,9 +8,8 @@ angular.module('portainer.app').controller('RegistriesController', [ 'DockerHubService', 'ModalService', 'Notifications', - 'ExtensionService', 'Authentication', - function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService, Authentication) { + function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, Authentication) { $scope.state = { actionInProgress: false, }; @@ -75,12 +74,10 @@ angular.module('portainer.app').controller('RegistriesController', [ $q.all({ registries: RegistryService.registries(), dockerhub: DockerHubService.dockerhub(), - registryManagement: ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.REGISTRY_MANAGEMENT), }) .then(function success(data) { $scope.registries = data.registries; $scope.dockerhub = data.dockerhub; - $scope.registryManagementAvailable = data.registryManagement; $scope.isAdmin = Authentication.isAdmin(); }) .catch(function error(err) { From 7aaf9d0eb7c7d7e92686c096391af60113d643e5 Mon Sep 17 00:00:00 2001 From: DarkAEther <30438425+DarkAEther@users.noreply.github.com> Date: Thu, 6 Aug 2020 03:28:44 +0530 Subject: [PATCH 114/195] fix(registries): remove trailing slash and protocol in registry URLs (#4131) * feat(registries) prevent trailing slash * fix(registries) avoid trailing slash in update registry * fix(registries) include trailing slash removal notice in tooltips * fix(registries) remove protocol when updating existing registry * fix(registries) uniform usage of string replace function for registry update --- .../forms/registry-form-custom/registry-form-custom.html | 2 +- app/portainer/models/registry.js | 1 + app/portainer/services/api/registryService.js | 2 ++ app/portainer/views/registries/edit/registry.html | 5 ++++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/portainer/components/forms/registry-form-custom/registry-form-custom.html b/app/portainer/components/forms/registry-form-custom/registry-form-custom.html index fc9f4da1e..7081aff7c 100644 --- a/app/portainer/components/forms/registry-form-custom/registry-form-custom.html +++ b/app/portainer/components/forms/registry-form-custom/registry-form-custom.html @@ -30,7 +30,7 @@
    diff --git a/app/portainer/models/registry.js b/app/portainer/models/registry.js index 54bf040c2..a87bf1b1f 100644 --- a/app/portainer/models/registry.js +++ b/app/portainer/models/registry.js @@ -51,6 +51,7 @@ export function RegistryCreateRequest(model) { this.Name = model.Name; this.Type = model.Type; this.URL = _.replace(model.URL, /^https?\:\/\//i, ''); + this.URL = _.replace(this.URL, /\/$/, ''); this.Authentication = model.Authentication; if (model.Authentication) { this.Username = model.Username; diff --git a/app/portainer/services/api/registryService.js b/app/portainer/services/api/registryService.js index d1468c91e..183cfc943 100644 --- a/app/portainer/services/api/registryService.js +++ b/app/portainer/services/api/registryService.js @@ -62,6 +62,8 @@ angular.module('portainer.app').factory('RegistryService', [ }; service.updateRegistry = function (registry) { + registry.URL = _.replace(registry.URL, /^https?\:\/\//i, ''); + registry.URL = _.replace(registry.URL, /\/$/, ''); return Registries.update({ id: registry.Id }, registry).$promise; }; diff --git a/app/portainer/views/registries/edit/registry.html b/app/portainer/views/registries/edit/registry.html index 4ddcb569d..011afb5ea 100644 --- a/app/portainer/views/registries/edit/registry.html +++ b/app/portainer/views/registries/edit/registry.html @@ -22,7 +22,10 @@
    From 2158cc5157a175a5e18c04d884e9063b511cb3b4 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Fri, 7 Aug 2020 01:46:25 +0300 Subject: [PATCH 115/195] feat(telemetry): replace GA with matomo (#4140) * feat(core/telemetry): add posthog * feat(core/telemetry): add posthog * feat(core/telemetry): add matomo * feat(core/telemetry): update matomo * feat(core/telemetry): update matomo * feat(core/telemetry): update matomo * feat(telemetry): remove google analytics code * refactor(telemetry): move matomo code to bundle * refactor(telemetry): move matomo lib to assets * refactor(telemetry): depreciate --no-analytics * feat(settings): introduce a setting to enable telemetry * fix(cli): fix typo * feat(settings): allow toggle telemetry from settings * fix(settings): handle case where AuthenticationMethod is missing * feat(admin): set telemetry on admin init * refactor(app); revert file * refactor(state-manager): move optout to state manager * feat(telemetry): set matomo url * feat(core/settings): minor UI update * feat(core/telemetry): update custom URL * feat(core/telemetry): add placeholder for privacy policy * feat(core/telemetry): add privacy policy link Co-authored-by: Anthony Lapenna --- .eslintrc.yml | 1 - api/bolt/migrator/migrate_dbversion24.go | 1 + api/cli/cli.go | 7 +- api/cmd/portainer/main.go | 4 +- api/http/handler/settings/settings_public.go | 2 + api/http/handler/settings/settings_update.go | 7 +- api/portainer.go | 4 +- api/swagger.yaml | 5088 ++++++++--------- app/__module.js | 6 +- app/assets/js/angulartics-matomo.js | 223 + app/config.js | 6 +- app/matomo-setup.js | 14 + app/portainer/__module.js | 18 +- app/portainer/models/settings.js | 2 + app/portainer/models/status.js | 1 - app/portainer/services/stateManager.js | 24 +- app/portainer/views/about/about.html | 14 +- app/portainer/views/init/admin/initAdmin.html | 11 + .../views/init/admin/initAdminController.js | 8 +- app/portainer/views/settings/settings.html | 12 + .../views/settings/settingsController.js | 4 + app/vendors.js | 2 +- gruntfile.js | 4 +- package.json | 7 +- webpack/webpack.common.js | 5 +- yarn.lock | 9 +- 26 files changed, 2879 insertions(+), 2605 deletions(-) create mode 100644 app/assets/js/angulartics-matomo.js create mode 100644 app/matomo-setup.js diff --git a/.eslintrc.yml b/.eslintrc.yml index b6829dd81..9a3201f96 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -6,7 +6,6 @@ env: globals: angular: true - __CONFIG_GA_ID: true extends: - 'eslint:recommended' diff --git a/api/bolt/migrator/migrate_dbversion24.go b/api/bolt/migrator/migrate_dbversion24.go index 4749607c5..b4843f2ff 100644 --- a/api/bolt/migrator/migrate_dbversion24.go +++ b/api/bolt/migrator/migrate_dbversion24.go @@ -15,6 +15,7 @@ func (m *Migrator) updateSettingsToDB25() error { } legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout + legacySettings.EnableTelemetry = true legacySettings.AllowContainerCapabilitiesForRegularUsers = true diff --git a/api/cli/cli.go b/api/cli/cli.go index 4bcec03fd..1d3aece2a 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -2,6 +2,7 @@ package cli import ( "errors" + "log" "time" "github.com/portainer/portainer/api" @@ -35,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(), - NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(), + NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Default(defaultNoAnalytics).Bool(), TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(), TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(), @@ -88,7 +89,9 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { } func displayDeprecationWarnings(flags *portainer.CLIFlags) { - + if flags.NoAnalytics != nil { + log.Println("Warning: The --no-analytics has been deprecated and will be removed in a future version of Portainer. It currently has no effect, telemetry settings are available in the Portainer settings.") + } } func validateEndpointURL(endpointURL string) error { diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index e3b2d0da9..92fcdb1ed 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -154,8 +154,7 @@ func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelServic func initStatus(flags *portainer.CLIFlags) *portainer.Status { return &portainer.Status{ - Analytics: !*flags.NoAnalytics, - Version: portainer.APIVersion, + Version: portainer.APIVersion, } } @@ -168,6 +167,7 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI settings.LogoURL = *flags.Logo settings.SnapshotInterval = *flags.SnapshotInterval settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures + settings.EnableTelemetry = true if *flags.Templates != "" { settings.TemplatesURL = *flags.Templates diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index f0d4f422a..e94f501e0 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -22,6 +22,7 @@ type publicSettingsResponse struct { EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` OAuthLoginURI string `json:"OAuthLoginURI"` + EnableTelemetry bool `json:"EnableTelemetry"` } // GET request on /api/settings/public @@ -43,6 +44,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers, EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, + EnableTelemetry: settings.EnableTelemetry, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", settings.OAuthSettings.AuthorizationURI, settings.OAuthSettings.ClientID, diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 7b2a74f0e..5d1aded0e 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -33,10 +33,11 @@ type settingsUpdatePayload struct { EdgeAgentCheckinInterval *int EnableEdgeComputeFeatures *bool UserSessionTimeout *string + EnableTelemetry *bool } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { - if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 { + if payload.AuthenticationMethod != nil && *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 { return errors.New("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)") } if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) { @@ -164,6 +165,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers } + if payload.EnableTelemetry != nil { + settings.EnableTelemetry = *payload.EnableTelemetry + } + tlsError := handler.updateTLS(settings) if tlsError != nil { return tlsError diff --git a/api/portainer.go b/api/portainer.go index 367061816..06f133957 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -532,6 +532,7 @@ type ( EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` UserSessionTimeout string `json:"UserSessionTimeout"` + EnableTelemetry bool `json:"EnableTelemetry"` // Deprecated fields DisplayDonationHeader bool @@ -566,8 +567,7 @@ type ( // Status represents the application status Status struct { - Analytics bool `json:"Analytics"` - Version string `json:"Version"` + Version string `json:"Version"` } // Tag represents a tag that can be associated to a resource diff --git a/api/swagger.yaml b/api/swagger.yaml index 2ecd61139..1e29c9fae 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1,5 +1,5 @@ --- -swagger: "2.0" +swagger: '2.0' info: description: | Portainer API is an HTTP API served by Portainer. It is used by the Portainer UI and everything you can do with the UI can be done using the HTTP API. @@ -54,4725 +54,4721 @@ info: **NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8). - version: "2.0.0-dev" - title: "Portainer API" + version: '2.0.0-dev' + title: 'Portainer API' contact: - email: "info@portainer.io" -host: "portainer.domain" -basePath: "/api" + email: 'info@portainer.io' +host: 'portainer.domain' +basePath: '/api' tags: -- name: "auth" - description: "Authenticate against Portainer HTTP API" -- name: "dockerhub" - description: "Manage how Portainer connects to the DockerHub" -- name: "endpoints" - description: "Manage Docker environments" -- name: "endpoint_groups" - description: "Manage endpoint groups" -- name: "extensions" - description: "Manage extensions" -- name: "registries" - description: "Manage Docker registries" -- name: "resource_controls" - description: "Manage access control on Docker resources" -- name: "roles" - description: "Manage roles" -- name: "settings" - description: "Manage Portainer settings" -- name: "status" - description: "Information about the Portainer instance" -- name: "stacks" - description: "Manage Docker stacks" -- name: "users" - description: "Manage users" -- name: "tags" - description: "Manage tags" -- name: "teams" - description: "Manage teams" -- name: "team_memberships" - description: "Manage team memberships" -- name: "templates" - description: "Manage App Templates" -- name: "stacks" - description: "Manage stacks" -- name: "upload" - description: "Upload files" -- name: "websocket" - description: "Create exec sessions using websockets" + - name: 'auth' + description: 'Authenticate against Portainer HTTP API' + - name: 'dockerhub' + description: 'Manage how Portainer connects to the DockerHub' + - name: 'endpoints' + description: 'Manage Docker environments' + - name: 'endpoint_groups' + description: 'Manage endpoint groups' + - name: 'extensions' + description: 'Manage extensions' + - name: 'registries' + description: 'Manage Docker registries' + - name: 'resource_controls' + description: 'Manage access control on Docker resources' + - name: 'roles' + description: 'Manage roles' + - name: 'settings' + description: 'Manage Portainer settings' + - name: 'status' + description: 'Information about the Portainer instance' + - name: 'stacks' + description: 'Manage Docker stacks' + - name: 'users' + description: 'Manage users' + - name: 'tags' + description: 'Manage tags' + - name: 'teams' + description: 'Manage teams' + - name: 'team_memberships' + description: 'Manage team memberships' + - name: 'templates' + description: 'Manage App Templates' + - name: 'stacks' + description: 'Manage stacks' + - name: 'upload' + description: 'Upload files' + - name: 'websocket' + description: 'Create exec sessions using websockets' schemes: -- "http" -- "https" + - 'http' + - 'https' paths: /auth: post: tags: - - "auth" - summary: "Authenticate a user" + - 'auth' + summary: 'Authenticate a user' description: | Use this endpoint to authenticate against Portainer using a username and password. **Access policy**: public - operationId: "AuthenticateUser" + operationId: 'AuthenticateUser' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' parameters: - - in: "body" - name: "body" - description: "Credentials used for authentication" - required: true - schema: - $ref: "#/definitions/AuthenticateUserRequest" + - in: 'body' + name: 'body' + description: 'Credentials used for authentication' + required: true + schema: + $ref: '#/definitions/AuthenticateUserRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/AuthenticateUserResponse" + $ref: '#/definitions/AuthenticateUserResponse' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid credentials" + err: 'Invalid credentials' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 503: - description: "Authentication disabled" + description: 'Authentication disabled' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Authentication is disabled" + err: 'Authentication is disabled' /dockerhub: get: tags: - - "dockerhub" - summary: "Retrieve DockerHub information" + - 'dockerhub' + summary: 'Retrieve DockerHub information' description: | Use this endpoint to retrieve the information used to connect to the DockerHub **Access policy**: authenticated - operationId: "DockerHubInspect" + operationId: 'DockerHubInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/DockerHubSubset" + $ref: '#/definitions/DockerHubSubset' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "dockerhub" - summary: "Update DockerHub information" + - 'dockerhub' + summary: 'Update DockerHub information' description: | Use this endpoint to update the information used to connect to the DockerHub **Access policy**: administrator - operationId: "DockerHubUpdate" + operationId: 'DockerHubUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "DockerHub information" - required: true - schema: - $ref: "#/definitions/DockerHubUpdateRequest" + - in: 'body' + name: 'body' + description: 'DockerHub information' + required: true + schema: + $ref: '#/definitions/DockerHubUpdateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/DockerHub" + $ref: '#/definitions/DockerHub' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /endpoints: get: tags: - - "endpoints" - summary: "List endpoints" + - 'endpoints' + summary: 'List endpoints' description: | List all endpoints based on the current user authorizations. Will return all endpoints if using an administrator account otherwise it will only return authorized endpoints. **Access policy**: restricted - operationId: "EndpointList" + operationId: 'EndpointList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/EndpointListResponse" + $ref: '#/definitions/EndpointListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "endpoints" - summary: "Create a new endpoint" + - 'endpoints' + summary: 'Create a new endpoint' description: | Create a new endpoint that will be used to manage a Docker environment. **Access policy**: administrator - operationId: "EndpointCreate" + operationId: 'EndpointCreate' consumes: - - "multipart/form-data" + - 'multipart/form-data' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "Name" - in: "formData" - type: "string" - description: "Name that will be used to identify this endpoint (example: my-endpoint)" - required: true - - name: "EndpointType" - in: "formData" - type: "integer" - description: "Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge agent environment)" - required: true - - name: "URL" - in: "formData" - type: "string" - description: "URL or IP address of a Docker host (example: docker.mydomain.tld:2375).\ - \ Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)" - - name: "PublicURL" - in: "formData" - type: "string" - description: "URL or IP address where exposed containers will be reachable.\ - \ Defaults to URL if not specified (example: docker.mydomain.tld:2375)" - - name: "GroupID" - in: "formData" - type: "string" - description: "Endpoint group identifier. If not specified will default to 1 (unassigned)." - - name: "TLS" - in: "formData" - type: "string" - description: "Require TLS to connect against this endpoint (example: true)" - - name: "TLSSkipVerify" - in: "formData" - type: "string" - description: "Skip server verification when using TLS (example: false)" - - name: "TLSSkipClientVerify" - in: "formData" - type: "string" - description: "Skip client verification when using TLS (example: false)" - - name: "TLSCACertFile" - in: "formData" - type: "file" - description: "TLS CA certificate file" - - name: "TLSCertFile" - in: "formData" - type: "file" - description: "TLS client certificate file" - - name: "TLSKeyFile" - in: "formData" - type: "file" - description: "TLS client key file" - - name: "AzureApplicationID" - in: "formData" - type: "string" - description: "Azure application ID. Required if endpoint type is set to 3" - - name: "AzureTenantID" - in: "formData" - type: "string" - description: "Azure tenant ID. Required if endpoint type is set to 3" - - name: "AzureAuthenticationKey" - in: "formData" - type: "string" - description: "Azure authentication key. Required if endpoint type is set to 3" + - name: 'Name' + in: 'formData' + type: 'string' + description: 'Name that will be used to identify this endpoint (example: my-endpoint)' + required: true + - name: 'EndpointType' + in: 'formData' + type: 'integer' + description: 'Environment type. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge agent environment)' + required: true + - name: 'URL' + in: 'formData' + type: 'string' + description: "URL or IP address of a Docker host (example: docker.mydomain.tld:2375).\ + \ Defaults to local if not specified (Linux: /var/run/docker.sock, Windows: //./pipe/docker_engine)" + - name: 'PublicURL' + in: 'formData' + type: 'string' + description: "URL or IP address where exposed containers will be reachable.\ + \ Defaults to URL if not specified (example: docker.mydomain.tld:2375)" + - name: 'GroupID' + in: 'formData' + type: 'string' + description: 'Endpoint group identifier. If not specified will default to 1 (unassigned).' + - name: 'TLS' + in: 'formData' + type: 'string' + description: 'Require TLS to connect against this endpoint (example: true)' + - name: 'TLSSkipVerify' + in: 'formData' + type: 'string' + description: 'Skip server verification when using TLS (example: false)' + - name: 'TLSSkipClientVerify' + in: 'formData' + type: 'string' + description: 'Skip client verification when using TLS (example: false)' + - name: 'TLSCACertFile' + in: 'formData' + type: 'file' + description: 'TLS CA certificate file' + - name: 'TLSCertFile' + in: 'formData' + type: 'file' + description: 'TLS client certificate file' + - name: 'TLSKeyFile' + in: 'formData' + type: 'file' + description: 'TLS client key file' + - name: 'AzureApplicationID' + in: 'formData' + type: 'string' + description: 'Azure application ID. Required if endpoint type is set to 3' + - name: 'AzureTenantID' + in: 'formData' + type: 'string' + description: 'Azure tenant ID. Required if endpoint type is set to 3' + - name: 'AzureAuthenticationKey' + in: 'formData' + type: 'string' + description: 'Azure authentication key. Required if endpoint type is set to 3' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Endpoint" + $ref: '#/definitions/Endpoint' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 503: - description: "Endpoint management disabled" + description: 'Endpoint management disabled' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint management is disabled" + err: 'Endpoint management is disabled' /endpoints/{id}: get: tags: - - "endpoints" - summary: "Inspect an endpoint" + - 'endpoints' + summary: 'Inspect an endpoint' description: | Retrieve details abount an endpoint. **Access policy**: restricted - operationId: "EndpointInspect" + operationId: 'EndpointInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Endpoint identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Endpoint identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Endpoint" + $ref: '#/definitions/Endpoint' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "Endpoint not found" + description: 'Endpoint not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint not found" + err: 'Endpoint not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "endpoints" - summary: "Update an endpoint" + - 'endpoints' + summary: 'Update an endpoint' description: | Update an endpoint. **Access policy**: administrator - operationId: "EndpointUpdate" + operationId: 'EndpointUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Endpoint identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Endpoint details" - required: true - schema: - $ref: "#/definitions/EndpointUpdateRequest" + - name: 'id' + in: 'path' + description: 'Endpoint identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'Endpoint details' + required: true + schema: + $ref: '#/definitions/EndpointUpdateRequest' responses: 200: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 404: - description: "Endpoint not found" + description: 'Endpoint not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint not found" + err: 'Endpoint not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 503: - description: "Endpoint management disabled" + description: 'Endpoint management disabled' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint management is disabled" + err: 'Endpoint management is disabled' delete: tags: - - "endpoints" - summary: "Remove an endpoint" + - 'endpoints' + summary: 'Remove an endpoint' description: | Remove an endpoint. **Access policy**: administrator - operationId: "EndpointDelete" + operationId: 'EndpointDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Endpoint identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Endpoint identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "Endpoint not found" + description: 'Endpoint not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint not found" + err: 'Endpoint not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 503: - description: "Endpoint management disabled" + description: 'Endpoint management disabled' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint management is disabled" + err: 'Endpoint management is disabled' /endpoints/{id}/job: post: tags: - - "endpoints" - summary: "Execute a job on the endpoint host" + - 'endpoints' + summary: 'Execute a job on the endpoint host' description: | Execute a job (script) on the underlying host of the endpoint. **Access policy**: administrator - operationId: "EndpointJob" + operationId: 'EndpointJob' consumes: - - "multipart/form-data" + - 'multipart/form-data' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Endpoint identifier" - required: true - type: "integer" - - name: "method" - in: "query" - description: "Job execution method. Possible values: file or string." - required: true - type: "string" - - name: "nodeName" - in: "query" - description: "Optional. Hostname of a node when targeting a Portainer agent cluster." - required: true - type: "string" - - in: "body" - name: "body" - description: "Job details. Required when method equals string." - required: true - schema: - $ref: "#/definitions/EndpointJobRequest" - - name: "Image" - in: "formData" - type: "string" - description: "Container image which will be used to execute the job. Required when method equals file." - - name: "file" - in: "formData" - type: "file" - description: "Job script file. Required when method equals file." + - name: 'id' + in: 'path' + description: 'Endpoint identifier' + required: true + type: 'integer' + - name: 'method' + in: 'query' + description: 'Job execution method. Possible values: file or string.' + required: true + type: 'string' + - name: 'nodeName' + in: 'query' + description: 'Optional. Hostname of a node when targeting a Portainer agent cluster.' + required: true + type: 'string' + - in: 'body' + name: 'body' + description: 'Job details. Required when method equals string.' + required: true + schema: + $ref: '#/definitions/EndpointJobRequest' + - name: 'Image' + in: 'formData' + type: 'string' + description: 'Container image which will be used to execute the job. Required when method equals file.' + - name: 'file' + in: 'formData' + type: 'file' + description: 'Job script file. Required when method equals file.' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Endpoint" + $ref: '#/definitions/Endpoint' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 404: - description: "Endpoint not found" + description: 'Endpoint not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint not found" + err: 'Endpoint not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /endpoint_groups: get: tags: - - "endpoint_groups" - summary: "List endpoint groups" + - 'endpoint_groups' + summary: 'List endpoint groups' description: | List all endpoint groups based on the current user authorizations. Will return all endpoint groups if using an administrator account otherwise it will only return authorized endpoint groups. **Access policy**: restricted - operationId: "EndpointGroupList" + operationId: 'EndpointGroupList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/EndpointGroupListResponse" + $ref: '#/definitions/EndpointGroupListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "endpoint_groups" - summary: "Create a new endpoint" + - 'endpoint_groups' + summary: 'Create a new endpoint' description: | Create a new endpoint group. **Access policy**: administrator - operationId: "EndpointGroupCreate" + operationId: 'EndpointGroupCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "Registry details" - required: true - schema: - $ref: "#/definitions/EndpointGroupCreateRequest" + - in: 'body' + name: 'body' + description: 'Registry details' + required: true + schema: + $ref: '#/definitions/EndpointGroupCreateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/EndpointGroup" + $ref: '#/definitions/EndpointGroup' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /endpoint_groups/{id}: get: tags: - - "endpoint_groups" - summary: "Inspect an endpoint group" + - 'endpoint_groups' + summary: 'Inspect an endpoint group' description: | Retrieve details abount an endpoint group. **Access policy**: administrator - operationId: "EndpointGroupInspect" + operationId: 'EndpointGroupInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Endpoint group identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Endpoint group identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/EndpointGroup" + $ref: '#/definitions/EndpointGroup' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "EndpointGroup not found" + description: 'EndpointGroup not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "EndpointGroup not found" + err: 'EndpointGroup not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "endpoint_groups" - summary: "Update an endpoint group" + - 'endpoint_groups' + summary: 'Update an endpoint group' description: | Update an endpoint group. **Access policy**: administrator - operationId: "EndpointGroupUpdate" + operationId: 'EndpointGroupUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "EndpointGroup identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "EndpointGroup details" - required: true - schema: - $ref: "#/definitions/EndpointGroupUpdateRequest" + - name: 'id' + in: 'path' + description: 'EndpointGroup identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'EndpointGroup details' + required: true + schema: + $ref: '#/definitions/EndpointGroupUpdateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/EndpointGroup" + $ref: '#/definitions/EndpointGroup' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 404: - description: "EndpointGroup not found" + description: 'EndpointGroup not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "EndpointGroup not found" + err: 'EndpointGroup not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 503: - description: "EndpointGroup management disabled" + description: 'EndpointGroup management disabled' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "EndpointGroup management is disabled" + err: 'EndpointGroup management is disabled' delete: tags: - - "endpoint_groups" - summary: "Remove an endpoint group" + - 'endpoint_groups' + summary: 'Remove an endpoint group' description: | Remove an endpoint group. **Access policy**: administrator - operationId: "EndpointGroupDelete" + operationId: 'EndpointGroupDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "EndpointGroup identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'EndpointGroup identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "EndpointGroup not found" + description: 'EndpointGroup not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "EndpointGroup not found" + err: 'EndpointGroup not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 503: - description: "EndpointGroup management disabled" + description: 'EndpointGroup management disabled' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "EndpointGroup management is disabled" + err: 'EndpointGroup management is disabled' /endpoint_groups/{id}/endpoints/{endpointId}: put: tags: - - "endpoint_groups" - summary: "Add an endpoint to an endpoint group" + - 'endpoint_groups' + summary: 'Add an endpoint to an endpoint group' description: | Add an endpoint to an endpoint group **Access policy**: administrator - operationId: "EndpointGroupAddEndpoint" + operationId: 'EndpointGroupAddEndpoint' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "EndpointGroup identifier" - required: true - type: "integer" - - name: "endpointId" - in: "path" - description: "Endpoint identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'EndpointGroup identifier' + required: true + type: 'integer' + - name: 'endpointId' + in: 'path' + description: 'Endpoint identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 404: - description: "EndpointGroup not found" + description: 'EndpointGroup not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "EndpointGroup not found" + err: 'EndpointGroup not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "endpoint_groups" - summary: "Remove an endpoint group" + - 'endpoint_groups' + summary: 'Remove an endpoint group' description: | Remove an endpoint group. **Access policy**: administrator - operationId: "EndpointGroupDeleteEndpoint" + operationId: 'EndpointGroupDeleteEndpoint' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "EndpointGroup identifier" - required: true - type: "integer" - - name: "endpointId" - in: "path" - description: "Endpoint identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'EndpointGroup identifier' + required: true + type: 'integer' + - name: 'endpointId' + in: 'path' + description: 'Endpoint identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "EndpointGroup not found" + description: 'EndpointGroup not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "EndpointGroup not found" + err: 'EndpointGroup not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /extensions: get: tags: - - "extensions" - summary: "List extensions" + - 'extensions' + summary: 'List extensions' description: | List all extensions registered inside Portainer. If the store parameter is set to true, will retrieve extensions details from the online repository. **Access policy**: administrator - operationId: "ExtensionList" + operationId: 'ExtensionList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "store" - in: "query" - description: "Retrieve online information about extensions. Possible values: true or false." - required: false - type: "boolean" + - name: 'store' + in: 'query' + description: 'Retrieve online information about extensions. Possible values: true or false.' + required: false + type: 'boolean' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/ExtensionListResponse" + $ref: '#/definitions/ExtensionListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "extensions" - summary: "Enable an extension" + - 'extensions' + summary: 'Enable an extension' description: | Enable an extension. **Access policy**: administrator - operationId: "ExtensionCreate" + operationId: 'ExtensionCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "Extension details" - required: true - schema: - $ref: "#/definitions/ExtensionCreateRequest" + - in: 'body' + name: 'body' + description: 'Extension details' + required: true + schema: + $ref: '#/definitions/ExtensionCreateRequest' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /extensions/{id}: get: tags: - - "extensions" - summary: "Inspect an extension" + - 'extensions' + summary: 'Inspect an extension' description: | Retrieve details abount an extension. **Access policy**: administrator - operationId: "ExtensionInspect" + operationId: 'ExtensionInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "extension identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'extension identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Extension" + $ref: '#/definitions/Extension' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "Extension not found" + description: 'Extension not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Extension not found" + err: 'Extension not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "extensions" - summary: "Update an extension" + - 'extensions' + summary: 'Update an extension' description: | Update an extension to a specific version of the extension. **Access policy**: administrator - operationId: "ExtensionUpdate" + operationId: 'ExtensionUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Extension identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Extension details" - required: true - schema: - $ref: "#/definitions/ExtensionUpdateRequest" + - name: 'id' + in: 'path' + description: 'Extension identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'Extension details' + required: true + schema: + $ref: '#/definitions/ExtensionUpdateRequest' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 404: - description: "Extension not found" + description: 'Extension not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Extension not found" + err: 'Extension not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "extensions" - summary: "Disable an extension" + - 'extensions' + summary: 'Disable an extension' description: | Disable an extension. **Access policy**: administrator - operationId: "ExtensionDelete" + operationId: 'ExtensionDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Extension identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Extension identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "Extension not found" + description: 'Extension not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Extension not found" + err: 'Extension not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /registries: get: tags: - - "registries" - summary: "List registries" + - 'registries' + summary: 'List registries' description: | List all registries based on the current user authorizations. Will return all registries if using an administrator account otherwise it will only return authorized registries. **Access policy**: restricted - operationId: "RegistryList" + operationId: 'RegistryList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/RegistryListResponse" + $ref: '#/definitions/RegistryListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "registries" - summary: "Create a new registry" + - 'registries' + summary: 'Create a new registry' description: | Create a new registry. **Access policy**: administrator - operationId: "RegistryCreate" + operationId: 'RegistryCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "Registry details" - required: true - schema: - $ref: "#/definitions/RegistryCreateRequest" + - in: 'body' + name: 'body' + description: 'Registry details' + required: true + schema: + $ref: '#/definitions/RegistryCreateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Registry" + $ref: '#/definitions/Registry' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 409: - description: "Registry already exists" + description: 'Registry already exists' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "A registry is already defined for this URL" + err: 'A registry is already defined for this URL' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /registries/{id}: get: tags: - - "registries" - summary: "Inspect a registry" + - 'registries' + summary: 'Inspect a registry' description: | Retrieve details about a registry. **Access policy**: administrator - operationId: "RegistryInspect" + operationId: 'RegistryInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Registry identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Registry identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Registry" + $ref: '#/definitions/Registry' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "Registry not found" + description: 'Registry not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Registry not found" + err: 'Registry not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "registries" - summary: "Update a registry" + - 'registries' + summary: 'Update a registry' description: | Update a registry. **Access policy**: administrator - operationId: "RegistryUpdate" + operationId: 'RegistryUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Registry identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Registry details" - required: true - schema: - $ref: "#/definitions/RegistryUpdateRequest" + - name: 'id' + in: 'path' + description: 'Registry identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'Registry details' + required: true + schema: + $ref: '#/definitions/RegistryUpdateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Registry" + $ref: '#/definitions/Registry' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 404: - description: "Registry not found" + description: 'Registry not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint not found" + err: 'Endpoint not found' 409: - description: "Registry already exists" + description: 'Registry already exists' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "A registry is already defined for this URL" + err: 'A registry is already defined for this URL' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "registries" - summary: "Remove a registry" + - 'registries' + summary: 'Remove a registry' description: | Remove a registry. **Access policy**: administrator - operationId: "RegistryDelete" + operationId: 'RegistryDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Registry identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Registry identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "Registry not found" + description: 'Registry not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Registry not found" + err: 'Registry not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /resource_controls: post: tags: - - "resource_controls" - summary: "Create a new resource control" + - 'resource_controls' + summary: 'Create a new resource control' description: | Create a new resource control to restrict access to a Docker resource. **Access policy**: administrator - operationId: "ResourceControlCreate" + operationId: 'ResourceControlCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "Resource control details" - required: true - schema: - $ref: "#/definitions/ResourceControlCreateRequest" + - in: 'body' + name: 'body' + description: 'Resource control details' + required: true + schema: + $ref: '#/definitions/ResourceControlCreateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/ResourceControl" + $ref: '#/definitions/ResourceControl' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 409: - description: "Resource control already exists" + description: 'Resource control already exists' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "A resource control is already applied on this resource" + err: 'A resource control is already applied on this resource' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /resource_controls/{id}: put: tags: - - "resource_controls" - summary: "Update a resource control" + - 'resource_controls' + summary: 'Update a resource control' description: | Update a resource control. **Access policy**: restricted - operationId: "ResourceControlUpdate" + operationId: 'ResourceControlUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Resource control identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Resource control details" - required: true - schema: - $ref: "#/definitions/ResourceControlUpdateRequest" + - name: 'id' + in: 'path' + description: 'Resource control identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'Resource control details' + required: true + schema: + $ref: '#/definitions/ResourceControlUpdateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/ResourceControl" + $ref: '#/definitions/ResourceControl' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 404: - description: "Resource control not found" + description: 'Resource control not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Resource control not found" + err: 'Resource control not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "resource_controls" - summary: "Remove a resource control" + - 'resource_controls' + summary: 'Remove a resource control' description: | Remove a resource control. **Access policy**: administrator - operationId: "ResourceControlDelete" + operationId: 'ResourceControlDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Resource control identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Resource control identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 404: - description: "Resource control not found" + description: 'Resource control not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Resource control not found" + err: 'Resource control not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /roles: get: tags: - - "roles" - summary: "List roles" + - 'roles' + summary: 'List roles' description: | List all roles available for use with the RBAC extension. **Access policy**: administrator - operationId: "RoleList" + operationId: 'RoleList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/RoleListResponse" + $ref: '#/definitions/RoleListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /settings: get: tags: - - "settings" - summary: "Retrieve Portainer settings" + - 'settings' + summary: 'Retrieve Portainer settings' description: | Retrieve Portainer settings. **Access policy**: administrator - operationId: "SettingsInspect" + operationId: 'SettingsInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Settings" + $ref: '#/definitions/Settings' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "settings" - summary: "Update Portainer settings" + - 'settings' + summary: 'Update Portainer settings' description: | Update Portainer settings. **Access policy**: administrator - operationId: "SettingsUpdate" + operationId: 'SettingsUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "New settings" - required: true - schema: - $ref: "#/definitions/SettingsUpdateRequest" + - in: 'body' + name: 'body' + description: 'New settings' + required: true + schema: + $ref: '#/definitions/SettingsUpdateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Settings" + $ref: '#/definitions/Settings' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /settings/public: get: tags: - - "settings" - summary: "Retrieve Portainer public settings" + - 'settings' + summary: 'Retrieve Portainer public settings' description: | Retrieve public settings. Returns a small set of settings that are not reserved to administrators only. **Access policy**: public - operationId: "PublicSettingsInspect" + operationId: 'PublicSettingsInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/PublicSettingsInspectResponse" + $ref: '#/definitions/PublicSettingsInspectResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /settings/authentication/checkLDAP: put: tags: - - "settings" - summary: "Test LDAP connectivity" + - 'settings' + summary: 'Test LDAP connectivity' description: | Test LDAP connectivity using LDAP details. **Access policy**: administrator - operationId: "SettingsLDAPCheck" + operationId: 'SettingsLDAPCheck' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "LDAP settings" - required: true - schema: - $ref: "#/definitions/SettingsLDAPCheckRequest" + - in: 'body' + name: 'body' + description: 'LDAP settings' + required: true + schema: + $ref: '#/definitions/SettingsLDAPCheckRequest' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /status: get: tags: - - "status" - summary: "Check Portainer status" + - 'status' + summary: 'Check Portainer status' description: | Retrieve Portainer status. **Access policy**: public - operationId: "StatusInspect" + operationId: 'StatusInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Status" + $ref: '#/definitions/Status' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /stacks: get: tags: - - "stacks" - summary: "List stacks" + - 'stacks' + summary: 'List stacks' description: | List all stacks based on the current user authorizations. Will return all stacks if using an administrator account otherwise it will only return the list of stacks the user have access to. **Access policy**: restricted - operationId: "StackList" + operationId: 'StackList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "filters" - in: "query" - description: | - Filters to process on the stack list. Encoded as JSON (a map[string]string). - For example, {"SwarmID": "jpofkc0i9uo9wtx1zesuk649w"} will only return stacks that are part - of the specified Swarm cluster. Available filters: EndpointID, SwarmID. - type: "string" + - name: 'filters' + in: 'query' + description: | + Filters to process on the stack list. Encoded as JSON (a map[string]string). + For example, {"SwarmID": "jpofkc0i9uo9wtx1zesuk649w"} will only return stacks that are part + of the specified Swarm cluster. Available filters: EndpointID, SwarmID. + type: 'string' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/StackListResponse" + $ref: '#/definitions/StackListResponse' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "stacks" - summary: "Deploy a new stack" + - 'stacks' + summary: 'Deploy a new stack' description: | Deploy a new stack into a Docker environment specified via the endpoint identifier. **Access policy**: restricted - operationId: "StackCreate" + operationId: 'StackCreate' consumes: - - "multipart/form-data" + - 'multipart/form-data' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "type" - in: "query" - description: "Stack deployment type. Possible values: 1 (Swarm stack) or 2 (Compose stack)." - required: true - type: "integer" - - name: "method" - in: "query" - description: "Stack deployment method. Possible values: file, string or repository." - required: true - type: "string" - - name: "endpointId" - in: "query" - description: "Identifier of the endpoint that will be used to deploy the stack." - required: true - type: "integer" - - in: "body" - name: "body" - description: "Stack details. Required when method equals string or repository." - schema: - $ref: "#/definitions/StackCreateRequest" - - name: "Name" - in: "formData" - type: "string" - description: "Name of the stack. Required when method equals file." - - name: "EndpointID" - in: "formData" - type: "string" - description: "Endpoint identifier used to deploy the stack. Required when method equals file." - - name: "SwarmID" - in: "formData" - type: "string" - description: "Swarm cluster identifier. Required when method equals file and type equals 1." - - name: "file" - in: "formData" - type: "file" - description: "Stack file. Required when method equals file." - - name: "Env" - in: "formData" - type: "string" - description: "Environment variables passed during deployment, represented as a JSON array [{'name': 'name', 'value': 'value'}]. Optional, used when method equals file and type equals 1." + - name: 'type' + in: 'query' + description: 'Stack deployment type. Possible values: 1 (Swarm stack) or 2 (Compose stack).' + required: true + type: 'integer' + - name: 'method' + in: 'query' + description: 'Stack deployment method. Possible values: file, string or repository.' + required: true + type: 'string' + - name: 'endpointId' + in: 'query' + description: 'Identifier of the endpoint that will be used to deploy the stack.' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'Stack details. Required when method equals string or repository.' + schema: + $ref: '#/definitions/StackCreateRequest' + - name: 'Name' + in: 'formData' + type: 'string' + description: 'Name of the stack. Required when method equals file.' + - name: 'EndpointID' + in: 'formData' + type: 'string' + description: 'Endpoint identifier used to deploy the stack. Required when method equals file.' + - name: 'SwarmID' + in: 'formData' + type: 'string' + description: 'Swarm cluster identifier. Required when method equals file and type equals 1.' + - name: 'file' + in: 'formData' + type: 'file' + description: 'Stack file. Required when method equals file.' + - name: 'Env' + in: 'formData' + type: 'string' + description: "Environment variables passed during deployment, represented as a JSON array [{'name': 'name', 'value': 'value'}]. Optional, used when method equals file and type equals 1." responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Stack" + $ref: '#/definitions/Stack' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 404: - description: "Endpoint not found" + description: 'Endpoint not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Endpoint not found" + err: 'Endpoint not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /stacks/{id}: get: tags: - - "stacks" - summary: "Inspect a stack" + - 'stacks' + summary: 'Inspect a stack' description: | Retrieve details about a stack. **Access policy**: restricted - operationId: "StackInspect" + operationId: 'StackInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Stack identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Stack identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Stack" + $ref: '#/definitions/Stack' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 404: - description: "Stack not found" + description: 'Stack not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Stack not found" + err: 'Stack not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "stacks" - summary: "Update a stack" + - 'stacks' + summary: 'Update a stack' description: | Update a stack. **Access policy**: restricted - operationId: "StackUpdate" + operationId: 'StackUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Stack identifier" - required: true - type: "integer" - - name: "endpointId" - in: "query" - description: "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this \ - optional parameter to set the endpoint identifier used by the stack." - type: "integer" - - in: "body" - name: "body" - description: "Stack details" - required: true - schema: - $ref: "#/definitions/StackUpdateRequest" + - name: 'id' + in: 'path' + description: 'Stack identifier' + required: true + type: 'integer' + - name: 'endpointId' + in: 'query' + description: "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this \ + optional parameter to set the endpoint identifier used by the stack." + type: 'integer' + - in: 'body' + name: 'body' + description: 'Stack details' + required: true + schema: + $ref: '#/definitions/StackUpdateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Stack" + $ref: '#/definitions/Stack' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 404: - description: "Stack not found" + description: 'Stack not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Stack not found" + err: 'Stack not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "stacks" - summary: "Remove a stack" + - 'stacks' + summary: 'Remove a stack' description: | Remove a stack. **Access policy**: restricted - operationId: "StackDelete" + operationId: 'StackDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Stack identifier" - required: true - type: "integer" - - name: "external" - in: "query" - description: "Set to true to delete an external stack. Only external Swarm stacks are supported." - type: "boolean" - - name: "endpointId" - in: "query" - description: "Endpoint identifier used to remove an external stack (required when external is set to true)" - type: "string" + - name: 'id' + in: 'path' + description: 'Stack identifier' + required: true + type: 'integer' + - name: 'external' + in: 'query' + description: 'Set to true to delete an external stack. Only external Swarm stacks are supported.' + type: 'boolean' + - name: 'endpointId' + in: 'query' + description: 'Endpoint identifier used to remove an external stack (required when external is set to true)' + type: 'string' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 404: - description: "Stack not found" + description: 'Stack not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Stack not found" + err: 'Stack not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /stacks/{id}/file: get: tags: - - "stacks" - summary: "Retrieve the content of the Stack file for the specified stack" + - 'stacks' + summary: 'Retrieve the content of the Stack file for the specified stack' description: | Get Stack file content. **Access policy**: restricted - operationId: "StackFileInspect" + operationId: 'StackFileInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Stack identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Stack identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/StackFileInspectResponse" + $ref: '#/definitions/StackFileInspectResponse' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 404: - description: "Stack not found" + description: 'Stack not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Stack not found" + err: 'Stack not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /stacks/{id}/migrate: post: tags: - - "stacks" - summary: "Migrate a stack to another endpoint" + - 'stacks' + summary: 'Migrate a stack to another endpoint' description: | Migrate a stack from an endpoint to another endpoint. It will re-create the stack inside the target endpoint before removing the original stack. **Access policy**: restricted - operationId: "StackMigrate" + operationId: 'StackMigrate' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Stack identifier" - required: true - type: "integer" - - name: "endpointId" - in: "query" - description: "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this \ - optional parameter to set the endpoint identifier used by the stack." - type: "integer" - - in: "body" - name: "body" - description: "Stack migration details." - schema: - $ref: "#/definitions/StackMigrateRequest" + - name: 'id' + in: 'path' + description: 'Stack identifier' + required: true + type: 'integer' + - name: 'endpointId' + in: 'query' + description: "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this \ + optional parameter to set the endpoint identifier used by the stack." + type: 'integer' + - in: 'body' + name: 'body' + description: 'Stack migration details.' + schema: + $ref: '#/definitions/StackMigrateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Stack" + $ref: '#/definitions/Stack' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' 404: - description: "Stack not found" + description: 'Stack not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Stack not found" + err: 'Stack not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /users: get: tags: - - "users" - summary: "List users" + - 'users' + summary: 'List users' description: | List Portainer users. Non-administrator users will only be able to list other non-administrator user accounts. **Access policy**: restricted - operationId: "UserList" + operationId: 'UserList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/UserListResponse" + $ref: '#/definitions/UserListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "users" - summary: "Create a new user" + - 'users' + summary: 'Create a new user' description: | Create a new Portainer user. Only team leaders and administrators can create users. Only administrators can create an administrator user account. **Access policy**: restricted - operationId: "UserCreate" + operationId: 'UserCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "User details" - required: true - schema: - $ref: "#/definitions/UserCreateRequest" + - in: 'body' + name: 'body' + description: 'User details' + required: true + schema: + $ref: '#/definitions/UserCreateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/UserSubset" + $ref: '#/definitions/UserSubset' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 409: - description: "User already exists" + description: 'User already exists' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "User already exists" + err: 'User already exists' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /users/{id}: get: tags: - - "users" - summary: "Inspect a user" + - 'users' + summary: 'Inspect a user' description: | Retrieve details about a user. **Access policy**: administrator - operationId: "UserInspect" + operationId: 'UserInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "User identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'User identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/User" + $ref: '#/definitions/User' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "User not found" + description: 'User not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "User not found" + err: 'User not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "users" - summary: "Update a user" + - 'users' + summary: 'Update a user' description: | Update user details. A regular user account can only update his details. **Access policy**: authenticated - operationId: "UserUpdate" + operationId: 'UserUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "User identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "User details" - required: true - schema: - $ref: "#/definitions/UserUpdateRequest" + - name: 'id' + in: 'path' + description: 'User identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'User details' + required: true + schema: + $ref: '#/definitions/UserUpdateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/User" + $ref: '#/definitions/User' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 404: - description: "User not found" + description: 'User not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "User not found" + err: 'User not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "users" - summary: "Remove a user" + - 'users' + summary: 'Remove a user' description: | Remove a user. **Access policy**: administrator - operationId: "UserDelete" + operationId: 'UserDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "User identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'User identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "User not found" + description: 'User not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "User not found" + err: 'User not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /users/{id}/memberships: get: tags: - - "users" - summary: "Inspect a user memberships" + - 'users' + summary: 'Inspect a user memberships' description: | Inspect a user memberships. **Access policy**: authenticated - operationId: "UserMembershipsInspect" + operationId: 'UserMembershipsInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "User identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'User identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/UserMembershipsResponse" + $ref: '#/definitions/UserMembershipsResponse' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /users/{id}/passwd: post: tags: - - "users" - summary: "Check password validity for a user" + - 'users' + summary: 'Check password validity for a user' description: | Check if the submitted password is valid for the specified user. **Access policy**: authenticated - operationId: "UserPasswordCheck" + operationId: 'UserPasswordCheck' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "User identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "User details" - required: true - schema: - $ref: "#/definitions/UserPasswordCheckRequest" + - name: 'id' + in: 'path' + description: 'User identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'User details' + required: true + schema: + $ref: '#/definitions/UserPasswordCheckRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/UserPasswordCheckResponse" + $ref: '#/definitions/UserPasswordCheckResponse' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 404: - description: "User not found" + description: 'User not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "User not found" + err: 'User not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /users/admin/check: get: tags: - - "users" - summary: "Check administrator account existence" + - 'users' + summary: 'Check administrator account existence' description: | Check if an administrator account exists in the database. **Access policy**: public - operationId: "UserAdminCheck" + operationId: 'UserAdminCheck' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 204: - description: "Success" + description: 'Success' 404: - description: "User not found" + description: 'User not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "User not found" + err: 'User not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /users/admin/init: post: tags: - - "users" - summary: "Initialize administrator account" + - 'users' + summary: 'Initialize administrator account' description: | Initialize the 'admin' user account. **Access policy**: public - operationId: "UserAdminInit" + operationId: 'UserAdminInit' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "User details" - required: true - schema: - $ref: "#/definitions/UserAdminInitRequest" + - in: 'body' + name: 'body' + description: 'User details' + required: true + schema: + $ref: '#/definitions/UserAdminInitRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/User" + $ref: '#/definitions/User' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 409: - description: "Admin user already initialized" + description: 'Admin user already initialized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "User already exists" + err: 'User already exists' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /upload/tls/{certificate}: post: tags: - - "upload" - summary: "Upload TLS files" + - 'upload' + summary: 'Upload TLS files' description: | Use this endpoint to upload TLS files. **Access policy**: administrator - operationId: "UploadTLS" + operationId: 'UploadTLS' consumes: - - multipart/form-data + - multipart/form-data produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "path" - name: "certificate" - description: "TLS file type. Valid values are 'ca', 'cert' or 'key'." - required: true - type: "string" - - in: "query" - name: "folder" - description: "Folder where the TLS file will be stored. Will be created if not existing." - required: true - type: "string" - - in: "formData" - name: "file" - type: "file" - description: "The file to upload." + - in: 'path' + name: 'certificate' + description: "TLS file type. Valid values are 'ca', 'cert' or 'key'." + required: true + type: 'string' + - in: 'query' + name: 'folder' + description: 'Folder where the TLS file will be stored. Will be created if not existing.' + required: true + type: 'string' + - in: 'formData' + name: 'file' + type: 'file' + description: 'The file to upload.' responses: 200: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data" + err: 'Invalid request data' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /tags: get: tags: - - "tags" - summary: "List tags" + - 'tags' + summary: 'List tags' description: | List tags. **Access policy**: administrator - operationId: "TagList" + operationId: 'TagList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/TagListResponse" + $ref: '#/definitions/TagListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "tags" - summary: "Create a new tag" + - 'tags' + summary: 'Create a new tag' description: | Create a new tag. **Access policy**: administrator - operationId: "TagCreate" + operationId: 'TagCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "Tag details" - required: true - schema: - $ref: "#/definitions/TagCreateRequest" + - in: 'body' + name: 'body' + description: 'Tag details' + required: true + schema: + $ref: '#/definitions/TagCreateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Tag" + $ref: '#/definitions/Tag' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 409: - description: "Conflict" + description: 'Conflict' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "A tag with the specified name already exists" + err: 'A tag with the specified name already exists' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /tags/{id}: delete: tags: - - "tags" - summary: "Remove a tag" + - 'tags' + summary: 'Remove a tag' description: | Remove a tag. **Access policy**: administrator - operationId: "TagDelete" + operationId: 'TagDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Tag identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Tag identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /teams: get: tags: - - "teams" - summary: "List teams" + - 'teams' + summary: 'List teams' description: | List teams. For non-administrator users, will only list the teams they are member of. **Access policy**: restricted - operationId: "TeamList" + operationId: 'TeamList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/TeamListResponse" + $ref: '#/definitions/TeamListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "teams" - summary: "Create a new team" + - 'teams' + summary: 'Create a new team' description: | Create a new team. **Access policy**: administrator - operationId: "TeamCreate" + operationId: 'TeamCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "Team details" - required: true - schema: - $ref: "#/definitions/TeamCreateRequest" + - in: 'body' + name: 'body' + description: 'Team details' + required: true + schema: + $ref: '#/definitions/TeamCreateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Team" + $ref: '#/definitions/Team' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 409: - description: "Team already exists" + description: 'Team already exists' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Team already exists" + err: 'Team already exists' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /teams/{id}: get: tags: - - "teams" - summary: "Inspect a team" + - 'teams' + summary: 'Inspect a team' description: | Retrieve details about a team. Access is only available for administrator and leaders of that team. **Access policy**: restricted - operationId: "TeamInspect" + operationId: 'TeamInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Team identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Team identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Team" + $ref: '#/definitions/Team' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 404: - description: "Team not found" + description: 'Team not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Team not found" + err: 'Team not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "teams" - summary: "Update a team" + - 'teams' + summary: 'Update a team' description: | Update a team. **Access policy**: administrator - operationId: "TeamUpdate" + operationId: 'TeamUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Team identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Team details" - required: true - schema: - $ref: "#/definitions/TeamUpdateRequest" + - name: 'id' + in: 'path' + description: 'Team identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'Team details' + required: true + schema: + $ref: '#/definitions/TeamUpdateRequest' responses: 200: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 404: - description: "Team not found" + description: 'Team not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Team not found" + err: 'Team not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "teams" - summary: "Remove a team" + - 'teams' + summary: 'Remove a team' description: | Remove a team. **Access policy**: administrator - operationId: "TeamDelete" + operationId: 'TeamDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Team identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Team identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 404: - description: "Team not found" + description: 'Team not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Team not found" + err: 'Team not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /teams/{id}/memberships: get: tags: - - "teams" - summary: "Inspect a team memberships" + - 'teams' + summary: 'Inspect a team memberships' description: | Inspect a team memberships. Access is only available for administrator and leaders of that team. **Access policy**: restricted - operationId: "TeamMembershipsInspect" + operationId: 'TeamMembershipsInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Team identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Team identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/TeamMembershipsResponse" + $ref: '#/definitions/TeamMembershipsResponse' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /team_memberships: get: tags: - - "team_memberships" - summary: "List team memberships" + - 'team_memberships' + summary: 'List team memberships' description: | List team memberships. Access is only available to administrators and team leaders. **Access policy**: restricted - operationId: "TeamMembershipList" + operationId: 'TeamMembershipList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/TeamMembershipListResponse" + $ref: '#/definitions/TeamMembershipListResponse' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "team_memberships" - summary: "Create a new team membership" + - 'team_memberships' + summary: 'Create a new team membership' description: | Create a new team memberships. Access is only available to administrators leaders of the associated team. **Access policy**: restricted - operationId: "TeamMembershipCreate" + operationId: 'TeamMembershipCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "Team membership details" - required: true - schema: - $ref: "#/definitions/TeamMembershipCreateRequest" + - in: 'body' + name: 'body' + description: 'Team membership details' + required: true + schema: + $ref: '#/definitions/TeamMembershipCreateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/TeamMembership" + $ref: '#/definitions/TeamMembership' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 409: - description: "Team membership already exists" + description: 'Team membership already exists' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Team membership already exists for this user and team." + err: 'Team membership already exists for this user and team.' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /team_memberships/{id}: put: tags: - - "team_memberships" - summary: "Update a team membership" + - 'team_memberships' + summary: 'Update a team membership' description: | Update a team membership. Access is only available to administrators leaders of the associated team. **Access policy**: restricted - operationId: "TeamMembershipUpdate" + operationId: 'TeamMembershipUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Team membership identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Team membership details" - required: true - schema: - $ref: "#/definitions/TeamMembershipUpdateRequest" + - name: 'id' + in: 'path' + description: 'Team membership identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'Team membership details' + required: true + schema: + $ref: '#/definitions/TeamMembershipUpdateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/TeamMembership" + $ref: '#/definitions/TeamMembership' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 404: - description: "Team membership not found" + description: 'Team membership not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Team membership not found" + err: 'Team membership not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "team_memberships" - summary: "Remove a team membership" + - 'team_memberships' + summary: 'Remove a team membership' description: | Remove a team membership. Access is only available to administrators leaders of the associated team. **Access policy**: restricted - operationId: "TeamMembershipDelete" + operationId: 'TeamMembershipDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "TeamMembership identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'TeamMembership identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 404: - description: "Team membership not found" + description: 'Team membership not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Team membership not found" + err: 'Team membership not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /templates: get: tags: - - "templates" - summary: "List available templates" + - 'templates' + summary: 'List available templates' description: | List available templates. Administrator templates will not be listed for non-administrator users. **Access policy**: restricted - operationId: "TemplateList" + operationId: 'TemplateList' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: [] responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/TemplateListResponse" + $ref: '#/definitions/TemplateListResponse' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' post: tags: - - "templates" - summary: "Create a new template" + - 'templates' + summary: 'Create a new template' description: | Create a new template. **Access policy**: administrator - operationId: "TemplateCreate" + operationId: 'TemplateCreate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - in: "body" - name: "body" - description: "Template details" - required: true - schema: - $ref: "#/definitions/TemplateCreateRequest" + - in: 'body' + name: 'body' + description: 'Template details' + required: true + schema: + $ref: '#/definitions/TemplateCreateRequest' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Template" + $ref: '#/definitions/Template' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' /templates/{id}: get: tags: - - "templates" - summary: "Inspect a template" + - 'templates' + summary: 'Inspect a template' description: | Retrieve details about a template. **Access policy**: administrator - operationId: "TemplateInspect" + operationId: 'TemplateInspect' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Template identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Template identifier' + required: true + type: 'integer' responses: 200: - description: "Success" + description: 'Success' schema: - $ref: "#/definitions/Template" + $ref: '#/definitions/Template' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 404: - description: "Template not found" + description: 'Template not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Template not found" + err: 'Template not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' put: tags: - - "templates" - summary: "Update a template" + - 'templates' + summary: 'Update a template' description: | Update a template. **Access policy**: administrator - operationId: "TemplateUpdate" + operationId: 'TemplateUpdate' consumes: - - "application/json" + - 'application/json' produces: - - "application/json" + - 'application/json' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Template identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Template details" - required: true - schema: - $ref: "#/definitions/TemplateUpdateRequest" + - name: 'id' + in: 'path' + description: 'Template identifier' + required: true + type: 'integer' + - in: 'body' + name: 'body' + description: 'Template details' + required: true + schema: + $ref: '#/definitions/TemplateUpdateRequest' responses: 200: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request data format" + err: 'Invalid request data format' 403: - description: "Unauthorized" + description: 'Unauthorized' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Access denied to resource" + err: 'Access denied to resource' 404: - description: "Template not found" + description: 'Template not found' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Template not found" + err: 'Template not found' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' delete: tags: - - "templates" - summary: "Remove a template" + - 'templates' + summary: 'Remove a template' description: | Remove a template. **Access policy**: administrator - operationId: "TemplateDelete" + operationId: 'TemplateDelete' security: - - jwt: [] + - jwt: [] parameters: - - name: "id" - in: "path" - description: "Template identifier" - required: true - type: "integer" + - name: 'id' + in: 'path' + description: 'Template identifier' + required: true + type: 'integer' responses: 204: - description: "Success" + description: 'Success' 400: - description: "Invalid request" + description: 'Invalid request' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' examples: application/json: - err: "Invalid request" + err: 'Invalid request' 500: - description: "Server error" + description: 'Server error' schema: - $ref: "#/definitions/GenericError" + $ref: '#/definitions/GenericError' securityDefinitions: jwt: - type: "apiKey" - name: "Authorization" - in: "header" + type: 'apiKey' + name: 'Authorization' + in: 'header' definitions: Tag: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Tag identifier" + description: 'Tag identifier' Name: - type: "string" - example: "org/acme" - description: "Tag name" + type: 'string' + example: 'org/acme' + description: 'Tag name' Team: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' Name: - type: "string" - example: "developers" - description: "Team name" + type: 'string' + example: 'developers' + description: 'Team name' TeamMembership: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Membership identifier" + description: 'Membership identifier' UserID: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' TeamID: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' Role: - type: "integer" + type: 'integer' example: 1 - description: "Team role (1 for team leader and 2 for team member)" + description: 'Team role (1 for team leader and 2 for team member)' UserSubset: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' Username: - type: "string" - example: "bob" - description: "Username" + type: 'string' + example: 'bob' + description: 'Username' Role: - type: "integer" + type: 'integer' example: 1 - description: "User role (1 for administrator account and 2 for regular account)" + description: 'User role (1 for administrator account and 2 for regular account)' User: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' Username: - type: "string" - example: "bob" - description: "Username" + type: 'string' + example: 'bob' + description: 'Username' Password: - type: "string" - example: "passwd" - description: "Password" + type: 'string' + example: 'passwd' + description: 'Password' Role: - type: "integer" + type: 'integer' example: 1 - description: "User role (1 for administrator account and 2 for regular account)" + description: 'User role (1 for administrator account and 2 for regular account)' Status: - type: "object" + type: 'object' properties: Authentication: - type: "boolean" + type: 'boolean' example: true - description: "Is authentication enabled" - Analytics: - type: "boolean" - example: true - description: "Is analytics enabled" + description: 'Is authentication enabled' Version: - type: "string" - example: "2.0.0-dev" - description: "Portainer API version" + type: 'string' + example: '2.0.0-dev' + description: 'Portainer API version' PublicSettingsInspectResponse: - type: "object" + type: 'object' properties: LogoURL: - type: "string" - example: "https://mycompany.mydomain.tld/logo.png" + type: 'string' + example: 'https://mycompany.mydomain.tld/logo.png' description: "URL to a logo that will be displayed on the login page as well\ \ as on top of the sidebar. Will use default Portainer logo when value is\ \ empty string" DisplayExternalContributors: - type: "boolean" + type: 'boolean' example: false description: "Whether to display or not external templates contributions as\ \ sub-menus in the UI." AuthenticationMethod: - type: "integer" + type: 'integer' example: 1 - description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." + description: 'Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP.' AllowBindMountsForRegularUsers: - type: "boolean" + type: 'boolean' example: false - description: "Whether non-administrator should be able to use bind mounts when creating containers" + description: 'Whether non-administrator should be able to use bind mounts when creating containers' AllowPrivilegedModeForRegularUsers: - type: "boolean" + type: 'boolean' example: true - description: "Whether non-administrator should be able to use privileged mode when creating containers" + description: 'Whether non-administrator should be able to use privileged mode when creating containers' TLSConfiguration: - type: "object" + type: 'object' properties: TLS: - type: "boolean" + type: 'boolean' example: true - description: "Use TLS" + description: 'Use TLS' TLSSkipVerify: - type: "boolean" + type: 'boolean' example: false - description: "Skip the verification of the server TLS certificate" + description: 'Skip the verification of the server TLS certificate' TLSCACertPath: - type: "string" - example: "/data/tls/ca.pem" - description: "Path to the TLS CA certificate file" + type: 'string' + example: '/data/tls/ca.pem' + description: 'Path to the TLS CA certificate file' TLSCertPath: - type: "string" - example: "/data/tls/cert.pem" - description: "Path to the TLS client certificate file" + type: 'string' + example: '/data/tls/cert.pem' + description: 'Path to the TLS client certificate file' TLSKeyPath: - type: "string" - example: "/data/tls/key.pem" - description: "Path to the TLS client key file" + type: 'string' + example: '/data/tls/key.pem' + description: 'Path to the TLS client key file' AzureCredentials: - type: "object" + type: 'object' properties: ApplicationID: - type: "string" - example: "eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4" - description: "Azure application ID" + type: 'string' + example: 'eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4' + description: 'Azure application ID' TenantID: - type: "string" - example: "34ddc78d-4fel-2358-8cc1-df84c8o839f5" - description: "Azure tenant ID" + type: 'string' + example: '34ddc78d-4fel-2358-8cc1-df84c8o839f5' + description: 'Azure tenant ID' AuthenticationKey: - type: "string" - example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" - description: "Azure authentication key" + type: 'string' + example: 'cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=' + description: 'Azure authentication key' LDAPSearchSettings: - type: "object" + type: 'object' properties: BaseDN: - type: "string" - example: "dc=ldap,dc=domain,dc=tld" - description: "The distinguished name of the element from which the LDAP server will search for users" + type: 'string' + example: 'dc=ldap,dc=domain,dc=tld' + description: 'The distinguished name of the element from which the LDAP server will search for users' Filter: - type: "string" - example: "(objectClass=account)" - description: "Optional LDAP search filter used to select user elements" + type: 'string' + example: '(objectClass=account)' + description: 'Optional LDAP search filter used to select user elements' UserNameAttribute: - type: "string" - example: "uid" - description: "LDAP attribute which denotes the username" + type: 'string' + example: 'uid' + description: 'LDAP attribute which denotes the username' LDAPGroupSearchSettings: - type: "object" + type: 'object' properties: GroupBaseDN: - type: "string" - example: "dc=ldap,dc=domain,dc=tld" - description: "The distinguished name of the element from which the LDAP server will search for groups." + type: 'string' + example: 'dc=ldap,dc=domain,dc=tld' + description: 'The distinguished name of the element from which the LDAP server will search for groups.' GroupFilter: - type: "string" - example: "(objectClass=account)" - description: "The LDAP search filter used to select group elements, optional." + type: 'string' + example: '(objectClass=account)' + description: 'The LDAP search filter used to select group elements, optional.' GroupAttribute: - type: "string" - example: "member" - description: "LDAP attribute which denotes the group membership." + type: 'string' + example: 'member' + description: 'LDAP attribute which denotes the group membership.' UserAccessPolicies: - type: "object" - description: "User access policies associated to a registry/endpoint/endpoint group. RoleID is not required for registry access policies and can be set to 0." + type: 'object' + description: 'User access policies associated to a registry/endpoint/endpoint group. RoleID is not required for registry access policies and can be set to 0.' additionalProperties: - $ref: "#/definitions/AccessPolicy" + $ref: '#/definitions/AccessPolicy' example: 1: { RoleID: 1 } 2: { RoleID: 3 } TeamAccessPolicies: - type: "object" - description: "Team access policies associated to a registry/endpoint/endpoint group. RoleID is not required for registry access policies and can be set to 0." + type: 'object' + description: 'Team access policies associated to a registry/endpoint/endpoint group. RoleID is not required for registry access policies and can be set to 0.' additionalProperties: - $ref: "#/definitions/AccessPolicy" + $ref: '#/definitions/AccessPolicy' example: 1: { RoleID: 1 } 2: { RoleID: 3 } AccessPolicy: - type: "object" + type: 'object' properties: RoleID: - type: "integer" - example: "1" - description: "Role identifier. Reference the role that will be associated to this access policy" + type: 'integer' + example: '1' + description: 'Role identifier. Reference the role that will be associated to this access policy' LDAPSettings: - type: "object" + type: 'object' properties: AnonymousMode: - type: "boolean" + type: 'boolean' example: true - description: "Enable this option if the server is configured for Anonymous access. When enabled, ReaderDN and Password will not be used." + description: 'Enable this option if the server is configured for Anonymous access. When enabled, ReaderDN and Password will not be used.' ReaderDN: - type: "string" - example: "cn=readonly-account,dc=ldap,dc=domain,dc=tld" - description: "Account that will be used to search for users" + type: 'string' + example: 'cn=readonly-account,dc=ldap,dc=domain,dc=tld' + description: 'Account that will be used to search for users' Password: - type: "string" - example: "readonly-password" - description: "Password of the account that will be used to search users" + type: 'string' + example: 'readonly-password' + description: 'Password of the account that will be used to search users' URL: - type: "string" - example: "myldap.domain.tld:389" - description: "URL or IP address of the LDAP server" + type: 'string' + example: 'myldap.domain.tld:389' + description: 'URL or IP address of the LDAP server' TLSConfig: - $ref: "#/definitions/TLSConfiguration" + $ref: '#/definitions/TLSConfiguration' StartTLS: - type: "boolean" + type: 'boolean' example: true - description: "Whether LDAP connection should use StartTLS" + description: 'Whether LDAP connection should use StartTLS' SearchSettings: - type: "array" + type: 'array' items: - $ref: "#/definitions/LDAPSearchSettings" + $ref: '#/definitions/LDAPSearchSettings' GroupSearchSettings: - type: "array" + type: 'array' items: - $ref: "#/definitions/LDAPGroupSearchSettings" + $ref: '#/definitions/LDAPGroupSearchSettings' AutoCreateUsers: - type: "boolean" + type: 'boolean' example: true - description: "Automatically provision users and assign them to matching LDAP group names" + description: 'Automatically provision users and assign them to matching LDAP group names' Settings: - type: "object" + type: 'object' properties: TemplatesURL: - type: "string" - example: "https://raw.githubusercontent.com/portainer/templates/master/templates.json" + type: 'string' + example: 'https://raw.githubusercontent.com/portainer/templates/master/templates.json' description: "URL to the templates that will be displayed in the UI when navigating\ \ to App Templates" LogoURL: - type: "string" - example: "https://mycompany.mydomain.tld/logo.png" + type: 'string' + example: 'https://mycompany.mydomain.tld/logo.png' description: "URL to a logo that will be displayed on the login page as well\ \ as on top of the sidebar. Will use default Portainer logo when value is\ \ empty string" BlackListedLabels: - type: "array" + type: 'array' description: "A list of label name & value that will be used to hide containers\ \ when querying containers" items: - $ref: "#/definitions/Settings_BlackListedLabels" + $ref: '#/definitions/Settings_BlackListedLabels' DisplayExternalContributors: - type: "boolean" + type: 'boolean' example: false description: "Whether to display or not external templates contributions as\ \ sub-menus in the UI." AuthenticationMethod: - type: "integer" + type: 'integer' example: 1 - description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." + description: 'Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP.' LDAPSettings: - $ref: "#/definitions/LDAPSettings" + $ref: '#/definitions/LDAPSettings' AllowBindMountsForRegularUsers: - type: "boolean" + type: 'boolean' example: false - description: "Whether non-administrator should be able to use bind mounts when creating containers" + description: 'Whether non-administrator should be able to use bind mounts when creating containers' AllowPrivilegedModeForRegularUsers: - type: "boolean" + type: 'boolean' example: true - description: "Whether non-administrator should be able to use privileged mode when creating containers" + description: 'Whether non-administrator should be able to use privileged mode when creating containers' Settings_BlackListedLabels: properties: name: - type: "string" - example: "com.foo" + type: 'string' + example: 'com.foo' value: - type: "string" - example: "bar" + type: 'string' + example: 'bar' Pair: properties: name: - type: "string" - example: "name" + type: 'string' + example: 'name' value: - type: "string" - example: "value" + type: 'string' + example: 'value' Registry: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Registry identifier" + description: 'Registry identifier' Name: - type: "string" - example: "my-registry" - description: "Registry name" + type: 'string' + example: 'my-registry' + description: 'Registry name' URL: - type: "string" - example: "registry.mydomain.tld:2375" - description: "URL or IP address of the Docker registry" + type: 'string' + example: 'registry.mydomain.tld:2375' + description: 'URL or IP address of the Docker registry' Authentication: - type: "boolean" + type: 'boolean' example: true - description: "Is authentication against this registry enabled" + description: 'Is authentication against this registry enabled' Username: - type: "string" - example: "registry_user" - description: "Username used to authenticate against this registry" + type: 'string' + example: 'registry_user' + description: 'Username used to authenticate against this registry' Password: - type: "string" - example: "registry_password" - description: "Password used to authenticate against this registry" + type: 'string' + example: 'registry_password' + description: 'Password used to authenticate against this registry' AuthorizedUsers: - type: "array" - description: "List of user identifiers authorized to use this registry" + type: 'array' + description: 'List of user identifiers authorized to use this registry' items: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' AuthorizedTeams: - type: "array" - description: "List of team identifiers authorized to use this registry" + type: 'array' + description: 'List of team identifiers authorized to use this registry' items: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' RegistrySubset: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Registry identifier" + description: 'Registry identifier' Name: - type: "string" - example: "my-registry" - description: "Registry name" + type: 'string' + example: 'my-registry' + description: 'Registry name' URL: - type: "string" - example: "registry.mydomain.tld:2375" - description: "URL or IP address of the Docker registry" + type: 'string' + example: 'registry.mydomain.tld:2375' + description: 'URL or IP address of the Docker registry' Authentication: - type: "boolean" + type: 'boolean' example: true - description: "Is authentication against this registry enabled" + description: 'Is authentication against this registry enabled' Username: - type: "string" - example: "registry_user" - description: "Username used to authenticate against this registry" + type: 'string' + example: 'registry_user' + description: 'Username used to authenticate against this registry' AuthorizedUsers: - type: "array" - description: "List of user identifiers authorized to use this registry" + type: 'array' + description: 'List of user identifiers authorized to use this registry' items: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' AuthorizedTeams: - type: "array" - description: "List of team identifiers authorized to use this registry" + type: 'array' + description: 'List of team identifiers authorized to use this registry' items: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' EndpointGroup: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Endpoint group identifier" + description: 'Endpoint group identifier' Name: - type: "string" - example: "my-endpoint-group" - description: "Endpoint group name" + type: 'string' + example: 'my-endpoint-group' + description: 'Endpoint group name' Description: - type: "string" - example: "Description associated to the endpoint group" - description: "Endpoint group description" + type: 'string' + example: 'Description associated to the endpoint group' + description: 'Endpoint group description' AuthorizedUsers: - type: "array" - description: "List of user identifiers authorized to connect to this endpoint group. Will be inherited by endpoints that are part of the group" + type: 'array' + description: 'List of user identifiers authorized to connect to this endpoint group. Will be inherited by endpoints that are part of the group' items: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' AuthorizedTeams: - type: "array" - description: "List of team identifiers authorized to connect to this endpoint. Will be inherited by endpoints that are part of the group" + type: 'array' + description: 'List of team identifiers authorized to connect to this endpoint. Will be inherited by endpoints that are part of the group' items: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' Labels: - type: "array" + type: 'array' items: - $ref: "#/definitions/Pair" + $ref: '#/definitions/Pair' Endpoint: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Endpoint identifier" + description: 'Endpoint identifier' Name: - type: "string" - example: "my-endpoint" - description: "Endpoint name" + type: 'string' + example: 'my-endpoint' + description: 'Endpoint name' Type: - type: "integer" + type: 'integer' example: 1 - description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment or 3 for an Azure environment." + description: 'Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment or 3 for an Azure environment.' URL: - type: "string" - example: "docker.mydomain.tld:2375" - description: "URL or IP address of the Docker host associated to this endpoint" + type: 'string' + example: 'docker.mydomain.tld:2375' + description: 'URL or IP address of the Docker host associated to this endpoint' PublicURL: - type: "string" - example: "docker.mydomain.tld:2375" - description: "URL or IP address where exposed containers will be reachable" + type: 'string' + example: 'docker.mydomain.tld:2375' + description: 'URL or IP address where exposed containers will be reachable' GroupID: - type: "integer" + type: 'integer' example: 1 - description: "Endpoint group identifier" + description: 'Endpoint group identifier' AuthorizedUsers: - type: "array" - description: "List of user identifiers authorized to connect to this endpoint" + type: 'array' + description: 'List of user identifiers authorized to connect to this endpoint' items: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' AuthorizedTeams: - type: "array" - description: "List of team identifiers authorized to connect to this endpoint" + type: 'array' + description: 'List of team identifiers authorized to connect to this endpoint' items: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' TLSConfig: - $ref: "#/definitions/TLSConfiguration" + $ref: '#/definitions/TLSConfiguration' AzureCredentials: - $ref: "#/definitions/AzureCredentials" + $ref: '#/definitions/AzureCredentials' EndpointSubset: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Endpoint identifier" + description: 'Endpoint identifier' Name: - type: "string" - example: "my-endpoint" - description: "Endpoint name" + type: 'string' + example: 'my-endpoint' + description: 'Endpoint name' Type: - type: "integer" + type: 'integer' example: 1 - description: "Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment, 3 for an Azure environment." + description: 'Endpoint environment type. 1 for a Docker environment, 2 for an agent on Docker environment, 3 for an Azure environment.' URL: - type: "string" - example: "docker.mydomain.tld:2375" - description: "URL or IP address of the Docker host associated to this endpoint" + type: 'string' + example: 'docker.mydomain.tld:2375' + description: 'URL or IP address of the Docker host associated to this endpoint' PublicURL: - type: "string" - example: "docker.mydomain.tld:2375" - description: "URL or IP address where exposed containers will be reachable" + type: 'string' + example: 'docker.mydomain.tld:2375' + description: 'URL or IP address where exposed containers will be reachable' GroupID: - type: "integer" + type: 'integer' example: 1 - description: "Endpoint group identifier" + description: 'Endpoint group identifier' AuthorizedUsers: - type: "array" - description: "List of user identifiers authorized to connect to this endpoint" + type: 'array' + description: 'List of user identifiers authorized to connect to this endpoint' items: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' AuthorizedTeams: - type: "array" - description: "List of team identifiers authorized to connect to this endpoint" + type: 'array' + description: 'List of team identifiers authorized to connect to this endpoint' items: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' TLSConfig: - $ref: "#/definitions/TLSConfiguration" + $ref: '#/definitions/TLSConfiguration' GenericError: - type: "object" + type: 'object' properties: err: - type: "string" - example: "Something bad happened" - description: "Error message" + type: 'string' + example: 'Something bad happened' + description: 'Error message' AuthenticateUserRequest: - type: "object" + type: 'object' required: - - "Password" - - "Username" + - 'Password' + - 'Username' properties: Username: - type: "string" - example: "admin" - description: "Username" + type: 'string' + example: 'admin' + description: 'Username' Password: - type: "string" - example: "mypassword" - description: "Password" + type: 'string' + example: 'mypassword' + description: 'Password' AuthenticateUserResponse: - type: "object" + type: 'object' properties: jwt: - type: "string" - example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE" - description: "JWT token used to authenticate against the API" + type: 'string' + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOjEsImV4cCI6MTQ5OTM3NjE1NH0.NJ6vE8FY1WG6jsRQzfMqeatJ4vh2TWAeeYfDhP71YEE' + description: 'JWT token used to authenticate against the API' DockerHubSubset: - type: "object" + type: 'object' properties: Authentication: - type: "boolean" + type: 'boolean' example: true - description: "Is authentication against DockerHub enabled" + description: 'Is authentication against DockerHub enabled' Username: - type: "string" - example: "hub_user" - description: "Username used to authenticate against the DockerHub" + type: 'string' + example: 'hub_user' + description: 'Username used to authenticate against the DockerHub' DockerHub: - type: "object" + type: 'object' properties: Authentication: - type: "boolean" + type: 'boolean' example: true - description: "Is authentication against DockerHub enabled" + description: 'Is authentication against DockerHub enabled' Username: - type: "string" - example: "hub_user" - description: "Username used to authenticate against the DockerHub" + type: 'string' + example: 'hub_user' + description: 'Username used to authenticate against the DockerHub' Password: - type: "string" - example: "hub_password" - description: "Password used to authenticate against the DockerHub" + type: 'string' + example: 'hub_password' + description: 'Password used to authenticate against the DockerHub' ResourceControl: - type: "object" + type: 'object' properties: ResourceID: - type: "string" - example: "617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08" + type: 'string' + example: '617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08' description: "Docker resource identifier on which access control will be applied.\ \ In the case of a resource control applied to a stack, use the stack name as identifier" Type: - type: "string" - example: "container" + type: 'string' + example: 'container' description: "Type of Docker resource. Valid values are: container, volume\ \ service, secret, config or stack" Public: - type: "boolean" + type: 'boolean' example: true - description: "Permit access to the associated resource to any user" + description: 'Permit access to the associated resource to any user' Users: - type: "array" - description: "List of user identifiers with access to the associated resource" + type: 'array' + description: 'List of user identifiers with access to the associated resource' items: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' Teams: - type: "array" - description: "List of team identifiers with access to the associated resource" + type: 'array' + description: 'List of team identifiers with access to the associated resource' items: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' SubResourceIDs: - type: "array" - description: "List of Docker resources that will inherit this access control" + type: 'array' + description: 'List of Docker resources that will inherit this access control' items: - type: "string" - example: "617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08" - description: "Docker resource identifier" + type: 'string' + example: '617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08' + description: 'Docker resource identifier' DockerHubUpdateRequest: - type: "object" + type: 'object' required: - - "Authentication" - - "Password" - - "Username" + - 'Authentication' + - 'Password' + - 'Username' properties: Authentication: - type: "boolean" + type: 'boolean' example: true - description: "Enable authentication against DockerHub" + description: 'Enable authentication against DockerHub' Username: - type: "string" - example: "hub_user" - description: "Username used to authenticate against the DockerHub" + type: 'string' + example: 'hub_user' + description: 'Username used to authenticate against the DockerHub' Password: - type: "string" - example: "hub_password" - description: "Password used to authenticate against the DockerHub" + type: 'string' + example: 'hub_password' + description: 'Password used to authenticate against the DockerHub' EndpointListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/EndpointSubset" + $ref: '#/definitions/EndpointSubset' EndpointGroupListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/EndpointGroup" + $ref: '#/definitions/EndpointGroup' EndpointUpdateRequest: - type: "object" + type: 'object' properties: Name: - type: "string" - example: "my-endpoint" - description: "Name that will be used to identify this endpoint" + type: 'string' + example: 'my-endpoint' + description: 'Name that will be used to identify this endpoint' URL: - type: "string" - example: "docker.mydomain.tld:2375" - description: "URL or IP address of a Docker host" + type: 'string' + example: 'docker.mydomain.tld:2375' + description: 'URL or IP address of a Docker host' PublicURL: - type: "string" - example: "docker.mydomain.tld:2375" + type: 'string' + example: 'docker.mydomain.tld:2375' description: "URL or IP address where exposed containers will be reachable.\ \ Defaults to URL if not specified" GroupID: - type: "integer" - example: "1" - description: "Group identifier" + type: 'integer' + example: '1' + description: 'Group identifier' TLS: - type: "boolean" + type: 'boolean' example: true - description: "Require TLS to connect against this endpoint" + description: 'Require TLS to connect against this endpoint' TLSSkipVerify: - type: "boolean" + type: 'boolean' example: false - description: "Skip server verification when using TLS" + description: 'Skip server verification when using TLS' TLSSkipClientVerify: - type: "boolean" + type: 'boolean' example: false - description: "Skip client verification when using TLS" + description: 'Skip client verification when using TLS' ApplicationID: - type: "string" - example: "eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4" - description: "Azure application ID" + type: 'string' + example: 'eag7cdo9-o09l-9i83-9dO9-f0b23oe78db4' + description: 'Azure application ID' TenantID: - type: "string" - example: "34ddc78d-4fel-2358-8cc1-df84c8o839f5" - description: "Azure tenant ID" + type: 'string' + example: '34ddc78d-4fel-2358-8cc1-df84c8o839f5' + description: 'Azure tenant ID' AuthenticationKey: - type: "string" - example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" - description: "Azure authentication key" + type: 'string' + example: 'cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=' + description: 'Azure authentication key' UserAccessPolicies: - $ref: "#/definitions/UserAccessPolicies" + $ref: '#/definitions/UserAccessPolicies' TeamAccessPolicies: - $ref: "#/definitions/TeamAccessPolicies" + $ref: '#/definitions/TeamAccessPolicies' RegistryCreateRequest: - type: "object" + type: 'object' required: - - "Authentication" - - "Name" - - "Password" - - "Type" - - "URL" - - "Username" + - 'Authentication' + - 'Name' + - 'Password' + - 'Type' + - 'URL' + - 'Username' properties: Name: - type: "string" - example: "my-registry" - description: "Name that will be used to identify this registry" + type: 'string' + example: 'my-registry' + description: 'Name that will be used to identify this registry' Type: - type: "integer" + type: 'integer' example: 1 - description: "Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)" + description: 'Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)' URL: - type: "string" - example: "registry.mydomain.tld:2375" - description: "URL or IP address of the Docker registry" + type: 'string' + example: 'registry.mydomain.tld:2375' + description: 'URL or IP address of the Docker registry' Authentication: - type: "boolean" + type: 'boolean' example: true - description: "Is authentication against this registry enabled" + description: 'Is authentication against this registry enabled' Username: - type: "string" - example: "registry_user" - description: "Username used to authenticate against this registry" + type: 'string' + example: 'registry_user' + description: 'Username used to authenticate against this registry' Password: - type: "string" - example: "registry_password" - description: "Password used to authenticate against this registry" + type: 'string' + example: 'registry_password' + description: 'Password used to authenticate against this registry' RegistryListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/RegistrySubset" + $ref: '#/definitions/RegistrySubset' RegistryUpdateRequest: - type: "object" + type: 'object' required: - - "Name" - - "URL" + - 'Name' + - 'URL' properties: Name: - type: "string" - example: "my-registry" - description: "Name that will be used to identify this registry" + type: 'string' + example: 'my-registry' + description: 'Name that will be used to identify this registry' URL: - type: "string" - example: "registry.mydomain.tld:2375" - description: "URL or IP address of the Docker registry" + type: 'string' + example: 'registry.mydomain.tld:2375' + description: 'URL or IP address of the Docker registry' Authentication: - type: "boolean" + type: 'boolean' example: true - description: "Is authentication against this registry enabled" + description: 'Is authentication against this registry enabled' Username: - type: "string" - example: "registry_user" - description: "Username used to authenticate against this registry" + type: 'string' + example: 'registry_user' + description: 'Username used to authenticate against this registry' Password: - type: "string" - example: "registry_password" - description: "Password used to authenticate against this registry" + type: 'string' + example: 'registry_password' + description: 'Password used to authenticate against this registry' UserAccessPolicies: - $ref: "#/definitions/UserAccessPolicies" + $ref: '#/definitions/UserAccessPolicies' TeamAccessPolicies: - $ref: "#/definitions/TeamAccessPolicies" + $ref: '#/definitions/TeamAccessPolicies' ResourceControlCreateRequest: - type: "object" + type: 'object' required: - - "ResourceID" - - "Type" + - 'ResourceID' + - 'Type' properties: ResourceID: - type: "string" - example: "617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08" + type: 'string' + example: '617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08' description: "Docker resource identifier on which access control will be applied.\ \ In the case of a resource control applied to a stack, use the stack name as identifier" Type: - type: "string" - example: "container" + type: 'string' + example: 'container' description: "Type of Docker resource. Valid values are: container, volume\ \ service, secret, config or stack" Public: - type: "boolean" + type: 'boolean' example: true - description: "Permit access to the associated resource to any user" + description: 'Permit access to the associated resource to any user' Users: - type: "array" - description: "List of user identifiers with access to the associated resource" + type: 'array' + description: 'List of user identifiers with access to the associated resource' items: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' Teams: - type: "array" - description: "List of team identifiers with access to the associated resource" + type: 'array' + description: 'List of team identifiers with access to the associated resource' items: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' SubResourceIDs: - type: "array" - description: "List of Docker resources that will inherit this access control" + type: 'array' + description: 'List of Docker resources that will inherit this access control' items: - type: "string" - example: "617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08" - description: "Docker resource identifier" + type: 'string' + example: '617c5f22bb9b023d6daab7cba43a57576f83492867bc767d1c59416b065e5f08' + description: 'Docker resource identifier' ResourceControlUpdateRequest: - type: "object" + type: 'object' properties: Public: - type: "boolean" + type: 'boolean' example: false - description: "Permit access to the associated resource to any user" + description: 'Permit access to the associated resource to any user' Users: - type: "array" - description: "List of user identifiers with access to the associated resource" + type: 'array' + description: 'List of user identifiers with access to the associated resource' items: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' Teams: - type: "array" - description: "List of team identifiers with access to the associated resource" + type: 'array' + description: 'List of team identifiers with access to the associated resource' items: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' SettingsUpdateRequest: - type: "object" + type: 'object' required: - - "TemplatesURL" - - "AuthenticationMethod" + - 'TemplatesURL' + - 'AuthenticationMethod' properties: TemplatesURL: - type: "string" - example: "https://raw.githubusercontent.com/portainer/templates/master/templates.json" + type: 'string' + example: 'https://raw.githubusercontent.com/portainer/templates/master/templates.json' description: "URL to the templates that will be displayed in the UI when navigating\ \ to App Templates" LogoURL: - type: "string" - example: "https://mycompany.mydomain.tld/logo.png" + type: 'string' + example: 'https://mycompany.mydomain.tld/logo.png' description: "URL to a logo that will be displayed on the login page as well\ \ as on top of the sidebar. Will use default Portainer logo when value is\ \ empty string" BlackListedLabels: - type: "array" + type: 'array' description: "A list of label name & value that will be used to hide containers\ \ when querying containers" items: - $ref: "#/definitions/Settings_BlackListedLabels" + $ref: '#/definitions/Settings_BlackListedLabels' DisplayExternalContributors: - type: "boolean" + type: 'boolean' example: false description: "Whether to display or not external templates contributions as\ \ sub-menus in the UI." AuthenticationMethod: - type: "integer" + type: 'integer' example: 1 - description: "Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP." + description: 'Active authentication method for the Portainer instance. Valid values are: 1 for managed or 2 for LDAP.' LDAPSettings: - $ref: "#/definitions/LDAPSettings" + $ref: '#/definitions/LDAPSettings' AllowBindMountsForRegularUsers: - type: "boolean" + type: 'boolean' example: true - description: "Whether non-administrator users should be able to use bind mounts when creating containers" + description: 'Whether non-administrator users should be able to use bind mounts when creating containers' AllowPrivilegedModeForRegularUsers: - type: "boolean" + type: 'boolean' example: true - description: "Whether non-administrator users should be able to use privileged mode when creating containers" + description: 'Whether non-administrator users should be able to use privileged mode when creating containers' EdgeAgentCheckinInterval: - type: "integer" - example: "30" - description: "Polling interval for Edge agent (in seconds)" + type: 'integer' + example: '30' + description: 'Polling interval for Edge agent (in seconds)' EndpointGroupCreateRequest: - type: "object" + type: 'object' required: - - "Name" + - 'Name' properties: Name: - type: "string" - example: "my-endpoint-group" - description: "Endpoint group name" + type: 'string' + example: 'my-endpoint-group' + description: 'Endpoint group name' Description: - type: "string" - example: "Endpoint group description" - description: "Endpoint group description" + type: 'string' + example: 'Endpoint group description' + description: 'Endpoint group description' Labels: - type: "array" + type: 'array' items: - $ref: "#/definitions/Pair" + $ref: '#/definitions/Pair' AssociatedEndpoints: - type: "array" - description: "List of endpoint identifiers that will be part of this group" + type: 'array' + description: 'List of endpoint identifiers that will be part of this group' items: - type: "integer" + type: 'integer' example: 1 - description: "Endpoint identifier" + description: 'Endpoint identifier' EndpointGroupUpdateRequest: - type: "object" + type: 'object' properties: Name: - type: "string" - example: "my-endpoint-group" - description: "Endpoint group name" + type: 'string' + example: 'my-endpoint-group' + description: 'Endpoint group name' Description: - type: "string" - example: "Endpoint group description" - description: "Endpoint group description" + type: 'string' + example: 'Endpoint group description' + description: 'Endpoint group description' Tags: - type: "array" - description: "List of tags associated to the endpoint group" + type: 'array' + description: 'List of tags associated to the endpoint group' items: - type: "string" - example: "zone/east-coast" - description: "Tag" + type: 'string' + example: 'zone/east-coast' + description: 'Tag' UserAccessPolicies: - $ref: "#/definitions/UserAccessPolicies" + $ref: '#/definitions/UserAccessPolicies' TeamAccessPolicies: - $ref: "#/definitions/TeamAccessPolicies" + $ref: '#/definitions/TeamAccessPolicies' UserCreateRequest: - type: "object" + type: 'object' required: - - "Password" - - "Role" - - "Username" + - 'Password' + - 'Role' + - 'Username' properties: Username: - type: "string" - example: "bob" - description: "Username" + type: 'string' + example: 'bob' + description: 'Username' Password: - type: "string" - example: "cg9Wgky3" - description: "Password" + type: 'string' + example: 'cg9Wgky3' + description: 'Password' Role: - type: "integer" + type: 'integer' example: 1 - description: "User role (1 for administrator account and 2 for regular account)" + description: 'User role (1 for administrator account and 2 for regular account)' UserListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/UserSubset" + $ref: '#/definitions/UserSubset' UserUpdateRequest: - type: "object" + type: 'object' properties: Password: - type: "string" - example: "cg9Wgky3" - description: "Password" + type: 'string' + example: 'cg9Wgky3' + description: 'Password' Role: - type: "integer" + type: 'integer' example: 1 - description: "User role (1 for administrator account and 2 for regular account)" + description: 'User role (1 for administrator account and 2 for regular account)' UserMembershipsResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/TeamMembership" + $ref: '#/definitions/TeamMembership' UserPasswordCheckRequest: - type: "object" + type: 'object' required: - - "Password" + - 'Password' properties: Password: - type: "string" - example: "cg9Wgky3" - description: "Password" + type: 'string' + example: 'cg9Wgky3' + description: 'Password' UserPasswordCheckResponse: - type: "object" + type: 'object' properties: valid: - type: "boolean" + type: 'boolean' example: true - description: "Is the password valid" + description: 'Is the password valid' TagListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/Tag" + $ref: '#/definitions/Tag' TagCreateRequest: - type: "object" + type: 'object' required: - - "Name" + - 'Name' properties: Name: - type: "string" - example: "org/acme" - description: "Name" + type: 'string' + example: 'org/acme' + description: 'Name' TeamCreateRequest: - type: "object" + type: 'object' required: - - "Name" + - 'Name' properties: Name: - type: "string" - example: "developers" - description: "Name" + type: 'string' + example: 'developers' + description: 'Name' TeamListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/Team" + $ref: '#/definitions/Team' TeamUpdateRequest: - type: "object" + type: 'object' required: - - "Name" + - 'Name' properties: Name: - type: "string" - example: "developers" - description: "Name" + type: 'string' + example: 'developers' + description: 'Name' TeamMembershipsResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/TeamMembership" + $ref: '#/definitions/TeamMembership' TeamMembershipCreateRequest: - type: "object" + type: 'object' required: - - "UserID" - - "TeamID" - - "Role" + - 'UserID' + - 'TeamID' + - 'Role' properties: UserID: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' TeamID: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' Role: - type: "integer" + type: 'integer' example: 1 - description: "Role for the user inside the team (1 for leader and 2 for regular member)" + description: 'Role for the user inside the team (1 for leader and 2 for regular member)' TeamMembershipListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/TeamMembership" + $ref: '#/definitions/TeamMembership' TeamMembershipUpdateRequest: - type: "object" + type: 'object' required: - - "UserID" - - "TeamID" - - "Role" + - 'UserID' + - 'TeamID' + - 'Role' properties: UserID: - type: "integer" + type: 'integer' example: 1 - description: "User identifier" + description: 'User identifier' TeamID: - type: "integer" + type: 'integer' example: 1 - description: "Team identifier" + description: 'Team identifier' Role: - type: "integer" + type: 'integer' example: 1 - description: "Role for the user inside the team (1 for leader and 2 for regular member)" + description: 'Role for the user inside the team (1 for leader and 2 for regular member)' SettingsLDAPCheckRequest: - type: "object" + type: 'object' properties: LDAPSettings: - $ref: "#/definitions/LDAPSettings" + $ref: '#/definitions/LDAPSettings' UserAdminInitRequest: - type: "object" + type: 'object' properties: Username: - type: "string" - example: "admin" - description: "Username for the admin user" + type: 'string' + example: 'admin' + description: 'Username for the admin user' Password: - type: "string" - example: "admin-password" - description: "Password for the admin user" + type: 'string' + example: 'admin-password' + description: 'Password for the admin user' TemplateListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/Template" + $ref: '#/definitions/Template' TemplateCreateRequest: - type: "object" + type: 'object' required: - - "type" - - "title" - - "description" + - 'type' + - 'title' + - 'description' properties: type: - type: "integer" + type: 'integer' example: 1 - description: "Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)" + description: 'Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)' title: - type: "string" - example: "Nginx" - description: "Title of the template" + type: 'string' + example: 'Nginx' + description: 'Title of the template' description: - type: "string" - example: "High performance web server" - description: "Description of the template" + type: 'string' + example: 'High performance web server' + description: 'Description of the template' administrator_only: - type: "boolean" + type: 'boolean' example: true - description: "Whether the template should be available to administrators only" + description: 'Whether the template should be available to administrators only' image: - type: "string" - example: "nginx:latest" - description: "Image associated to a container template. Mandatory for a container template" + type: 'string' + example: 'nginx:latest' + description: 'Image associated to a container template. Mandatory for a container template' repository: - $ref: "#/definitions/TemplateRepository" + $ref: '#/definitions/TemplateRepository' name: - type: "string" - example: "mystackname" - description: "Default name for the stack/container to be used on deployment" + type: 'string' + example: 'mystackname' + description: 'Default name for the stack/container to be used on deployment' logo: - type: "string" - example: "https://cloudinovasi.id/assets/img/logos/nginx.png" + type: 'string' + example: 'https://cloudinovasi.id/assets/img/logos/nginx.png' description: "URL of the template's logo" env: - type: "array" - description: "A list of environment variables used during the template deployment" + type: 'array' + description: 'A list of environment variables used during the template deployment' items: - $ref: "#/definitions/TemplateEnv" + $ref: '#/definitions/TemplateEnv' note: - type: "string" - example: "This is my custom template" - description: "A note that will be displayed in the UI. Supports HTML content" + type: 'string' + example: 'This is my custom template' + description: 'A note that will be displayed in the UI. Supports HTML content' platform: - type: "string" - example: "linux" + type: 'string' + example: 'linux' description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform" categories: - type: "array" - description: "A list of categories associated to the template" + type: 'array' + description: 'A list of categories associated to the template' items: - type: "string" - example: "database" + type: 'string' + example: 'database' registry: - type: "string" - example: "quay.io" - description: "The URL of a registry associated to the image for a container template" + type: 'string' + example: 'quay.io' + description: 'The URL of a registry associated to the image for a container template' command: - type: "string" - example: "ls -lah" - description: "The command that will be executed in a container template" + type: 'string' + example: 'ls -lah' + description: 'The command that will be executed in a container template' network: - type: "string" - example: "mynet" - description: "Name of a network that will be used on container deployment if it exists inside the environment" + type: 'string' + example: 'mynet' + description: 'Name of a network that will be used on container deployment if it exists inside the environment' volumes: - type: "array" - description: "A list of volumes used during the container template deployment" + type: 'array' + description: 'A list of volumes used during the container template deployment' items: - $ref: "#/definitions/TemplateVolume" + $ref: '#/definitions/TemplateVolume' ports: - type: "array" - description: "A list of ports exposed by the container" + type: 'array' + description: 'A list of ports exposed by the container' items: - type: "string" - example: "8080:80/tcp" + type: 'string' + example: '8080:80/tcp' labels: - type: "array" - description: "Container labels" + type: 'array' + description: 'Container labels' items: $ref: '#/definitions/Pair' privileged: - type: "boolean" + type: 'boolean' example: true - description: "Whether the container should be started in privileged mode" + description: 'Whether the container should be started in privileged mode' interactive: - type: "boolean" + type: 'boolean' example: true - description: "Whether the container should be started in interactive mode (-i -t equivalent on the CLI)" + description: 'Whether the container should be started in interactive mode (-i -t equivalent on the CLI)' restart_policy: - type: "string" - example: "on-failure" - description: "Container restart policy" + type: 'string' + example: 'on-failure' + description: 'Container restart policy' hostname: - type: "string" - example: "mycontainer" - description: "Container hostname" + type: 'string' + example: 'mycontainer' + description: 'Container hostname' TemplateUpdateRequest: - type: "object" + type: 'object' properties: type: - type: "integer" + type: 'integer' example: 1 - description: "Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)" + description: 'Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)' title: - type: "string" - example: "Nginx" - description: "Title of the template" + type: 'string' + example: 'Nginx' + description: 'Title of the template' description: - type: "string" - example: "High performance web server" - description: "Description of the template" + type: 'string' + example: 'High performance web server' + description: 'Description of the template' administrator_only: - type: "boolean" + type: 'boolean' example: true - description: "Whether the template should be available to administrators only" + description: 'Whether the template should be available to administrators only' image: - type: "string" - example: "nginx:latest" - description: "Image associated to a container template. Mandatory for a container template" + type: 'string' + example: 'nginx:latest' + description: 'Image associated to a container template. Mandatory for a container template' repository: - $ref: "#/definitions/TemplateRepository" + $ref: '#/definitions/TemplateRepository' name: - type: "string" - example: "mystackname" - description: "Default name for the stack/container to be used on deployment" + type: 'string' + example: 'mystackname' + description: 'Default name for the stack/container to be used on deployment' logo: - type: "string" - example: "https://cloudinovasi.id/assets/img/logos/nginx.png" + type: 'string' + example: 'https://cloudinovasi.id/assets/img/logos/nginx.png' description: "URL of the template's logo" env: - type: "array" - description: "A list of environment variables used during the template deployment" + type: 'array' + description: 'A list of environment variables used during the template deployment' items: - $ref: "#/definitions/TemplateEnv" + $ref: '#/definitions/TemplateEnv' note: - type: "string" - example: "This is my custom template" - description: "A note that will be displayed in the UI. Supports HTML content" + type: 'string' + example: 'This is my custom template' + description: 'A note that will be displayed in the UI. Supports HTML content' platform: - type: "string" - example: "linux" + type: 'string' + example: 'linux' description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform" categories: - type: "array" - description: "A list of categories associated to the template" + type: 'array' + description: 'A list of categories associated to the template' items: - type: "string" - example: "database" + type: 'string' + example: 'database' registry: - type: "string" - example: "quay.io" - description: "The URL of a registry associated to the image for a container template" + type: 'string' + example: 'quay.io' + description: 'The URL of a registry associated to the image for a container template' command: - type: "string" - example: "ls -lah" - description: "The command that will be executed in a container template" + type: 'string' + example: 'ls -lah' + description: 'The command that will be executed in a container template' network: - type: "string" - example: "mynet" - description: "Name of a network that will be used on container deployment if it exists inside the environment" + type: 'string' + example: 'mynet' + description: 'Name of a network that will be used on container deployment if it exists inside the environment' volumes: - type: "array" - description: "A list of volumes used during the container template deployment" + type: 'array' + description: 'A list of volumes used during the container template deployment' items: - $ref: "#/definitions/TemplateVolume" + $ref: '#/definitions/TemplateVolume' ports: - type: "array" - description: "A list of ports exposed by the container" + type: 'array' + description: 'A list of ports exposed by the container' items: - type: "string" - example: "8080:80/tcp" + type: 'string' + example: '8080:80/tcp' labels: - type: "array" - description: "Container labels" + type: 'array' + description: 'Container labels' items: $ref: '#/definitions/Pair' privileged: - type: "boolean" + type: 'boolean' example: true - description: "Whether the container should be started in privileged mode" + description: 'Whether the container should be started in privileged mode' interactive: - type: "boolean" + type: 'boolean' example: true - description: "Whether the container should be started in interactive mode (-i -t equivalent on the CLI)" + description: 'Whether the container should be started in interactive mode (-i -t equivalent on the CLI)' restart_policy: - type: "string" - example: "on-failure" - description: "Container restart policy" + type: 'string' + example: 'on-failure' + description: 'Container restart policy' hostname: - type: "string" - example: "mycontainer" - description: "Container hostname" + type: 'string' + example: 'mycontainer' + description: 'Container hostname' Template: - type: "object" + type: 'object' properties: id: - type: "integer" + type: 'integer' example: 1 - description: "Template identifier" + description: 'Template identifier' type: - type: "integer" + type: 'integer' example: 1 - description: "Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)" + description: 'Template type. Valid values are: 1 (container), 2 (Swarm stack) or 3 (Compose stack)' title: - type: "string" - example: "Nginx" - description: "Title of the template" + type: 'string' + example: 'Nginx' + description: 'Title of the template' description: - type: "string" - example: "High performance web server" - description: "Description of the template" + type: 'string' + example: 'High performance web server' + description: 'Description of the template' administrator_only: - type: "boolean" + type: 'boolean' example: true - description: "Whether the template should be available to administrators only" + description: 'Whether the template should be available to administrators only' image: - type: "string" - example: "nginx:latest" - description: "Image associated to a container template. Mandatory for a container template" + type: 'string' + example: 'nginx:latest' + description: 'Image associated to a container template. Mandatory for a container template' repository: - $ref: "#/definitions/TemplateRepository" + $ref: '#/definitions/TemplateRepository' name: - type: "string" - example: "mystackname" - description: "Default name for the stack/container to be used on deployment" + type: 'string' + example: 'mystackname' + description: 'Default name for the stack/container to be used on deployment' logo: - type: "string" - example: "https://cloudinovasi.id/assets/img/logos/nginx.png" + type: 'string' + example: 'https://cloudinovasi.id/assets/img/logos/nginx.png' description: "URL of the template's logo" env: - type: "array" - description: "A list of environment variables used during the template deployment" + type: 'array' + description: 'A list of environment variables used during the template deployment' items: - $ref: "#/definitions/TemplateEnv" + $ref: '#/definitions/TemplateEnv' note: - type: "string" - example: "This is my custom template" - description: "A note that will be displayed in the UI. Supports HTML content" + type: 'string' + example: 'This is my custom template' + description: 'A note that will be displayed in the UI. Supports HTML content' platform: - type: "string" - example: "linux" + type: 'string' + example: 'linux' description: "Platform associated to the template. Valid values are: 'linux', 'windows' or leave empty for multi-platform" categories: - type: "array" - description: "A list of categories associated to the template" + type: 'array' + description: 'A list of categories associated to the template' items: - type: "string" - example: "database" + type: 'string' + example: 'database' registry: - type: "string" - example: "quay.io" - description: "The URL of a registry associated to the image for a container template" + type: 'string' + example: 'quay.io' + description: 'The URL of a registry associated to the image for a container template' command: - type: "string" - example: "ls -lah" - description: "The command that will be executed in a container template" + type: 'string' + example: 'ls -lah' + description: 'The command that will be executed in a container template' network: - type: "string" - example: "mynet" - description: "Name of a network that will be used on container deployment if it exists inside the environment" + type: 'string' + example: 'mynet' + description: 'Name of a network that will be used on container deployment if it exists inside the environment' volumes: - type: "array" - description: "A list of volumes used during the container template deployment" + type: 'array' + description: 'A list of volumes used during the container template deployment' items: - $ref: "#/definitions/TemplateVolume" + $ref: '#/definitions/TemplateVolume' ports: - type: "array" - description: "A list of ports exposed by the container" + type: 'array' + description: 'A list of ports exposed by the container' items: - type: "string" - example: "8080:80/tcp" + type: 'string' + example: '8080:80/tcp' labels: - type: "array" - description: "Container labels" + type: 'array' + description: 'Container labels' items: $ref: '#/definitions/Pair' privileged: - type: "boolean" + type: 'boolean' example: true - description: "Whether the container should be started in privileged mode" + description: 'Whether the container should be started in privileged mode' interactive: - type: "boolean" + type: 'boolean' example: true - description: "Whether the container should be started in interactive mode (-i -t equivalent on the CLI)" + description: 'Whether the container should be started in interactive mode (-i -t equivalent on the CLI)' restart_policy: - type: "string" - example: "on-failure" - description: "Container restart policy" + type: 'string' + example: 'on-failure' + description: 'Container restart policy' hostname: - type: "string" - example: "mycontainer" - description: "Container hostname" + type: 'string' + example: 'mycontainer' + description: 'Container hostname' TemplateVolume: - type: "object" + type: 'object' properties: container: - type: "string" - example: "/data" - description: "Path inside the container" + type: 'string' + example: '/data' + description: 'Path inside the container' bind: - type: "string" - example: "/tmp" - description: "Path on the host" + type: 'string' + example: '/tmp' + description: 'Path on the host' readonly: - type: "boolean" + type: 'boolean' example: true - description: "Whether the volume used should be readonly" + description: 'Whether the volume used should be readonly' TemplateEnv: - type: "object" + type: 'object' properties: name: - type: "string" - example: "MYSQL_ROOT_PASSWORD" - description: "name of the environment variable" + type: 'string' + example: 'MYSQL_ROOT_PASSWORD' + description: 'name of the environment variable' label: - type: "string" - example: "Root password" - description: "Text for the label that will be generated in the UI" + type: 'string' + example: 'Root password' + description: 'Text for the label that will be generated in the UI' description: - type: "string" - example: "MySQL root account password" - description: "Content of the tooltip that will be generated in the UI" + type: 'string' + example: 'MySQL root account password' + description: 'Content of the tooltip that will be generated in the UI' default: - type: "string" - example: "default_value" - description: "Default value that will be set for the variable" + type: 'string' + example: 'default_value' + description: 'Default value that will be set for the variable' preset: - type: "boolean" + type: 'boolean' example: true - description: "If set to true, will not generate any input for this variable in the UI" + description: 'If set to true, will not generate any input for this variable in the UI' select: - type: "array" - description: "A list of name/value that will be used to generate a dropdown in the UI" + type: 'array' + description: 'A list of name/value that will be used to generate a dropdown in the UI' items: $ref: '#/definitions/TemplateEnvSelect' TemplateEnvSelect: - type: "object" + type: 'object' properties: text: - type: "string" - example: "text value" - description: "Some text that will displayed as a choice" + type: 'string' + example: 'text value' + description: 'Some text that will displayed as a choice' value: - type: "string" - example: "value" - description: "A value that will be associated to the choice" + type: 'string' + example: 'value' + description: 'A value that will be associated to the choice' default: - type: "boolean" + type: 'boolean' example: true - description: "Will set this choice as the default choice" + description: 'Will set this choice as the default choice' TemplateRepository: - type: "object" + type: 'object' required: - - "URL" + - 'URL' properties: URL: - type: "string" - example: "https://github.com/portainer/portainer-compose" - description: "URL of a git repository used to deploy a stack template. Mandatory for a Swarm/Compose stack template" + type: 'string' + example: 'https://github.com/portainer/portainer-compose' + description: 'URL of a git repository used to deploy a stack template. Mandatory for a Swarm/Compose stack template' stackfile: - type: "string" - example: "./subfolder/docker-compose.yml" - description: "Path to the stack file inside the git repository" + type: 'string' + example: './subfolder/docker-compose.yml' + description: 'Path to the stack file inside the git repository' StackMigrateRequest: - type: "object" + type: 'object' required: - - "EndpointID" + - 'EndpointID' properties: EndpointID: - type: "integer" + type: 'integer' example: 2 - description: "Endpoint identifier of the target endpoint where the stack will be relocated" + description: 'Endpoint identifier of the target endpoint where the stack will be relocated' SwarmID: - type: "string" - example: "jpofkc0i9uo9wtx1zesuk649w" - description: "Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated" + type: 'string' + example: 'jpofkc0i9uo9wtx1zesuk649w' + description: 'Swarm cluster identifier, must match the identifier of the cluster where the stack will be relocated' Name: - type: "string" - example: "new-stack" - description: "If provided will rename the migrated stack" + type: 'string' + example: 'new-stack' + description: 'If provided will rename the migrated stack' EndpointJobRequest: - type: "object" + type: 'object' required: - - "Image" - - "FileContent" + - 'Image' + - 'FileContent' properties: Image: - type: "string" - example: "ubuntu:latest" - description: "Container image which will be used to execute the job" + type: 'string' + example: 'ubuntu:latest' + description: 'Container image which will be used to execute the job' FileContent: - type: "string" - example: "ls -lah /host/tmp" - description: "Content of the job script" + type: 'string' + example: 'ls -lah /host/tmp' + description: 'Content of the job script' StackCreateRequest: - type: "object" + type: 'object' required: - - "Name" + - 'Name' properties: Name: - type: "string" - example: "myStack" - description: "Name of the stack" + type: 'string' + example: 'myStack' + description: 'Name of the stack' SwarmID: - type: "string" - example: "jpofkc0i9uo9wtx1zesuk649w" - description: "Swarm cluster identifier. Required when creating a Swarm stack (type 1)." + type: 'string' + example: 'jpofkc0i9uo9wtx1zesuk649w' + description: 'Swarm cluster identifier. Required when creating a Swarm stack (type 1).' StackFileContent: - type: "string" + type: 'string' example: "version: 3\n services:\n web:\n image:nginx" description: "Content of the Stack file. Required when using the 'string' deployment method." RepositoryURL: - type: "string" - example: "https://github.com/openfaas/faas" + type: 'string' + example: 'https://github.com/openfaas/faas' description: "URL of a Git repository hosting the Stack file. Required when using the 'repository' deployment method." RepositoryReferenceName: - type: "string" - example: "refs/heads/master" + type: 'string' + example: 'refs/heads/master' description: "Reference name of a Git repository hosting the Stack file. Used in 'repository' deployment method." ComposeFilePathInRepository: - type: "string" - example: "docker-compose.yml" + type: 'string' + example: 'docker-compose.yml' description: "Path to the Stack file inside the Git repository. Will default to 'docker-compose.yml' if not specified." RepositoryAuthentication: - type: "boolean" + type: 'boolean' example: true - description: "Use basic authentication to clone the Git repository." + description: 'Use basic authentication to clone the Git repository.' RepositoryUsername: - type: "string" - example: "myGitUsername" - description: "Username used in basic authentication. Required when RepositoryAuthentication is true." + type: 'string' + example: 'myGitUsername' + description: 'Username used in basic authentication. Required when RepositoryAuthentication is true.' RepositoryPassword: - type: "string" - example: "myGitPassword" - description: "Password used in basic authentication. Required when RepositoryAuthentication is true." + type: 'string' + example: 'myGitPassword' + description: 'Password used in basic authentication. Required when RepositoryAuthentication is true.' Env: - type: "array" - description: "A list of environment variables used during stack deployment" + type: 'array' + description: 'A list of environment variables used during stack deployment' items: - $ref: "#/definitions/Stack_Env" + $ref: '#/definitions/Stack_Env' Stack_Env: properties: name: - type: "string" - example: "MYSQL_ROOT_PASSWORD" + type: 'string' + example: 'MYSQL_ROOT_PASSWORD' value: - type: "string" - example: "password" + type: 'string' + example: 'password' StackListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/Stack" + $ref: '#/definitions/Stack' Stack: - type: "object" + type: 'object' properties: Id: - type: "string" - example: "myStack_jpofkc0i9uo9wtx1zesuk649w" - description: "Stack identifier" + type: 'string' + example: 'myStack_jpofkc0i9uo9wtx1zesuk649w' + description: 'Stack identifier' Name: - type: "string" - example: "myStack" - description: "Stack name" + type: 'string' + example: 'myStack' + description: 'Stack name' Type: - type: "integer" - example: "1" - description: "Stack type. 1 for a Swarm stack, 2 for a Compose stack" + type: 'integer' + example: '1' + description: 'Stack type. 1 for a Swarm stack, 2 for a Compose stack' EndpointID: - type: "integer" - example: "1" - description: "Endpoint identifier. Reference the endpoint that will be used for deployment " + type: 'integer' + example: '1' + description: 'Endpoint identifier. Reference the endpoint that will be used for deployment ' EntryPoint: - type: "string" - example: "docker-compose.yml" - description: "Path to the Stack file" + type: 'string' + example: 'docker-compose.yml' + description: 'Path to the Stack file' SwarmID: - type: "string" - example: "jpofkc0i9uo9wtx1zesuk649w" - description: "Cluster identifier of the Swarm cluster where the stack is deployed" + type: 'string' + example: 'jpofkc0i9uo9wtx1zesuk649w' + description: 'Cluster identifier of the Swarm cluster where the stack is deployed' ProjectPath: - type: "string" - example: "/data/compose/myStack_jpofkc0i9uo9wtx1zesuk649w" - description: "Path on disk to the repository hosting the Stack file" + type: 'string' + example: '/data/compose/myStack_jpofkc0i9uo9wtx1zesuk649w' + description: 'Path on disk to the repository hosting the Stack file' Env: - type: "array" - description: "A list of environment variables used during stack deployment" + type: 'array' + description: 'A list of environment variables used during stack deployment' items: - $ref: "#/definitions/Stack_Env" + $ref: '#/definitions/Stack_Env' StackUpdateRequest: - type: "object" + type: 'object' properties: StackFileContent: - type: "string" + type: 'string' example: "version: 3\n services:\n web:\n image:nginx" - description: "New content of the Stack file." + description: 'New content of the Stack file.' Env: - type: "array" - description: "A list of environment variables used during stack deployment" + type: 'array' + description: 'A list of environment variables used during stack deployment' items: - $ref: "#/definitions/Stack_Env" + $ref: '#/definitions/Stack_Env' Prune: - type: "boolean" + type: 'boolean' example: false - description: "Prune services that are no longer referenced (only available for Swarm stacks)" + description: 'Prune services that are no longer referenced (only available for Swarm stacks)' StackFileInspectResponse: - type: "object" + type: 'object' properties: StackFileContent: - type: "string" + type: 'string' example: "version: 3\n services:\n web:\n image:nginx" - description: "Content of the Stack file." + description: 'Content of the Stack file.' LicenseInformation: - type: "object" + type: 'object' properties: LicenseKey: - type: "string" - description: "License key" - example: "1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ" + type: 'string' + description: 'License key' + example: '1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ' Company: - type: "string" - description: "Company associated to the license" - example: "Portainer.io" + type: 'string' + description: 'Company associated to the license' + example: 'Portainer.io' Expiration: - type: "string" - description: "License expiry date" - example: "2077-07-07" + type: 'string' + description: 'License expiry date' + example: '2077-07-07' Valid: - type: "boolean" - description: "Is the license valid" - example: "true" + type: 'boolean' + description: 'Is the license valid' + example: 'true' Extension: - type: "object" + type: 'object' properties: Id: - type: "integer" + type: 'integer' example: 1 - description: "Extension identifier" + description: 'Extension identifier' Name: - type: "string" - example: "Registry Manager" - description: "Extension name" + type: 'string' + example: 'Registry Manager' + description: 'Extension name' Enabled: - type: "boolean" - example: "true" - description: "Is the extension enabled" + type: 'boolean' + example: 'true' + description: 'Is the extension enabled' ShortDescription: - type: "string" - description: "Short description about the extension" - example: "Enable in-app registry management" + type: 'string' + description: 'Short description about the extension' + example: 'Enable in-app registry management' DescriptionURL: - type: "string" - description: "URL to the file containing the extension description" + type: 'string' + description: 'URL to the file containing the extension description' example: https://portainer-io-assets.sfo2.digitaloceanspaces.com/description_registry_manager.html" Available: - type: "boolean" - description: "Is the extension available for download and activation" - example: "true" + type: 'boolean' + description: 'Is the extension available for download and activation' + example: 'true' Images: - type: "array" - description: "List of screenshot URLs" + type: 'array' + description: 'List of screenshot URLs' items: - type: "string" - example: "https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/rm01.png" - description: "Screenshot URL" + type: 'string' + example: 'https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/rm01.png' + description: 'Screenshot URL' Logo: - type: "string" - description: "Icon associated to the extension" - example: "fa-database" + type: 'string' + description: 'Icon associated to the extension' + example: 'fa-database' Price: - type: "string" - description: "Extension price" - example: "US$9.95" + type: 'string' + description: 'Extension price' + example: 'US$9.95' PriceDescription: - type: "string" - description: "Details about extension pricing" - example: "Price per instance per year" + type: 'string' + description: 'Details about extension pricing' + example: 'Price per instance per year' ShopURL: - type: "string" - description: "URL used to buy the extension" - example: "https://portainer.io/checkout/?add-to-cart=1164" + type: 'string' + description: 'URL used to buy the extension' + example: 'https://portainer.io/checkout/?add-to-cart=1164' UpdateAvailable: - type: "boolean" - description: "Is an update available for this extension" - example: "true" + type: 'boolean' + description: 'Is an update available for this extension' + example: 'true' Version: - type: "string" - description: "Extension version" - example: "1.0.0" + type: 'string' + description: 'Extension version' + example: '1.0.0' License: - $ref: "#/definitions/LicenseInformation" + $ref: '#/definitions/LicenseInformation' ExtensionListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/Extension" + $ref: '#/definitions/Extension' ExtensionCreateRequest: - type: "object" + type: 'object' required: - - "License" + - 'License' properties: License: - type: "string" - example: "1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ" - description: "License key" + type: 'string' + example: '1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ' + description: 'License key' ExtensionUpdateRequest: - type: "object" + type: 'object' required: - - "Version" + - 'Version' properties: Version: - type: "string" - example: "1.1.0" - description: "New version of the extension" + type: 'string' + example: '1.1.0' + description: 'New version of the extension' RoleListResponse: - type: "array" + type: 'array' items: - $ref: "#/definitions/Role" + $ref: '#/definitions/Role' Role: - type: "object" + type: 'object' properties: Id: - type: "integer" - description: "Role identifier" + type: 'integer' + description: 'Role identifier' example: 2 Name: - type: "string" - description: "Role name" - example: "HelpDesk" + type: 'string' + description: 'Role name' + example: 'HelpDesk' Description: - type: "string" - description: "Role description" - example: "Read-only access of all resources in an endpoint" + type: 'string' + description: 'Role description' + example: 'Read-only access of all resources in an endpoint' Authorizations: - $ref: "#/definitions/Authorizations" + $ref: '#/definitions/Authorizations' Authorizations: - type: "object" - description: "Authorizations associated to a role" + type: 'object' + description: 'Authorizations associated to a role' additionalProperties: - type: "object" + type: 'object' properties: authorization: - type: "string" + type: 'string' value: - type: "boolean" + type: 'boolean' example: - "DockerContainerList": true - "DockerVolumeList": true + 'DockerContainerList': true + 'DockerVolumeList': true diff --git a/app/__module.js b/app/__module.js index 489ebe0a3..87b5ef593 100644 --- a/app/__module.js +++ b/app/__module.js @@ -3,6 +3,9 @@ import '@babel/polyfill'; import angular from 'angular'; +import './matomo-setup'; +import './assets/js/angulartics-matomo'; + import './agent'; import './azure/_module'; import './docker/__module'; @@ -21,7 +24,6 @@ angular.module('portainer', [ 'angularUtils.directives.dirPagination', 'LocalStorageModule', 'angular-jwt', - 'angular-google-analytics', 'angular-json-tree', 'angular-loading-bar', 'angular-clipboard', @@ -37,6 +39,8 @@ angular.module('portainer', [ 'portainer.integrations', 'rzModule', 'moment-picker', + 'angulartics', + 'angulartics.matomo', ]); if (require) { diff --git a/app/assets/js/angulartics-matomo.js b/app/assets/js/angulartics-matomo.js new file mode 100644 index 000000000..1b6211ca8 --- /dev/null +++ b/app/assets/js/angulartics-matomo.js @@ -0,0 +1,223 @@ +import angular from 'angular'; + +// forked from https://github.com/angulartics/angulartics-piwik/blob/master/src/angulartics-piwik.js + +/* global _paq */ +/** + * @ngdoc overview + * @name angulartics.piwik + * Enables analytics support for Piwik/Matomo (http://piwik.org/docs/tracking-api/) + */ +angular.module('angulartics.matomo', ['angulartics']).config([ + '$analyticsProvider', + '$windowProvider', + function ($analyticsProvider, $windowProvider) { + var $window = $windowProvider.$get(); + + $analyticsProvider.settings.pageTracking.trackRelativePath = true; + + // Add piwik specific trackers to angulartics API + + // Requires the CustomDimensions plugin for Piwik. + $analyticsProvider.api.setCustomDimension = function (dimensionId, value) { + if ($window._paq) { + $window._paq.push(['setCustomDimension', dimensionId, value]); + } + }; + + // Requires the CustomDimensions plugin for Piwik. + $analyticsProvider.api.deleteCustomDimension = function (dimensionId) { + if ($window._paq) { + $window._paq.push(['deleteCustomDimension', dimensionId]); + } + }; + + // scope: visit or page. Defaults to 'page' + $analyticsProvider.api.setCustomVariable = function (varIndex, varName, value, scope) { + if ($window._paq) { + scope = scope || 'page'; + $window._paq.push(['setCustomVariable', varIndex, varName, value, scope]); + } + }; + + // scope: visit or page. Defaults to 'page' + $analyticsProvider.api.deleteCustomVariable = function (varIndex, scope) { + if ($window._paq) { + scope = scope || 'page'; + $window._paq.push(['deleteCustomVariable', varIndex, scope]); + } + }; + + // trackSiteSearch(keyword, category, [searchCount]) + $analyticsProvider.api.trackSiteSearch = function (keyword, category, searchCount) { + // keyword is required + if ($window._paq && keyword) { + var params = ['trackSiteSearch', keyword, category || false]; + + // searchCount is optional + if (angular.isDefined(searchCount)) { + params.push(searchCount); + } + + $window._paq.push(params); + } + }; + + // logs a conversion for goal 1. revenue is optional + // trackGoal(goalID, [revenue]); + $analyticsProvider.api.trackGoal = function (goalID, revenue) { + if ($window._paq) { + _paq.push(['trackGoal', goalID, revenue || 0]); + } + }; + + // track outlink or download + // linkType is 'link' or 'download', 'link' by default + // trackLink(url, [linkType]); + $analyticsProvider.api.trackLink = function (url, linkType) { + var type = linkType || 'link'; + if ($window._paq) { + $window._paq.push(['trackLink', url, type]); + } + }; + + // Set default angulartics page and event tracking + + $analyticsProvider.registerSetUsername(function (username) { + if ($window._paq) { + $window._paq.push(['setUserId', username]); + } + }); + + // locationObj is the angular $location object + $analyticsProvider.registerPageTrack(function (path) { + if ($window._paq) { + $window._paq.push(['setDocumentTitle', $window.document.title]); + $window._paq.push(['setReferrerUrl', '']); + $window._paq.push(['setCustomUrl', 'http://portainer-ce.app' + path]); + $window._paq.push(['trackPageView']); + } + }); + + /** + * @name eventTrack + * Track a basic event in Piwik, or send an ecommerce event. + * + * @param {string} action A string corresponding to the type of event that needs to be tracked. + * @param {object} properties The properties that need to be logged with the event. + */ + $analyticsProvider.registerEventTrack(function (action, properties) { + if ($window._paq) { + properties = properties || {}; + + switch (action) { + /** + * @description Sets the current page view as a product or category page view. When you call + * setEcommerceView it must be followed by a call to trackPageView to record the product or + * category page view. + * + * @link https://piwik.org/docs/ecommerce-analytics/#tracking-product-page-views-category-page-views-optional + * @link https://developer.piwik.org/api-reference/tracking-javascript#ecommerce + * + * @property productSKU (required) SKU: Product unique identifier + * @property productName (optional) Product name + * @property categoryName (optional) Product category, or array of up to 5 categories + * @property price (optional) Product Price as displayed on the page + */ + case 'setEcommerceView': + $window._paq.push(['setEcommerceView', properties.productSKU, properties.productName, properties.categoryName, properties.price]); + break; + + /** + * @description Adds a product into the ecommerce order. Must be called for each product in + * the order. + * + * @link https://piwik.org/docs/ecommerce-analytics/#tracking-ecommerce-orders-items-purchased-required + * @link https://developer.piwik.org/api-reference/tracking-javascript#ecommerce + * + * @property productSKU (required) SKU: Product unique identifier + * @property productName (optional) Product name + * @property categoryName (optional) Product category, or array of up to 5 categories + * @property price (recommended) Product price + * @property quantity (optional, default to 1) Product quantity + */ + case 'addEcommerceItem': + $window._paq.push(['addEcommerceItem', properties.productSKU, properties.productName, properties.productCategory, properties.price, properties.quantity]); + break; + + /** + * @description Tracks a shopping cart. Call this javascript function every time a user is + * adding, updating or deleting a product from the cart. + * + * @link https://piwik.org/docs/ecommerce-analytics/#tracking-add-to-cart-items-added-to-the-cart-optional + * @link https://developer.piwik.org/api-reference/tracking-javascript#ecommerce + * + * @property grandTotal (required) Cart amount + */ + case 'trackEcommerceCartUpdate': + $window._paq.push(['trackEcommerceCartUpdate', properties.grandTotal]); + break; + + /** + * @description Tracks an Ecommerce order, including any ecommerce item previously added to + * the order. orderId and grandTotal (ie. revenue) are required parameters. + * + * @link https://piwik.org/docs/ecommerce-analytics/#tracking-ecommerce-orders-items-purchased-required + * @link https://developer.piwik.org/api-reference/tracking-javascript#ecommerce + * + * @property orderId (required) Unique Order ID + * @property grandTotal (required) Order Revenue grand total (includes tax, shipping, and subtracted discount) + * @property subTotal (optional) Order sub total (excludes shipping) + * @property tax (optional) Tax amount + * @property shipping (optional) Shipping amount + * @property discount (optional) Discount offered (set to false for unspecified parameter) + */ + case 'trackEcommerceOrder': + $window._paq.push(['trackEcommerceOrder', properties.orderId, properties.grandTotal, properties.subTotal, properties.tax, properties.shipping, properties.discount]); + break; + + /** + * @description Logs an event with an event category (Videos, Music, Games...), an event + * action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...), and an optional + * event name and optional numeric value. + * + * @link https://piwik.org/docs/event-tracking/ + * @link https://developer.piwik.org/api-reference/tracking-javascript#using-the-tracker-object + * + * @property category + * @property action + * @property name (optional, recommended) + * @property value (optional) + */ + default: + // PAQ requires that eventValue be an integer, see: http://piwik.org/docs/event-tracking + if (properties.value) { + var parsed = parseInt(properties.value, 10); + properties.value = isNaN(parsed) ? 0 : parsed; + } + + $window._paq.push([ + 'trackEvent', + properties.category, + action, + properties.name || properties.label, // Changed in favour of Piwik documentation. Added fallback so it's backwards compatible. + properties.value, + ]); + } + } + }); + + /** + * @name exceptionTrack + * Sugar on top of the eventTrack method for easily handling errors + * + * @param {object} error An Error object to track: error.toString() used for event 'action', error.stack used for event 'label'. + * @param {object} cause The cause of the error given from $exceptionHandler, not used. + */ + $analyticsProvider.registerExceptionTrack(function (error) { + if ($window._paq) { + $window._paq.push(['trackEvent', 'Exceptions', error.toString(), error.stack, 0]); + } + }); + }, +]); diff --git a/app/config.js b/app/config.js index 0f8d8234e..5e74a916d 100644 --- a/app/config.js +++ b/app/config.js @@ -7,11 +7,10 @@ angular.module('portainer').config([ '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', - 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', 'cfpLoadingBarProvider', - function ($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) { + function ($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) { 'use strict'; var environment = '@@ENVIRONMENT'; @@ -52,9 +51,6 @@ angular.module('portainer').config([ }, ]); - AnalyticsProvider.setAccount({ tracker: __CONFIG_GA_ID, set: { anonymizeIp: true } }); - AnalyticsProvider.startOffline(true); - toastr.options.timeOut = 3000; Terminal.applyAddon(fit); diff --git a/app/matomo-setup.js b/app/matomo-setup.js new file mode 100644 index 000000000..b85aa4480 --- /dev/null +++ b/app/matomo-setup.js @@ -0,0 +1,14 @@ +const _paq = (window._paq = window._paq || []); +/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ +_paq.push(['enableLinkTracking']); + +var u = 'https://portainer-ce.matomo.cloud/'; +_paq.push(['setTrackerUrl', u + 'matomo.php']); +_paq.push(['setSiteId', '1']); +var d = document, + g = d.createElement('script'), + s = d.getElementsByTagName('script')[0]; +g.type = 'text/javascript'; +g.async = true; +g.src = '//cdn.matomo.cloud/portainer-ce.matomo.cloud/matomo.js'; +s.parentNode.insertBefore(g, s); diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 9bb8fb74b..b312bcd40 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -15,16 +15,6 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat await Authentication.init(); } -function initAnalytics(Analytics, $rootScope) { - Analytics.offline(false); - Analytics.registerScriptTags(); - Analytics.registerTrackers(); - $rootScope.$on('$stateChangeSuccess', function (event, toState) { - Analytics.trackPage(toState.url); - Analytics.pageView(); - }); -} - angular.module('portainer.app', ['portainer.oauth']).config([ '$stateRegistryProvider', function ($stateRegistryProvider) { @@ -38,23 +28,19 @@ angular.module('portainer.app', ['portainer.oauth']).config([ 'StateManager', 'Authentication', 'Notifications', - 'Analytics', 'authManager', '$rootScope', '$state', '$async', '$q', - (StateManager, Authentication, Notifications, Analytics, authManager, $rootScope, $state, $async, $q) => { + (StateManager, Authentication, Notifications, authManager, $rootScope, $state, $async, $q) => { const deferred = $q.defer(); const appState = StateManager.getState(); if (!appState.loading) { deferred.resolve(); } else { StateManager.initialize() - .then(function success(state) { - if (state.application.analytics) { - initAnalytics(Analytics, $rootScope); - } + .then(function success() { return $async(initAuthentication, authManager, Authentication, $rootScope, $state); }) .then(() => deferred.resolve()) diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index bec5e7894..40f0003f4 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -17,6 +17,7 @@ export function SettingsViewModel(data) { this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; this.UserSessionTimeout = data.UserSessionTimeout; + this.EnableTelemetry = data.EnableTelemetry; } export function PublicSettingsViewModel(settings) { @@ -32,6 +33,7 @@ export function PublicSettingsViewModel(settings) { this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; this.LogoURL = settings.LogoURL; this.OAuthLoginURI = settings.OAuthLoginURI; + this.EnableTelemetry = settings.EnableTelemetry; } export function LDAPSettingsViewModel(data) { diff --git a/app/portainer/models/status.js b/app/portainer/models/status.js index 20057e382..96652cd6d 100644 --- a/app/portainer/models/status.js +++ b/app/portainer/models/status.js @@ -1,7 +1,6 @@ export function StatusViewModel(data) { this.Authentication = data.Authentication; this.Snapshot = data.Snapshot; - this.Analytics = data.Analytics; this.Version = data.Version; } diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 7b28dfc86..600ee21a5 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -11,7 +11,19 @@ angular.module('portainer.app').factory('StateManager', [ 'StatusService', 'APPLICATION_CACHE_VALIDITY', 'AgentPingService', - function StateManagerFactory($q, SystemService, InfoHelper, EndpointProvider, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY, AgentPingService) { + '$analytics', + function StateManagerFactory( + $q, + SystemService, + InfoHelper, + EndpointProvider, + LocalStorage, + SettingsService, + StatusService, + APPLICATION_CACHE_VALIDITY, + AgentPingService, + $analytics + ) { 'use strict'; var manager = {}; @@ -106,9 +118,15 @@ angular.module('portainer.app').factory('StateManager', [ LocalStorage.storeApplicationState(state.application); }; + manager.updateEnableTelemetry = function updateEnableTelemetry(enableTelemetry) { + state.application.enableTelemetry = enableTelemetry; + $analytics.setOptOut(!enableTelemetry); + LocalStorage.storeApplicationState(state.application); + }; + function assignStateFromStatusAndSettings(status, settings) { - state.application.analytics = status.Analytics; state.application.version = status.Version; + state.application.enableTelemetry = settings.EnableTelemetry; state.application.logo = settings.LogoURL; state.application.snapshotInterval = settings.SnapshotInterval; state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures; @@ -134,6 +152,7 @@ angular.module('portainer.app').factory('StateManager', [ var status = data.status; var settings = data.settings; assignStateFromStatusAndSettings(status, settings); + $analytics.setOptOut(!settings.EnableTelemetry); LocalStorage.storeApplicationState(state.application); deferred.resolve(state); }) @@ -176,6 +195,7 @@ angular.module('portainer.app').factory('StateManager', [ } else { state.application = applicationState; state.loading = false; + $analytics.setOptOut(!state.application.enableTelemetry); deferred.resolve(state); } } else { diff --git a/app/portainer/views/about/about.html b/app/portainer/views/about/about.html index ef609050a..e58332715 100644 --- a/app/portainer/views/about/about.html +++ b/app/portainer/views/about/about.html @@ -29,20 +29,16 @@
    Opt-out
      -
    • You may opt-out by passing the --no-analytics flag as part of the docker run command when starting Portainer.
    • -
    • If you believe that we could improve our analytics approach make sure to let us know! There is an open discussion on - Github
    • +
    • You may opt-out by turning off the analytics in the settings page.
    What we collect & GDPR
      -
    • We dont know who uses Portainer, where its used, to what scale its used, all we know (from analytics) is how often Portainer is used and which pages within the app - are most frequently used.
    • +
    • + We don't know who uses Portainer, where its used, to what scale its used, all we know (from analytics) is how often Portainer is used and which pages within the app + are most frequently used. +
    • As we are only collecting a very small amount of totally anonymous data, it is deemed that opt-in is not required.
    diff --git a/app/portainer/views/init/admin/initAdmin.html b/app/portainer/views/init/admin/initAdmin.html index aa5a5129d..b3643ea58 100644 --- a/app/portainer/views/init/admin/initAdmin.html +++ b/app/portainer/views/init/admin/initAdmin.html @@ -82,6 +82,17 @@
    + +
    +
    + + Allow collection of anonymous statistics. You can find more information about this in our + privacy policy. +
    +
    +
    diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js index 3bc8f1c9f..1f7826019 100644 --- a/app/portainer/views/init/admin/initAdminController.js +++ b/app/portainer/views/init/admin/initAdminController.js @@ -5,16 +5,18 @@ angular.module('portainer.app').controller('InitAdminController', [ 'Notifications', 'Authentication', 'StateManager', + 'SettingsService', 'UserService', 'EndpointService', 'ExtensionService', - function ($async, $scope, $state, Notifications, Authentication, StateManager, UserService, EndpointService, ExtensionService) { + function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, ExtensionService) { $scope.logo = StateManager.getState().application.logo; $scope.formValues = { Username: 'admin', Password: '', ConfirmPassword: '', + enableTelemetry: true, }; $scope.state = { @@ -45,6 +47,10 @@ angular.module('portainer.app').controller('InitAdminController', [ .then(function success() { return retrieveAndSaveEnabledExtensions(); }) + .then(function success() { + StateManager.updateEnableTelemetry($scope.formValues.enableTelemetry); + return SettingsService.update({ enableTelemetry: $scope.formValues.enableTelemetry }); + }) .then(function () { return EndpointService.endpoints(0, 100); }) diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index b9e3c97cf..d5091ef43 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -26,6 +26,18 @@
    +
    +
    + + +
    +
    + You can find more information about this in our + privacy policy. +
    +
    diff --git a/app/portainer/views/settings/settingsController.js b/app/portainer/views/settings/settingsController.js index 09e6f862a..5bd78d9f0 100644 --- a/app/portainer/views/settings/settingsController.js +++ b/app/portainer/views/settings/settingsController.js @@ -36,6 +36,7 @@ angular.module('portainer.app').controller('SettingsController', [ allowDeviceMappingForRegularUsers: false, allowStackManagementForRegularUsers: false, disableContainerCapabilitiesForRegularUsers: false, + enableTelemetry: false, }; $scope.isContainerEditDisabled = function isContainerEditDisabled() { @@ -85,6 +86,7 @@ angular.module('portainer.app').controller('SettingsController', [ settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers; settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers; settings.AllowContainerCapabilitiesForRegularUsers = !$scope.formValues.disableContainerCapabilitiesForRegularUsers; + settings.EnableTelemetry = $scope.formValues.enableTelemetry; $scope.state.actionInProgress = true; updateSettings(settings); @@ -105,6 +107,7 @@ angular.module('portainer.app').controller('SettingsController', [ StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers); StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers); StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers); + StateManager.updateEnableTelemetry(settings.EnableTelemetry); $state.reload(); }) .catch(function error(err) { @@ -133,6 +136,7 @@ angular.module('portainer.app').controller('SettingsController', [ $scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers; $scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers; $scope.formValues.disableContainerCapabilitiesForRegularUsers = !settings.AllowContainerCapabilitiesForRegularUsers; + $scope.formValues.enableTelemetry = settings.EnableTelemetry; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve application settings'); diff --git a/app/vendors.js b/app/vendors.js index c925c910f..8bdc36d0d 100644 --- a/app/vendors.js +++ b/app/vendors.js @@ -25,7 +25,6 @@ import 'angular-resource'; import 'angular-utils-pagination'; import 'angular-local-storage'; import 'angular-jwt'; -import 'angular-google-analytics'; import 'angular-json-tree'; import 'angular-loading-bar'; import 'angular-clipboard'; @@ -37,5 +36,6 @@ import 'js-yaml/dist/js-yaml.js'; import 'angular-ui-bootstrap'; import 'angular-moment-picker'; import 'angular-multiselect/isteven-multi-select.js'; +import 'angulartics/dist/angulartics.min.js'; window.angular = angular; diff --git a/gruntfile.js b/gruntfile.js index 919fc6d93..d99563bdf 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -177,12 +177,12 @@ function shell_run_container() { 'docker rm -f portainer', 'docker run -d -p 8000:8000 -p 9000:9000 -v $(pwd)/dist:/app -v ' + portainer_data + - ':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics', + ':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer', ].join(';'); } function shell_run_localserver() { - return './dist/portainer --no-analytics'; + return './dist/portainer'; } function shell_install_yarndeps() { diff --git a/package.json b/package.json index 077ecadc2..145b540df 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,6 @@ "bugs": { "url": "https://github.com/portainer/portainer/issues" }, - "config": { - "GA_ID": "UA-84944922-2" - }, "licenses": [ { "type": "Zlib", @@ -56,7 +53,6 @@ "angular": "1.8.0", "angular-clipboard": "^1.6.2", "angular-file-saver": "^1.1.3", - "angular-google-analytics": "github:revolunet/angular-google-analytics#semver:~1.1.9", "angular-json-tree": "1.0.1", "angular-jwt": "~0.1.8", "angular-loading-bar": "~0.9.0", @@ -71,6 +67,7 @@ "angular-utils-pagination": "~0.11.1", "angularjs-scroll-glue": "^2.2.0", "angularjs-slider": "^6.4.0", + "angulartics": "^1.6.0", "babel-plugin-angularjs-annotate": "^0.10.0", "bootbox": "^5.4.0", "bootstrap": "^3.4.0", @@ -162,4 +159,4 @@ "*.js": "eslint --cache --fix", "*.{js,css,md,html}": "prettier --write" } -} \ No newline at end of file +} diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index f0258a201..461ebbcb5 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -1,5 +1,5 @@ const path = require('path'); -const { ProvidePlugin, IgnorePlugin, DefinePlugin } = require('webpack'); +const { ProvidePlugin, IgnorePlugin } = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const WebpackBuildNotifierPlugin = require('webpack-build-notifier'); const CleanTerminalPlugin = require('clean-terminal-webpack-plugin'); @@ -92,9 +92,6 @@ module.exports = { collections: true, paths: true, }), - new DefinePlugin({ - __CONFIG_GA_ID: JSON.stringify(pkg.config.GA_ID), - }), ], optimization: { splitChunks: { diff --git a/yarn.lock b/yarn.lock index c7cc60ff1..1abaa00ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1205,10 +1205,6 @@ angular-file-saver@^1.1.3: blob-tmp "^1.0.0" file-saver "^1.3.3" -"angular-google-analytics@github:revolunet/angular-google-analytics#semver:~1.1.9": - version "1.1.8" - resolved "https://codeload.github.com/revolunet/angular-google-analytics/tar.gz/92768a525870bc066dcf85fbe9d9f115358a6d91" - angular-json-tree@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/angular-json-tree/-/angular-json-tree-1.0.1.tgz#61b6e76ab165130335d9ec46fa572eb99604de51" @@ -1301,6 +1297,11 @@ angularjs-slider@^6.4.0: resolved "https://registry.yarnpkg.com/angularjs-slider/-/angularjs-slider-6.7.0.tgz#eb2229311b81b79315a36e7b5eb700e128f50319" integrity sha512-Cizsuax65wN2Y+htmA3safE5ALOSCyWcKyWkziaO8vCVymi26bQQs6kKDhkYc8GFix/KE7Oc9gH3QLlTUgD38w== +angulartics@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/angulartics/-/angulartics-1.6.0.tgz#a89c17ef8ea2334ebced65d6265951846f848172" + integrity sha512-fywhCi1InawcX+rpLv9NQ32Ed87KoZeH20SUIsRUz9dYJSxuk4/uxiKiopITveGxTC8THYHFEATj9Y/X+BvMqA== + ansi-colors@^3.0.0, ansi-colors@^3.2.1: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" From e4ca58a0423c153b84c1f84f204fbd93da7b823d Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Fri, 7 Aug 2020 00:55:36 +0200 Subject: [PATCH 116/195] fix(application): hpa breaks application edit (#4166) --- app/kubernetes/services/applicationService.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index a6bfdd2f7..e3c549de8 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -276,8 +276,10 @@ class KubernetesApplicationService { const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp); const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind); - if (_.isEmpty(oldFormValues.AutoScaler)) { - await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler); + if (!oldFormValues.AutoScaler.IsUsed) { + if (newFormValues.AutoScaler.IsUsed) { + await this.KubernetesHorizontalPodAutoScalerService.create(newAutoScaler); + } } else { const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp); const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind); From d85708f6eaa54e38a0b9a5a7a75848e8751b3142 Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Fri, 7 Aug 2020 01:11:47 +0200 Subject: [PATCH 117/195] feat(docker/services): Add the ability to edit a service networks (#3957) * feat(services): update services details view * feat(services): Add the ability to edit a service networks * feat(services): show ingress network * refactor(services): use lodash * feat(networks): disable sending when updating * feat(networks): limit size of select * feat(services): update networks only when network is new * feat(services): prevent submitting of empty networks * feat(services): show unique networks * fix(service): use empty array default for networks * feat(service): show only swarm networks * feat(services): show placeholder for network * feat(services): show spaced select box * feat(services): show macvlan ip * feat(service): fetch the network subnet * feat(services): show empty ip when network is not connected Co-authored-by: Chaim Lev-Ari --- app/docker/models/network.js | 1 + app/docker/services/networkService.js | 1 - .../services/edit/includes/networks.html | 60 ++++++++++++++-- .../views/services/edit/serviceController.js | 70 ++++++++++++++++++- 4 files changed, 123 insertions(+), 9 deletions(-) diff --git a/app/docker/models/network.js b/app/docker/models/network.js index e3beb5e1f..32c145efa 100644 --- a/app/docker/models/network.js +++ b/app/docker/models/network.js @@ -10,6 +10,7 @@ export function NetworkViewModel(data) { this.IPAM = data.IPAM; this.Containers = data.Containers; this.Options = data.Options; + this.Ingress = data.Ingress; this.Labels = data.Labels; if (this.Labels && this.Labels['com.docker.compose.project']) { diff --git a/app/docker/services/networkService.js b/app/docker/services/networkService.js index 3804aaeed..ad056ae48 100644 --- a/app/docker/services/networkService.js +++ b/app/docker/services/networkService.js @@ -41,7 +41,6 @@ angular.module('portainer.docker').factory('NetworkService', [ Network.query({ filters: filters }) .$promise.then(function success(data) { var networks = data; - var filteredNetworks = networks .filter(function (network) { if (localNetworks && network.Scope === 'local') { diff --git a/app/docker/views/services/edit/includes/networks.html b/app/docker/views/services/edit/includes/networks.html index 40f4fed2d..aa3060c10 100644 --- a/app/docker/views/services/edit/includes/networks.html +++ b/app/docker/views/services/edit/includes/networks.html @@ -1,26 +1,74 @@
    - - + + + +

    This service is not connected to any networks.

    - + + + - + - + + + +
    Name ID IP addressActions
    - {{ network.NetworkID }} + + {{ network.Name }} {{ network.Addr }} + {{ network.Id }} + + {{ network.Addr }} + + + + +
    + + +
    diff --git a/app/docker/views/services/edit/serviceController.js b/app/docker/views/services/edit/serviceController.js index 2e5907ecf..ef6bd3f8f 100644 --- a/app/docker/views/services/edit/serviceController.js +++ b/app/docker/views/services/edit/serviceController.js @@ -17,6 +17,7 @@ require('./includes/servicelabels.html'); require('./includes/tasks.html'); require('./includes/updateconfig.html'); +import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; angular.module('portainer.docker').controller('ServiceController', [ @@ -51,6 +52,7 @@ angular.module('portainer.docker').controller('ServiceController', [ 'EndpointProvider', 'clipboard', 'WebhookHelper', + 'NetworkService', function ( $q, $scope, @@ -82,7 +84,8 @@ angular.module('portainer.docker').controller('ServiceController', [ WebhookService, EndpointProvider, clipboard, - WebhookHelper + WebhookHelper, + NetworkService ) { $scope.state = { updateInProgress: false, @@ -210,6 +213,25 @@ angular.module('portainer.docker').controller('ServiceController', [ $scope.updateMount = function updateMount(service) { updateServiceArray(service, 'ServiceMounts', service.ServiceMounts); }; + + $scope.addNetwork = function addNetwork(service) { + if (!service.Networks) { + service.Networks = []; + } + service.Networks.push({ Editable: true }); + }; + + $scope.removeNetwork = function removeNetwork(service, index) { + var removedElement = service.Networks.splice(index, 1); + if (removedElement && removedElement.length && removedElement[0].Id) { + updateServiceArray(service, 'Networks', service.Networks); + } + }; + + $scope.updateNetwork = function updateNetwork(service) { + updateServiceArray(service, 'Networks', service.Networks); + }; + $scope.addPlacementConstraint = function addPlacementConstraint(service) { service.ServiceConstraints.push({ key: '', operator: '==', value: '' }); updateServiceArray(service, 'ServiceConstraints', service.ServiceConstraints); @@ -370,6 +392,14 @@ angular.module('portainer.docker').controller('ServiceController', [ config.TaskTemplate.ContainerSpec.Image = service.Image; } + if ($scope.hasChanges(service, ['Networks'])) { + config.Networks = _.map( + _.filter(service.Networks, (item) => item.Id && item.Editable), + (item) => ({ Target: item.Id }) + ); + config.TaskTemplate.Networks = config.Networks; + } + config.TaskTemplate.ContainerSpec.Secrets = service.ServiceSecrets ? service.ServiceSecrets.map(SecretHelper.secretConfig) : []; config.TaskTemplate.ContainerSpec.Configs = service.ServiceConfigs ? service.ServiceConfigs.map(ConfigHelper.configConfig) : []; config.TaskTemplate.ContainerSpec.Hosts = service.Hosts ? ServiceHelper.translateHostnameIPToHostsEntries(service.Hosts) : []; @@ -629,11 +659,12 @@ angular.module('portainer.docker').controller('ServiceController', [ configs: apiVersion >= 1.3 ? ConfigService.configs() : [], availableImages: ImageService.images(), availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25), + availableNetworks: NetworkService.networks(true, true, apiVersion >= 1.25), settings: SettingsService.publicSettings(), webhooks: WebhookService.webhooks(service.Id, EndpointProvider.endpointID()), }); }) - .then(function success(data) { + .then(async function success(data) { $scope.nodes = data.nodes; $scope.configs = data.configs; $scope.secrets = data.secrets; @@ -642,6 +673,36 @@ angular.module('portainer.docker').controller('ServiceController', [ $scope.availableVolumes = data.volumes; $scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers; $scope.isAdmin = Authentication.isAdmin(); + $scope.availableNetworks = data.availableNetworks; + $scope.swarmNetworks = _.filter($scope.availableNetworks, (network) => network.Scope === 'swarm'); + + const serviceNetworks = _.uniqBy(_.concat($scope.service.Model.Spec.Networks || [], $scope.service.Model.Spec.TaskTemplate.Networks || []), 'Target'); + const networks = _.filter( + _.map(serviceNetworks, ({ Target }) => _.find(data.availableNetworks, { Id: Target })), + Boolean + ); + + if (_.some($scope.service.Ports, (port) => port.PublishMode === 'ingress')) { + const ingressNetwork = _.find($scope.availableNetworks, (network) => network.Ingress); + if (ingressNetwork) { + networks.unshift(ingressNetwork); + } + } + + $scope.service.Networks = await Promise.all( + _.map(networks, async (item) => { + let addr = ''; + if (item.IPAM.Config.length) { + addr = item.IPAM.Config[0].Subnet; + } else { + const network = await NetworkService.network(item.Id); + addr = (network && network.IPAM && network.IPAM.Config && network.IPAM.Config.length && network.IPAM.Config[0].Subnet) || ''; + } + return { Id: item.Id, Name: item.Name, Addr: addr, Editable: !item.Ingress }; + }) + ); + + originalService.Networks = angular.copy($scope.service.Networks); if (data.webhooks.length > 0) { var webhook = data.webhooks[0]; @@ -699,6 +760,11 @@ angular.module('portainer.docker').controller('ServiceController', [ previousServiceValues.push(name); } + $scope.filterNetworks = filterNetworks; + function filterNetworks(networks, current) { + return networks.filter((network) => !network.Ingress && (network.Id === current.Id || $scope.service.Networks.every((serviceNetwork) => network.Id !== serviceNetwork.Id))); + } + function updateServiceArray(service, name) { previousServiceValues.push(name); service.hasChanges = true; From b8f8c75380323b46a341ca08c2bb8743b0d5b5ec Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Fri, 7 Aug 2020 02:03:00 +0200 Subject: [PATCH 118/195] feat(k8s/resource-pool): prevent admins from making changes to "system" namespaces (#4167) --- .../views/resource-pools/edit/resourcePool.html | 10 +++++----- .../resource-pools/edit/resourcePoolController.js | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html index c27b9d162..855305a07 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePool.html +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -21,9 +21,9 @@
    -
    Quota
    +
    Quota
    -
    +
    -
    +
    Resource limits
    @@ -121,10 +121,10 @@
    -
    +
    Actions
    -
    +
    diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 3cb2a6a42..501a7de24 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -77,6 +77,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [ CpuLimit: 0, MemoryLimit: 0, MemoryReservation: 0, + CmdMode: 'default', + EntrypointMode: 'default', NodeName: null, capabilities: [], LogDriverName: '', @@ -89,6 +91,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.state = { formValidationError: '', actionInProgress: false, + mode: '', }; $scope.refreshSlider = function () { @@ -97,12 +100,18 @@ angular.module('portainer.docker').controller('CreateContainerController', [ }); }; + $scope.onImageNameChange = function () { + $scope.formValues.CmdMode = 'default'; + $scope.formValues.EntrypointMode = 'default'; + }; + $scope.config = { Image: '', Env: [], Cmd: '', MacAddress: '', ExposedPorts: {}, + Entrypoint: '', HostConfig: { RestartPolicy: { Name: 'no', @@ -212,6 +221,20 @@ angular.module('portainer.docker').controller('CreateContainerController', [ config.Tty = tty; } + function prepareCmd(config) { + if (_.isEmpty(config.Cmd) || $scope.formValues.CmdMode == 'default') { + delete config.Cmd; + } else { + config.Cmd = ContainerHelper.commandStringToArray(config.Cmd); + } + } + + function prepareEntrypoint(config) { + if ($scope.formValues.EntrypointMode == 'default' || (_.isEmpty(config.Cmd) && _.isEmpty(config.Entrypoint))) { + config.Entrypoint = null; + } + } + function prepareEnvironmentVariables(config) { var env = []; config.Env.forEach(function (v) { @@ -368,7 +391,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [ function prepareConfiguration() { var config = angular.copy($scope.config); - config.Cmd = ContainerHelper.commandStringToArray(config.Cmd); + prepareCmd(config); + prepareEntrypoint(config); prepareNetworkConfig(config); prepareImageConfig(config); preparePortBindings(config); @@ -386,8 +410,16 @@ angular.module('portainer.docker').controller('CreateContainerController', [ function loadFromContainerCmd() { if ($scope.config.Cmd) { $scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd); - } else { - $scope.config.Cmd = ''; + $scope.formValues.CmdMode = 'override'; + } + } + + function loadFromContainerEntrypoint() { + if (_.has($scope.config, 'Entrypoint')) { + if ($scope.config.Entrypoint == null) { + $scope.config.Entrypoint = ''; + } + $scope.formValues.EntrypointMode = 'override'; } } @@ -577,8 +609,10 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.formValues.AccessControlData.AccessControlEnabled = false; } $scope.fromContainer = fromContainer; + $scope.state.mode = 'duplicate'; $scope.config = ContainerHelper.configFromContainer(fromContainer.Model); loadFromContainerCmd(d); + loadFromContainerEntrypoint(d); loadFromContainerLogging(d); loadFromContainerPortBindings(d); loadFromContainerVolumes(d); @@ -706,7 +740,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ function create() { var oldContainer = null; - HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName); return findCurrentContainer().then(setOldContainer).then(confirmCreateContainer).then(startCreationProcess).catch(notifyOnError).finally(final); diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 14e0a89ef..01b05e60c 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -3,6 +3,16 @@ Containers > Add container + + +

    + + The new container may fail to start if the image is changed, and settings from the previous container aren't compatible. Common causes include entrypoint, cmd or + other settings set by an image. +

    +
    +
    +
    @@ -35,6 +45,7 @@ auto-complete="true" label-class="col-sm-1" input-class="col-sm-11" + on-image-change="onImageNameChange()" > @@ -192,15 +203,47 @@
    - +
    +
    + + +
    + +
    - +
    - +
    +
    + + +
    + +
    From 61f97469ab9ef4ee87bdb996f4eb0fbb44b49e61 Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Fri, 7 Aug 2020 06:40:24 +0200 Subject: [PATCH 122/195] feat(application): Add the ability to use existing volumes when creating an application (#4044) * feat(applications): update UI to use existing volumes * feat(application): Add the ability to use existing volumes when creating an application * feat(application): Existing persisted folders should default to associated volumes * feat(application): add form validation to existing volume * feat(application): remove the ability to use an existing volume with statefulset application * feat(k8s/applications): minor UI update * feat(k8s/application): minor UI update * feat(volume): allow to increase volume size and few other things * feat(volumes): add the ability to allow volume expansion * fix(storage): fix the storage patch request * fix(k8s/applications): remove conflict leftover * feat(k8s/configure): minor UI update * feat(k8s/volume): minor UI update * fix(storage): change few things Co-authored-by: Anthony Lapenna --- api/portainer.go | 7 +- .../converters/persistentVolumeClaim.js | 24 +++-- app/kubernetes/converters/storageClass.js | 18 ++++ .../models/application/formValues.js | 2 + app/kubernetes/models/storage-class/models.js | 1 + .../models/storage-class/payload.js | 16 +++ app/kubernetes/rest/storage.js | 6 ++ app/kubernetes/services/applicationService.js | 9 +- app/kubernetes/services/storageService.js | 20 ++++ .../create/createApplication.html | 102 +++++++++++++++--- .../create/createApplicationController.js | 96 ++++++++++++++++- app/kubernetes/views/configure/configure.html | 8 +- .../views/configure/configureController.js | 13 +++ app/kubernetes/views/volumes/edit/volume.html | 60 ++++++++++- .../views/volumes/edit/volumeController.js | 76 ++++++++++++- app/portainer/services/modalService.js | 18 ++++ 16 files changed, 436 insertions(+), 40 deletions(-) create mode 100644 app/kubernetes/models/storage-class/payload.js diff --git a/api/portainer.go b/api/portainer.go index 06f133957..f6f960380 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -344,9 +344,10 @@ type ( // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration KubernetesStorageClassConfig struct { - Name string `json:"Name"` - AccessModes []string `json:"AccessModes"` - Provisioner string `json:"Provisioner"` + Name string `json:"Name"` + AccessModes []string `json:"AccessModes"` + Provisioner string `json:"Provisioner"` + AllowVolumeExpansion bool `json:"AllowVolumeExpansion"` } // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server diff --git a/app/kubernetes/converters/persistentVolumeClaim.js b/app/kubernetes/converters/persistentVolumeClaim.js index 8a7a76060..60ea28c58 100644 --- a/app/kubernetes/converters/persistentVolumeClaim.js +++ b/app/kubernetes/converters/persistentVolumeClaim.js @@ -28,16 +28,28 @@ class KubernetesPersistentVolumeClaimConverter { _.remove(formValues.PersistedFolders, (item) => item.NeedsDeletion); const res = _.map(formValues.PersistedFolders, (item) => { const pvc = new KubernetesPersistentVolumeClaim(); - if (item.PersistentVolumeClaimName) { - pvc.Name = item.PersistentVolumeClaimName; - pvc.PreviousName = item.PersistentVolumeClaimName; + if (!_.isEmpty(item.ExistingVolume)) { + const existantPVC = item.ExistingVolume.PersistentVolumeClaim; + pvc.Name = existantPVC.Name; + if (item.PersistentVolumeClaimName) { + pvc.PreviousName = item.PersistentVolumeClaimName; + } + pvc.StorageClass = existantPVC.StorageClass; + pvc.Storage = existantPVC.Storage.charAt(0) + 'i'; + pvc.CreationDate = existantPVC.CreationDate; + pvc.Id = existantPVC.Id; } else { - pvc.Name = formValues.Name + '-' + pvc.Name; + if (item.PersistentVolumeClaimName) { + pvc.Name = item.PersistentVolumeClaimName; + pvc.PreviousName = item.PersistentVolumeClaimName; + } else { + pvc.Name = formValues.Name + '-' + pvc.Name; + } + pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i'; + pvc.StorageClass = item.StorageClass; } pvc.MountPath = item.ContainerPath; pvc.Namespace = formValues.ResourcePool.Namespace.Name; - pvc.Storage = '' + item.Size + item.SizeUnit.charAt(0) + 'i'; - pvc.StorageClass = item.StorageClass; pvc.ApplicationOwner = formValues.ApplicationOwner; pvc.ApplicationName = formValues.Name; return pvc; diff --git a/app/kubernetes/converters/storageClass.js b/app/kubernetes/converters/storageClass.js index 773c96862..7491c32e7 100644 --- a/app/kubernetes/converters/storageClass.js +++ b/app/kubernetes/converters/storageClass.js @@ -1,4 +1,6 @@ import { KubernetesStorageClass } from 'Kubernetes/models/storage-class/models'; +import { KubernetesStorageClassCreatePayload } from 'Kubernetes/models/storage-class/payload'; +import * as JsonPatch from 'fast-json-patch'; class KubernetesStorageClassConverter { /** @@ -8,8 +10,24 @@ class KubernetesStorageClassConverter { const res = new KubernetesStorageClass(); res.Name = data.metadata.name; res.Provisioner = data.provisioner; + res.AllowVolumeExpansion = data.allowVolumeExpansion; return res; } + + static createPayload(storageClass) { + const res = new KubernetesStorageClassCreatePayload(); + res.metadata.name = storageClass.Name; + res.provisioner = storageClass.Provisioner; + res.allowVolumeExpansion = storageClass.AllowVolumeExpansion; + return res; + } + + static patchPayload(oldStorageClass, newStorageClass) { + const oldPayload = KubernetesStorageClassConverter.createPayload(oldStorageClass); + const newPayload = KubernetesStorageClassConverter.createPayload(newStorageClass); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; + } } export default KubernetesStorageClassConverter; diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 49d3cc827..7ca9ca1b9 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -92,6 +92,8 @@ const _KubernetesApplicationPersistedFolderFormValue = Object.freeze({ Size: '', SizeUnit: 'GB', StorageClass: {}, + ExistingVolume: null, + UseNewVolume: true, }); export class KubernetesApplicationPersistedFolderFormValue { diff --git a/app/kubernetes/models/storage-class/models.js b/app/kubernetes/models/storage-class/models.js index 153976f64..beb5bb3d5 100644 --- a/app/kubernetes/models/storage-class/models.js +++ b/app/kubernetes/models/storage-class/models.js @@ -25,6 +25,7 @@ const _KubernetesStorageClass = Object.freeze({ Name: '', AccessModes: [], Provisioner: '', + AllowVolumeExpansion: false, }); export class KubernetesStorageClass { diff --git a/app/kubernetes/models/storage-class/payload.js b/app/kubernetes/models/storage-class/payload.js new file mode 100644 index 000000000..47e183669 --- /dev/null +++ b/app/kubernetes/models/storage-class/payload.js @@ -0,0 +1,16 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +/** + * KubernetesStorageClassCreatePayload Model + */ +const _KubernetesStorageClassCreatePayload = Object.freeze({ + metadata: new KubernetesCommonMetadataPayload(), + provisioner: '', + allowVolumeExpansion: false, +}); + +export class KubernetesStorageClassCreatePayload { + constructor() { + Object.assign(this, JSON.parse(JSON.stringify(_KubernetesStorageClassCreatePayload))); + } +} diff --git a/app/kubernetes/rest/storage.js b/app/kubernetes/rest/storage.js index 44e6406bf..d3ef80d68 100644 --- a/app/kubernetes/rest/storage.js +++ b/app/kubernetes/rest/storage.js @@ -29,6 +29,12 @@ angular.module('portainer.kubernetes').factory('KubernetesStorage', [ }, create: { method: 'POST' }, update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }, delete: { method: 'DELETE' }, } ); diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index e3c549de8..614267068 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -209,7 +209,7 @@ class KubernetesApplicationService { app.ServiceName = headlessService.metadata.name; } else { const claimPromises = _.map(claims, (item) => { - if (!item.PreviousName) { + if (!item.PreviousName && !item.Id) { return this.KubernetesPersistentVolumeClaimService.create(item); } }); @@ -255,11 +255,12 @@ class KubernetesApplicationService { await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService); } else { const claimPromises = _.map(newClaims, (newClaim) => { - if (!newClaim.PreviousName) { + if (!newClaim.PreviousName && !newClaim.Id) { return this.KubernetesPersistentVolumeClaimService.create(newClaim); + } else if (!newClaim.Id) { + const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName }); + return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim); } - const oldClaim = _.find(oldClaims, { Name: newClaim.PreviousName }); - return this.KubernetesPersistentVolumeClaimService.patch(oldClaim, newClaim); }); await Promise.all(claimPromises); } diff --git a/app/kubernetes/services/storageService.js b/app/kubernetes/services/storageService.js index ea2a5f053..550a8f5c9 100644 --- a/app/kubernetes/services/storageService.js +++ b/app/kubernetes/services/storageService.js @@ -2,6 +2,7 @@ import angular from 'angular'; import _ from 'lodash-es'; import PortainerError from 'Portainer/error'; import KubernetesStorageClassConverter from 'Kubernetes/converters/storageClass'; +import { KubernetesCommonParams } from 'Kubernetes/models/common/params'; class KubernetesStorageService { /* @ngInject */ @@ -10,6 +11,7 @@ class KubernetesStorageService { this.KubernetesStorage = KubernetesStorage; this.getAsync = this.getAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); } /** @@ -31,6 +33,24 @@ class KubernetesStorageService { get(endpointId) { return this.$async(this.getAsync, endpointId); } + + /** + * PATCH + */ + async patchAsync(oldStorageClass, newStorageClass) { + try { + const params = new KubernetesCommonParams(); + params.id = newStorageClass.Name; + const payload = KubernetesStorageClassConverter.patchPayload(oldStorageClass, newStorageClass); + await this.KubernetesStorage().patch(params, payload).$promise; + } catch (err) { + throw new PortainerError('Unable to patch storage class', err); + } + } + + patch(oldStorageClass, newStorageClass) { + return this.$async(this.patchAsync, oldStorageClass, newStorageClass); + } } export default KubernetesStorageService; diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 4dfaf0d00..57315ce2f 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -319,8 +319,8 @@
    -
    -
    +
    +
    path in container
    -
    +
    + + + + +
    + +
    requested size
    -
    +
    storage + + +
    + +
    +
    + + +
    @@ -401,13 +450,26 @@
    -
    +
    + +

    Size is required.

    This value must be greater than zero.

    +
    + +

    Volume is required.

    +
    +

    This volume is already used.

    +
    @@ -467,7 +529,12 @@

    All the instances of this application will use the same data

    -
    +
    Every instance of this application will use their own data

    -
    +
    diff --git a/app/kubernetes/views/volumes/edit/volumeController.js b/app/kubernetes/views/volumes/edit/volumeController.js index a91be622c..6f20455c9 100644 --- a/app/kubernetes/views/volumes/edit/volumeController.js +++ b/app/kubernetes/views/volumes/edit/volumeController.js @@ -2,10 +2,23 @@ import angular from 'angular'; import _ from 'lodash-es'; import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; +import filesizeParser from 'filesize-parser'; class KubernetesVolumeController { /* @ngInject */ - constructor($async, $state, Notifications, LocalStorage, KubernetesVolumeService, KubernetesEventService, KubernetesNamespaceHelper, KubernetesApplicationService) { + constructor( + $async, + $state, + Notifications, + LocalStorage, + KubernetesVolumeService, + KubernetesEventService, + KubernetesNamespaceHelper, + KubernetesApplicationService, + KubernetesPersistentVolumeClaimService, + ModalService, + KubernetesPodService + ) { this.$async = $async; this.$state = $state; this.Notifications = Notifications; @@ -15,10 +28,14 @@ class KubernetesVolumeController { this.KubernetesEventService = KubernetesEventService; this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.KubernetesApplicationService = KubernetesApplicationService; + this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; + this.ModalService = ModalService; + this.KubernetesPodService = KubernetesPodService; this.onInit = this.onInit.bind(this); this.getVolume = this.getVolume.bind(this); this.getVolumeAsync = this.getVolumeAsync.bind(this); + this.updateVolumeAsync = this.updateVolumeAsync.bind(this); this.getEvents = this.getEvents.bind(this); this.getEventsAsync = this.getEventsAsync.bind(this); } @@ -44,9 +61,57 @@ class KubernetesVolumeController { return KubernetesVolumeHelper.isUsed(this.volume); } + onChangeSize() { + if (this.state.volumeSize) { + const size = filesizeParser(this.state.volumeSize + this.state.volumeSizeUnit); + if (this.state.oldVolumeSize > size) { + this.state.volumeSizeError = true; + } else { + this.volume.PersistentVolumeClaim.Storage = size; + this.state.volumeSizeError = false; + } + } + } + + sizeIsValid() { + return !this.state.volumeSizeError && this.state.oldVolumeSize !== this.volume.PersistentVolumeClaim.Storage; + } + /** * VOLUME */ + + async updateVolumeAsync(redeploy) { + try { + this.volume.PersistentVolumeClaim.Storage = this.state.volumeSize + this.state.volumeSizeUnit.charAt(0) + 'i'; + await this.KubernetesPersistentVolumeClaimService.patch(this.oldVolume.PersistentVolumeClaim, this.volume.PersistentVolumeClaim); + this.Notifications.success('Volume successfully updated'); + + if (redeploy) { + const promises = _.flatten( + _.map(this.volume.Applications, (app) => { + return _.map(app.Pods, (item) => this.KubernetesPodService.delete(item)); + }) + ); + await Promise.all(promises); + this.Notifications.success('Applications successfully redeployed'); + } + + this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to update volume.'); + } + } + + updateVolume() { + this.ModalService.confirmRedeploy( + 'One or multiple applications are currently using this volume.
    For the change to be taken into account these applications will need to be redeployed. Do you want us to reschedule it now?', + (redeploy) => { + return this.$async(this.updateVolumeAsync, redeploy); + } + ); + } + async getVolumeAsync() { try { const [volume, applications] = await Promise.all([ @@ -55,6 +120,10 @@ class KubernetesVolumeController { ]); volume.Applications = KubernetesVolumeHelper.getUsingApplications(volume, applications); this.volume = volume; + this.oldVolume = angular.copy(volume); + this.state.volumeSize = parseInt(volume.PersistentVolumeClaim.Storage.slice(0, -2)); + this.state.volumeSizeUnit = volume.PersistentVolumeClaim.Storage.slice(-2); + this.state.oldVolumeSize = filesizeParser(volume.PersistentVolumeClaim.Storage); } catch (err) { this.Notifications.error('Failure', err, 'Unable to retrieve volume'); } @@ -101,6 +170,11 @@ class KubernetesVolumeController { namespace: this.$transition$.params().namespace, name: this.$transition$.params().name, eventWarningCount: 0, + availableSizeUnits: ['MB', 'GB', 'TB'], + increaseSize: false, + volumeSize: 0, + volumeSizeUnit: 'GB', + volumeSizeError: false, }; this.state.activeTab = this.LocalStorage.getActiveTab('volume'); diff --git a/app/portainer/services/modalService.js b/app/portainer/services/modalService.js index 196483ba4..18e6f0b14 100644 --- a/app/portainer/services/modalService.js +++ b/app/portainer/services/modalService.js @@ -150,6 +150,24 @@ angular.module('portainer.app').factory('ModalService', [ }); }; + service.confirmRedeploy = function (message, callback) { + message = $sanitize(message); + service.confirm({ + title: '', + message: message, + buttons: { + confirm: { + label: 'Redeploy the applications', + className: 'btn-primary', + }, + cancel: { + label: "I'll do it later", + }, + }, + callback: callback, + }); + }; + service.confirmDeletionAsync = function confirmDeletionAsync(message) { return new Promise((resolve) => { service.confirmDeletion(message, (confirmed) => resolve(confirmed)); From 26ee78e1e735c4e48226426ba3961a9c012c7788 Mon Sep 17 00:00:00 2001 From: itsconquest Date: Fri, 7 Aug 2020 16:50:56 +1200 Subject: [PATCH 123/195] refactor(UX): fix improper grammar (#4161) --- app/docker/views/containers/create/createcontainer.html | 2 +- app/edge/components/group-form/groupForm.html | 2 +- .../edge-stacks/createEdgeStackView/createEdgeStackView.html | 2 +- app/portainer/views/stacks/create/createstack.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 01b05e60c..8252f596c 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -430,7 +430,7 @@
    - You don't have any shared network. Head over the networks view to create one. + You don't have any shared networks. Head over to the networks view to create one.
    diff --git a/app/edge/components/group-form/groupForm.html b/app/edge/components/group-form/groupForm.html index b527440d0..5e0064254 100644 --- a/app/edge/components/group-form/groupForm.html +++ b/app/edge/components/group-form/groupForm.html @@ -65,7 +65,7 @@
    -
    No Edge endpoints available. Head over the Endpoints view to add endpoints.
    +
    No Edge endpoints are available. Head over to the Endpoints view to add endpoints.
    diff --git a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html index d366039ac..316d77501 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html +++ b/app/edge/views/edge-stacks/createEdgeStackView/createEdgeStackView.html @@ -32,7 +32,7 @@
    - No Edge groups are available. Head over the Edge groups view to create one. + No Edge groups are available. Head over to the Edge groups view to create one.
    diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html index 09ebb8b05..1154eb4b7 100644 --- a/app/portainer/views/stacks/create/createstack.html +++ b/app/portainer/views/stacks/create/createstack.html @@ -209,7 +209,7 @@ - No custom template are available. Head over the custom template view to create one. + No custom templates are available. Head over to the custom template view to create one.
    From e7a33347c6aad709cc33cc1b0e6964105df66d75 Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Sat, 8 Aug 2020 00:43:34 +0200 Subject: [PATCH 124/195] fix(k8s/storage): missing endpoint id in storage patch request (#4174) --- app/kubernetes/services/storageService.js | 7 ++++--- app/kubernetes/views/configure/configureController.js | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/kubernetes/services/storageService.js b/app/kubernetes/services/storageService.js index 550a8f5c9..b1c9d0161 100644 --- a/app/kubernetes/services/storageService.js +++ b/app/kubernetes/services/storageService.js @@ -37,10 +37,11 @@ class KubernetesStorageService { /** * PATCH */ - async patchAsync(oldStorageClass, newStorageClass) { + async patchAsync(endpointId, oldStorageClass, newStorageClass) { try { const params = new KubernetesCommonParams(); params.id = newStorageClass.Name; + params.endpointId = endpointId; const payload = KubernetesStorageClassConverter.patchPayload(oldStorageClass, newStorageClass); await this.KubernetesStorage().patch(params, payload).$promise; } catch (err) { @@ -48,8 +49,8 @@ class KubernetesStorageService { } } - patch(oldStorageClass, newStorageClass) { - return this.$async(this.patchAsync, oldStorageClass, newStorageClass); + patch(endpointId, oldStorageClass, newStorageClass) { + return this.$async(this.patchAsync, endpointId, oldStorageClass, newStorageClass); } } diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index 4e6770ca1..804dc66c7 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -57,7 +57,7 @@ class KubernetesConfigureController { const storagePromises = _.map(classes, (storageClass) => { const oldStorageClass = _.find(this.oldStorageClasses, { Name: storageClass.Name }); if (oldStorageClass) { - return this.KubernetesStorageService.patch(oldStorageClass, storageClass); + return this.KubernetesStorageService.patch(this.state.endpointId, oldStorageClass, storageClass); } }); @@ -89,6 +89,7 @@ class KubernetesConfigureController { actionInProgress: false, displayConfigureClassPanel: {}, viewReady: false, + endpointId: this.$stateParams.id, }; this.formValues = { @@ -97,8 +98,7 @@ class KubernetesConfigureController { }; try { - const endpointId = this.$stateParams.id; - [this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(endpointId), this.EndpointService.endpoint(endpointId)]); + [this.StorageClasses, this.endpoint] = await Promise.all([this.KubernetesStorageService.get(this.state.endpointId), this.EndpointService.endpoint(this.state.endpointId)]); _.forEach(this.StorageClasses, (item) => { item.availableAccessModes = new KubernetesStorageClassAccessPolicies(); const storage = _.find(this.endpoint.Kubernetes.Configuration.StorageClasses, (sc) => sc.Name === item.Name); From cb1a1e7be568ac1ad7092448869d376dfe79c396 Mon Sep 17 00:00:00 2001 From: Maxime Bajeux Date: Sat, 8 Aug 2020 00:46:11 +0200 Subject: [PATCH 125/195] feat(k8s/resource-pool): add a modal when reducing the quota of an in use RP (#4170) * feat(resourcepool): Reducing the Quota assigned to a RP * fix(k8s/resource-pool): fix an issue with hasResourceQuotaBeenReduce condition Co-authored-by: Anthony Lapenna --- .../edit/resourcePoolController.js | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js index ae5f2113e..8c5995b3e 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePoolController.js +++ b/app/kubernetes/views/resource-pools/edit/resourcePoolController.js @@ -1,7 +1,7 @@ import angular from 'angular'; import _ from 'lodash-es'; import filesizeParser from 'filesize-parser'; -import { KubernetesResourceQuotaDefaults, KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; +import { KubernetesResourceQuota, KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; @@ -19,7 +19,8 @@ class KubernetesResourcePoolController { KubernetesEventService, KubernetesPodService, KubernetesApplicationService, - KubernetesNamespaceHelper + KubernetesNamespaceHelper, + ModalService ) { this.$async = $async; this.$state = $state; @@ -34,6 +35,7 @@ class KubernetesResourcePoolController { this.KubernetesPodService = KubernetesPodService; this.KubernetesApplicationService = KubernetesApplicationService; this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; + this.ModalService = ModalService; this.onInit = this.onInit.bind(this); this.createResourceQuotaAsync = this.createResourceQuotaAsync.bind(this); @@ -81,6 +83,17 @@ class KubernetesResourcePoolController { await this.KubernetesResourceQuotaService.create(quota); } + hasResourceQuotaBeenReduce() { + if (this.formValues.hasQuota) { + const cpuLimit = this.formValues.CpuLimit; + const memoryLimit = KubernetesResourceReservationHelper.bytesValue(this.formValues.MemoryLimit); + if (cpuLimit < this.oldQuota.CpuLimit || memoryLimit < this.oldQuota.MemoryLimit) { + return true; + } + } + return false; + } + async updateResourcePoolAsync() { this.state.actionInProgress = true; try { @@ -112,7 +125,18 @@ class KubernetesResourcePoolController { } updateResourcePool() { - return this.$async(this.updateResourcePoolAsync); + if (this.hasResourceQuotaBeenReduce()) { + this.ModalService.confirmUpdate( + 'Reducing the quota assigned to an "in-use" resource pool may have unintended consequences, including preventing running applications from functioning correctly and potentially even blocking them from running at all.', + (confirmed) => { + if (confirmed) { + return this.$async(this.updateResourcePoolAsync); + } + } + ); + } else { + return this.$async(this.updateResourcePoolAsync); + } } hasEventWarnings() { @@ -200,6 +224,7 @@ class KubernetesResourcePoolController { const quota = pool.Quota; if (quota) { + this.oldQuota = angular.copy(quota); this.formValues.hasQuota = true; this.formValues.CpuLimit = quota.CpuLimit; this.formValues.MemoryLimit = KubernetesResourceReservationHelper.megaBytesValue(quota.MemoryLimit); From c5731e237ec34767357e85e74592537ce3470af0 Mon Sep 17 00:00:00 2001 From: itsconquest Date: Mon, 10 Aug 2020 10:27:27 +1200 Subject: [PATCH 126/195] fix(docker/container): handle multiple ips with the same port (#4121) * fix(containers): handle multiple ips with the same port * fix(containers): fix parsing --- app/docker/helpers/containerHelper.js | 24 ++++++++++++------- .../containers/edit/containerController.js | 15 +++++++----- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index 1ecd3d0cf..29a7e99b9 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -179,7 +179,11 @@ angular.module('portainer.docker').factory('ContainerHelper', [ } const bindKey = containerPort + '/' + portBinding.protocol; - bindings[bindKey] = [{ HostIp: hostIp, HostPort: hostPort }]; + if (bindings[bindKey]) { + bindings[bindKey].push({ HostIp: hostIp, HostPort: hostPort }); + } else { + bindings[bindKey] = [{ HostIp: hostIp, HostPort: hostPort }]; + } } }); return bindings; @@ -196,12 +200,15 @@ angular.module('portainer.docker').factory('ContainerHelper', [ _.forEach(portBindingKeysByProtocol, (portBindingKeys, protocol) => { // Group the port bindings by host IP - const portBindingKeysByHostIp = _.groupBy(portBindingKeys, (portKey) => { - const portBinding = portBindings[portKey][0]; - return portBinding.HostIp || ''; - }); + const portBindingKeysByHostIp = {}; + for (const portKey of portBindingKeys) { + for (const portBinding of portBindings[portKey]) { + portBindingKeysByHostIp[portBinding.HostIp] = portBindingKeysByHostIp[portBinding.HostIp] || []; + portBindingKeysByHostIp[portBinding.HostIp].push(portKey); + } + } - _.forEach(portBindingKeysByHostIp, (portBindingKeys) => { + _.forEach(portBindingKeysByHostIp, (portBindingKeys, ip) => { // Sort by host port const sortedPortBindingKeys = _.orderBy(portBindingKeys, (portKey) => { return parseInt(_.split(portKey, '/')[0]); @@ -213,6 +220,7 @@ angular.module('portainer.docker').factory('ContainerHelper', [ const portKeySplit = _.split(portKey, '/'); const containerPort = parseInt(portKeySplit[0]); const portBinding = portBindings[portKey][0]; + portBindings[portKey].shift(); const hostPort = parsePort(portBinding.HostPort); // We only combine single ports, and skip the host port ranges on one container port @@ -234,8 +242,8 @@ angular.module('portainer.docker').factory('ContainerHelper', [ } let bindingHostPort = portBinding.HostPort.toString(); - if (portBinding.HostIp) { - bindingHostPort = portBinding.HostIp + ':' + bindingHostPort; + if (ip) { + bindingHostPort = `${ip}:${bindingHostPort}`; } const binding = { diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index aee0f07f1..028fa89b7 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -1,4 +1,5 @@ import moment from 'moment'; +import _ from 'lodash-es'; import { PorImageRegistryModel } from 'Docker/models/porImageRegistry'; angular.module('portainer.docker').controller('ContainerController', [ @@ -83,12 +84,14 @@ angular.module('portainer.docker').controller('ContainerController', [ $scope.portBindings = []; if (container.NetworkSettings.Ports) { - angular.forEach(Object.keys(container.NetworkSettings.Ports), function (portMapping) { - if (container.NetworkSettings.Ports[portMapping]) { - var mapping = {}; - mapping.container = portMapping; - mapping.host = container.NetworkSettings.Ports[portMapping][0].HostIp + ':' + container.NetworkSettings.Ports[portMapping][0].HostPort; - $scope.portBindings.push(mapping); + _.forEach(Object.keys(container.NetworkSettings.Ports), function (key) { + if (container.NetworkSettings.Ports[key]) { + _.forEach(container.NetworkSettings.Ports[key], (portMapping) => { + const mapping = {}; + mapping.container = key; + mapping.host = `${portMapping.HostIp}:${portMapping.HostPort}`; + $scope.portBindings.push(mapping); + }); } }); } From 8408484f8bda8611f1b07e27505890049d610873 Mon Sep 17 00:00:00 2001 From: itsconquest Date: Mon, 10 Aug 2020 10:59:00 +1200 Subject: [PATCH 127/195] feat(docker/node): change table to div and fix styling (#4173) --- .../node-labels-table/node-labels-table.html | 44 +++++++------------ .../swarm-node-details-panel.html | 2 +- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/app/docker/components/host-view-panels/node-labels-table/node-labels-table.html b/app/docker/components/host-view-panels/node-labels-table/node-labels-table.html index e26ec168c..4e2dff80b 100644 --- a/app/docker/components/host-view-panels/node-labels-table/node-labels-table.html +++ b/app/docker/components/host-view-panels/node-labels-table/node-labels-table.html @@ -2,32 +2,18 @@ There are no labels for this node.
    - - - - - - - - - - - - - -
    LabelValue
    -
    - Name - -
    -
    -
    - Value - - - - -
    -
    +
    +
    +
    + name + +
    +
    + value + +
    + +
    +
    diff --git a/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html b/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html index 225d820e9..2a0cd5143 100644 --- a/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html +++ b/app/docker/components/host-view-panels/swarm-node-details-panel/swarm-node-details-panel.html @@ -45,7 +45,7 @@ - + - - -
    @@ -96,11 +81,6 @@
    - Remove -
    -
    - -
    - - - The Role-Based Access Control extension is required to select a specific role. - -
    -
    +
    @@ -67,10 +57,8 @@ title-icon="fa-user-lock" table-key="{{ 'access_' + ctrl.entityType }}" order-by="Name" - rbac-enabled="ctrl.rbacEnabled && ctrl.entityType !== 'registry'" inherit-from="ctrl.inheritFrom" dataset="ctrl.authorizedUsersAndTeams" - roles="ctrl.roles" update-action="ctrl.updateAction" remove-action="ctrl.unauthorizeAccess" > diff --git a/app/portainer/components/accessManagement/porAccessManagementController.js b/app/portainer/components/accessManagement/porAccessManagementController.js index 1234d3f3c..460c302d9 100644 --- a/app/portainer/components/accessManagement/porAccessManagementController.js +++ b/app/portainer/components/accessManagement/porAccessManagementController.js @@ -4,11 +4,9 @@ import angular from 'angular'; class PorAccessManagementController { /* @ngInject */ - constructor(Notifications, ExtensionService, AccessService, RoleService) { + constructor(Notifications, AccessService) { this.Notifications = Notifications; - this.ExtensionService = ExtensionService; this.AccessService = AccessService; - this.RoleService = RoleService; this.unauthorizeAccess = this.unauthorizeAccess.bind(this); this.updateAction = this.updateAction.bind(this); @@ -31,11 +29,10 @@ class PorAccessManagementController { const entity = this.accessControlledEntity; const oldUserAccessPolicies = entity.UserAccessPolicies; const oldTeamAccessPolicies = entity.TeamAccessPolicies; - const selectedRoleId = this.rbacEnabled ? this.formValues.selectedRole.Id : 0; const selectedUserAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'user'); const selectedTeamAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'team'); - const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId); + const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, 0); this.accessControlledEntity.UserAccessPolicies = accessPolicies.userAccessPolicies; this.accessControlledEntity.TeamAccessPolicies = accessPolicies.teamAccessPolicies; this.updateAccess(); @@ -56,18 +53,7 @@ class PorAccessManagementController { try { const entity = this.accessControlledEntity; const parent = this.inheritFrom; - // TODO: refactor - // extract this code and locate it in AccessService.accesses() function - // see resourcePoolAccessController for another usage of AccessService.accesses() - // which needs RBAC support - this.roles = []; - this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC); - if (this.rbacEnabled) { - this.roles = await this.RoleService.roles(); - this.formValues = { - selectedRole: this.roles[0], - }; - } + const data = await this.AccessService.accesses(entity, parent, this.roles); this.availableUsersAndTeams = _.orderBy(data.availableUsersAndTeams, 'Name', 'asc'); this.authorizedUsersAndTeams = data.authorizedUsersAndTeams; diff --git a/app/portainer/components/extension-list/extension-item/extension-item.js b/app/portainer/components/extension-list/extension-item/extension-item.js deleted file mode 100644 index e2d4b0723..000000000 --- a/app/portainer/components/extension-list/extension-item/extension-item.js +++ /dev/null @@ -1,8 +0,0 @@ -angular.module('portainer.app').component('extensionItem', { - templateUrl: './extensionItem.html', - controller: 'ExtensionItemController', - bindings: { - model: '<', - currentDate: '<', - }, -}); diff --git a/app/portainer/components/extension-list/extension-item/extensionItem.html b/app/portainer/components/extension-list/extension-item/extensionItem.html deleted file mode 100644 index 18a41ccc4..000000000 --- a/app/portainer/components/extension-list/extension-item/extensionItem.html +++ /dev/null @@ -1,47 +0,0 @@ - -
    -
    - - - - - - - - - - -
    - - - {{ $ctrl.model.Name }} - - - - coming soon - deal - expired - enabled - update available - -
    - - -
    - - - {{ $ctrl.model.ShortDescription }} - - - - Licensed to {{ $ctrl.model.License.Company }} - Expires on {{ $ctrl.model.License.Expiration }} - -
    - -
    - -
    - -
    diff --git a/app/portainer/components/extension-list/extension-item/extensionItemController.js b/app/portainer/components/extension-list/extension-item/extensionItemController.js deleted file mode 100644 index 8bf0d218c..000000000 --- a/app/portainer/components/extension-list/extension-item/extensionItemController.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('portainer.app').controller('ExtensionItemController', [ - '$state', - function ($state) { - var ctrl = this; - ctrl.goToExtensionView = goToExtensionView; - - function goToExtensionView() { - if (ctrl.model.Available) { - $state.go('portainer.extensions.extension', { id: ctrl.model.Id }); - } - } - }, -]); diff --git a/app/portainer/components/extension-list/extension-list.js b/app/portainer/components/extension-list/extension-list.js deleted file mode 100644 index 5b9b0a055..000000000 --- a/app/portainer/components/extension-list/extension-list.js +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('portainer.app').component('extensionList', { - templateUrl: './extensionList.html', - bindings: { - extensions: '<', - currentDate: '<', - }, -}); diff --git a/app/portainer/components/extension-list/extensionList.html b/app/portainer/components/extension-list/extensionList.html deleted file mode 100644 index 2673b28d3..000000000 --- a/app/portainer/components/extension-list/extensionList.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - - -
    -
    Available extensions
    -
    - -
    - -
    -
    -
    -
    diff --git a/app/portainer/models/extension.js b/app/portainer/models/extension.js deleted file mode 100644 index 2d64182d5..000000000 --- a/app/portainer/models/extension.js +++ /dev/null @@ -1,17 +0,0 @@ -export function ExtensionViewModel(data) { - this.Id = data.Id; - this.Name = data.Name; - this.Enabled = data.Enabled; - this.Description = data.Description; - this.Price = data.Price; - this.PriceDescription = data.PriceDescription; - this.Available = data.Available; - this.Deal = data.Deal; - this.ShortDescription = data.ShortDescription; - this.License = data.License; - this.Version = data.Version; - this.UpdateAvailable = data.UpdateAvailable; - this.ShopURL = data.ShopURL; - this.Images = data.Images; - this.Logo = data.Logo; -} diff --git a/app/portainer/rest/extension.js b/app/portainer/rest/extension.js deleted file mode 100644 index d93f5e873..000000000 --- a/app/portainer/rest/extension.js +++ /dev/null @@ -1,18 +0,0 @@ -angular.module('portainer.app').factory('Extension', [ - '$resource', - 'API_ENDPOINT_EXTENSIONS', - function ExtensionFactory($resource, API_ENDPOINT_EXTENSIONS) { - 'use strict'; - return $resource( - API_ENDPOINT_EXTENSIONS + '/:id/:action', - {}, - { - create: { method: 'POST' }, - query: { method: 'GET', isArray: true }, - get: { method: 'GET', params: { id: '@id' } }, - delete: { method: 'DELETE', params: { id: '@id' } }, - update: { method: 'POST', params: { id: '@id', action: 'update' } }, - } - ); - }, -]); diff --git a/app/portainer/services/api/accessService.js b/app/portainer/services/api/accessService.js index 2c9acd0dd..d09f4bdfe 100644 --- a/app/portainer/services/api/accessService.js +++ b/app/portainer/services/api/accessService.js @@ -11,15 +11,7 @@ angular.module('portainer.app').factory('AccessService', [ 'use strict'; var service = {}; - function _getRole(roles, roleId) { - if (roles.length) { - const role = _.find(roles, (role) => role.Id === roleId); - return role ? role : { Id: 0, Name: '-' }; - } - return { Id: 0, Name: '-' }; - } - - function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies, roles) { + function _mapAccessData(accesses, authorizedPolicies, inheritedPolicies) { var availableAccesses = []; var authorizedAccesses = []; @@ -30,14 +22,11 @@ angular.module('portainer.app').factory('AccessService', [ const inherited = inheritedPolicies && inheritedPolicies[access.Id]; if (authorized && inherited) { - access.Role = _getRole(roles, authorizedPolicies[access.Id].RoleId); access.Override = true; authorizedAccesses.push(access); } else if (authorized && !inherited) { - access.Role = _getRole(roles, authorizedPolicies[access.Id].RoleId); authorizedAccesses.push(access); } else if (!authorized && inherited) { - access.Role = _getRole(roles, inheritedPolicies[access.Id].RoleId); access.Inherited = true; authorizedAccesses.push(access); availableAccesses.push(access); @@ -52,7 +41,7 @@ angular.module('portainer.app').factory('AccessService', [ }; } - function getAccesses(authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies, roles) { + function getAccesses(authorizedUserPolicies, authorizedTeamPolicies, inheritedUserPolicies, inheritedTeamPolicies) { var deferred = $q.defer(); $q.all({ @@ -67,8 +56,8 @@ angular.module('portainer.app').factory('AccessService', [ return new TeamAccessViewModel(team); }); - var userAccessData = _mapAccessData(userAccesses, authorizedUserPolicies, inheritedUserPolicies, roles); - var teamAccessData = _mapAccessData(teamAccesses, authorizedTeamPolicies, inheritedTeamPolicies, roles); + var userAccessData = _mapAccessData(userAccesses, authorizedUserPolicies, inheritedUserPolicies); + var teamAccessData = _mapAccessData(teamAccesses, authorizedTeamPolicies, inheritedTeamPolicies); var accessData = { availableUsersAndTeams: userAccessData.available.concat(teamAccessData.available), @@ -84,7 +73,7 @@ angular.module('portainer.app').factory('AccessService', [ return deferred.promise; } - async function accessesAsync(entity, parent, roles) { + async function accessesAsync(entity, parent) { try { if (!entity) { throw { msg: 'Unable to retrieve accesses' }; @@ -101,14 +90,14 @@ angular.module('portainer.app').factory('AccessService', [ if (parent && !parent.TeamAccessPolicies) { parent.TeamAccessPolicies = {}; } - return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {}, roles); + return await getAccesses(entity.UserAccessPolicies, entity.TeamAccessPolicies, parent ? parent.UserAccessPolicies : {}, parent ? parent.TeamAccessPolicies : {}); } catch (err) { throw err; } } - function accesses(entity, parent, roles) { - return $async(accessesAsync, entity, parent, roles); + function accesses(entity, parent) { + return $async(accessesAsync, entity, parent); } service.accesses = accesses; diff --git a/app/portainer/services/api/extensionService.js b/app/portainer/services/api/extensionService.js deleted file mode 100644 index 576dc3e12..000000000 --- a/app/portainer/services/api/extensionService.js +++ /dev/null @@ -1,102 +0,0 @@ -import _ from 'lodash-es'; -import { ExtensionViewModel } from '../../models/extension'; - -angular.module('portainer.app').factory('ExtensionService', [ - '$q', - 'Extension', - 'StateManager', - '$async', - 'FileUploadService', - function ExtensionServiceFactory($q, Extension, StateManager, $async, FileUploadService) { - 'use strict'; - var service = {}; - - service.EXTENSIONS = Object.freeze({ - REGISTRY_MANAGEMENT: 1, - OAUTH_AUTHENTICATION: 2, - RBAC: 3, - }); - - service.enable = enable; - service.update = update; - service.delete = _delete; - service.extensions = extensions; - service.extension = extension; - service.extensionEnabled = extensionEnabled; - service.retrieveAndSaveEnabledExtensions = retrieveAndSaveEnabledExtensions; - - function enable(license, extensionFile) { - if (extensionFile) { - return FileUploadService.uploadExtension(license, extensionFile); - } else { - return Extension.create({ license: license }).$promise; - } - } - - function update(id, version) { - return Extension.update({ id: id, version: version }).$promise; - } - - function _delete(id) { - return Extension.delete({ id: id }).$promise; - } - - function extensions(store) { - var deferred = $q.defer(); - - Extension.query({ store: store }) - .$promise.then(function success(data) { - var extensions = data.map(function (item) { - return new ExtensionViewModel(item); - }); - deferred.resolve(extensions); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve extensions', err: err }); - }); - - return deferred.promise; - } - - function extension(id) { - var deferred = $q.defer(); - - Extension.get({ id: id }) - .$promise.then(function success(data) { - var extension = new ExtensionViewModel(data); - deferred.resolve(extension); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to retrieve extension details', err: err }); - }); - - return deferred.promise; - } - - function extensionEnabled(extensionId) { - return $async(extensionsEnabledAsync, extensionId); - } - - async function extensionsEnabledAsync(extensionId) { - if (extensionId === service.EXTENSIONS.RBAC) { - return StateManager.getExtension(extensionId) ? true : false; - } else { - const extensions = await service.extensions(false); - const extension = _.find(extensions, (ext) => ext.Id === extensionId); - return extension ? extension.Enabled : false; - } - } - - function retrieveAndSaveEnabledExtensions() { - return $async(retrieveAndSaveEnabledExtensionsAsync); - } - - async function retrieveAndSaveEnabledExtensionsAsync() { - const extensions = await service.extensions(false); - _.forEach(extensions, (ext) => delete ext.License); - StateManager.saveExtensions(extensions); - } - - return service; - }, -]); diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index fe581645a..b39a151a8 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -197,19 +197,6 @@ angular.module('portainer.app').factory('FileUploadService', [ return $q.all(queue); }; - service.uploadExtension = function (license, extensionFile) { - const payload = { - License: license, - file: extensionFile, - ArchiveFileName: extensionFile.name, - }; - return Upload.upload({ - url: 'api/extensions/upload', - data: payload, - ignoreLoadingBar: true, - }); - }; - return service; }, ]); diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index 95bc618d5..5026331bc 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -50,12 +50,6 @@ angular.module('portainer.app').factory('LocalStorage', [ getUIState: function () { return localStorageService.get('UI_STATE'); }, - storeExtensionState: function (state) { - localStorageService.set('EXTENSION_STATE', state); - }, - getExtensionState: function () { - return localStorageService.get('EXTENSION_STATE'); - }, storeJWT: function (jwt) { localStorageService.set('JWT', jwt); }, @@ -145,7 +139,7 @@ angular.module('portainer.app').factory('LocalStorage', [ localStorageService.clearAll(); }, cleanAuthData() { - localStorageService.remove('JWT', 'EXTENSION_STATE', 'APPLICATION_STATE', 'LOGIN_STATE_UUID'); + localStorageService.remove('JWT', 'APPLICATION_STATE', 'LOGIN_STATE_UUID'); }, cleanEndpointData() { localStorageService.remove('ENDPOINT_ID', 'ENDPOINT_PUBLIC_URL', 'ENDPOINT_OFFLINE_MODE', 'ENDPOINTS_DATA', 'ENDPOINT_STATE'); diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 600ee21a5..70520cb7d 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -1,4 +1,3 @@ -import _ from 'lodash-es'; import moment from 'moment'; angular.module('portainer.app').factory('StateManager', [ @@ -60,7 +59,6 @@ angular.module('portainer.app').factory('StateManager', [ manager.clean = function () { state.endpoint = {}; - state.extensions = []; }; manager.updateLogo = function (logoURL) { @@ -174,11 +172,6 @@ angular.module('portainer.app').factory('StateManager', [ state.UI = UIState; } - const extensionState = LocalStorage.getExtensionState(); - if (extensionState) { - state.extensions = extensionState; - } - var endpointState = LocalStorage.getEndpointState(); if (endpointState) { state.endpoint = endpointState; @@ -276,19 +269,6 @@ angular.module('portainer.app').factory('StateManager', [ return state.endpoint.agentApiVersion; }; - manager.saveExtensions = function (extensions) { - state.extensions = extensions; - LocalStorage.storeExtensionState(state.extensions); - }; - - manager.getExtensions = function () { - return state.extensions; - }; - - manager.getExtension = function (extensionId) { - return _.find(state.extensions, { Id: extensionId, Enabled: true }); - }; - return manager; }, ]); diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index a2acfc0be..ff44d2669 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -12,7 +12,6 @@ class AuthenticationController { Authentication, UserService, EndpointService, - ExtensionService, StateManager, Notifications, SettingsService, @@ -28,7 +27,6 @@ class AuthenticationController { this.Authentication = Authentication; this.UserService = UserService; this.EndpointService = EndpointService; - this.ExtensionService = ExtensionService; this.StateManager = StateManager; this.Notifications = Notifications; this.SettingsService = SettingsService; @@ -47,7 +45,6 @@ class AuthenticationController { OAuthProvider: '', }; - this.retrieveAndSaveEnabledExtensionsAsync = this.retrieveAndSaveEnabledExtensionsAsync.bind(this); this.checkForEndpointsAsync = this.checkForEndpointsAsync.bind(this); this.checkForLatestVersionAsync = this.checkForLatestVersionAsync.bind(this); this.postLoginSteps = this.postLoginSteps.bind(this); @@ -117,14 +114,6 @@ class AuthenticationController { * POST LOGIN STEPS SECTION */ - async retrieveAndSaveEnabledExtensionsAsync() { - try { - await this.ExtensionService.retrieveAndSaveEnabledExtensions(); - } catch (err) { - this.error(err, 'Unable to retrieve enabled extensions'); - } - } - async checkForEndpointsAsync() { try { const endpoints = await this.EndpointService.endpoints(0, 1); @@ -158,7 +147,6 @@ class AuthenticationController { } async postLoginSteps() { - await this.retrieveAndSaveEnabledExtensionsAsync(); await this.checkForEndpointsAsync(); await this.checkForLatestVersionAsync(); } diff --git a/app/portainer/views/extensions/extensions.html b/app/portainer/views/extensions/extensions.html deleted file mode 100644 index 2f50d4138..000000000 --- a/app/portainer/views/extensions/extensions.html +++ /dev/null @@ -1,129 +0,0 @@ - - - Portainer extensions - - - - -

    - Portainer CE is a great way of managing clusters, provisioning containers and services and managing container environment lifecycles. To extend the benefit of Portainer CE - even more, and to address the needs of larger, complex or critical environments, the Portainer team provides a growing range of low-cost Extensions. -

    - -

    - To ensure that Portainer remains the best choice for managing production container platforms, the Portainer team have chosen a modular, extensible design approach, where - additional capability can be added to the Portainer CE core as needed, and at very low cost. -

    - -

    - Available through a simple subscription process from the list below, Portainer Extensions provide a simple way to enhance Portainer CE’s core functionality through - incremental capability in important areas. -

    - -

    - For additional information on Portainer Extensions, see our website - here. -

    -
    -
    - -
    -
    - - -
    -
    - Enable extension -
    - -
    -
    -

    - Portainer will download the latest version of the extension. Ensure that you have a valid license. -

    -

    - You will need to upload the extension archive manually. Ensure that you have a valid license. -

    -

    - You can download the latest version of our extensions here. -

    -

    - - Switch to offline activation - - - Switch to online activation - -

    -
    -
    - -
    - -
    - -
    -
    - -
    -
    -
    -

    This field is required.

    -

    Invalid license format.

    -
    -
    -
    - -
    -
    - - - {{ formValues.ExtensionFile.name }} - - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    - - - -

    - - Portainer must be connected to the Internet to fetch the list of available extensions. -

    -
    -
    diff --git a/app/portainer/views/extensions/extensionsController.js b/app/portainer/views/extensions/extensionsController.js deleted file mode 100644 index 1404ec7fa..000000000 --- a/app/portainer/views/extensions/extensionsController.js +++ /dev/null @@ -1,67 +0,0 @@ -import moment from 'moment'; - -angular.module('portainer.app').controller('ExtensionsController', [ - '$scope', - '$state', - 'ExtensionService', - 'Notifications', - function ($scope, $state, ExtensionService, Notifications) { - $scope.state = { - actionInProgress: false, - currentDate: moment().format('YYYY-MM-dd'), - }; - - $scope.formValues = { - License: '', - ExtensionFile: null, - }; - - function initView() { - ExtensionService.extensions(true) - .then(function onSuccess(data) { - $scope.extensions = data; - }) - .catch(function onError(err) { - $scope.extensions = []; - Notifications.error('Failure', err, 'Unable to access extension store'); - }); - } - - $scope.enableExtension = function () { - const license = $scope.formValues.License; - const extensionFile = $scope.formValues.ExtensionFile; - - $scope.state.actionInProgress = true; - ExtensionService.enable(license, extensionFile) - .then(function onSuccess() { - return ExtensionService.retrieveAndSaveEnabledExtensions(); - }) - .then(function () { - Notifications.success('Extension successfully enabled'); - $state.reload(); - }) - .catch(function onError(err) { - Notifications.error('Failure', err, 'Unable to enable extension'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; - - $scope.isValidLicenseFormat = function (form) { - var valid = true; - - if (!$scope.formValues.License) { - return; - } - - if (isNaN($scope.formValues.License[0])) { - valid = false; - } - - form.extension_license.$setValidity('invalidLicense', valid); - }; - - initView(); - }, -]); diff --git a/app/portainer/views/extensions/inspect/extension.html b/app/portainer/views/extensions/inspect/extension.html deleted file mode 100644 index 4ac2b782e..000000000 --- a/app/portainer/views/extensions/inspect/extension.html +++ /dev/null @@ -1,196 +0,0 @@ - - - Portainer extensions > {{ extension.Name }} - - -
    -
    - - -
    -
    -
    -
    {{ extension.Name }} extension
    - - -
    - -
    -
    - {{ extension.ShortDescription }} -
    -
    -
    - -
    -
    -
    - Enabled - Expired - {{ extension.Price }} -
    - -
    - {{ extension.PriceDescription }} -
    - -
    - -
    - -
    -
    - - - - - -
    - Coming soon -
    - -
    - -
    - -
    - -
    - - -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - -
    -
    - - Offline update - -
    -
    -
    -

    - You will need to upload the extension archive manually. You can download the latest version of our extensions - here. -

    -
    -
    -
    -
    - - - {{ formValues.ExtensionFile.name }} - - -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - -
    -
    - - -
    - - Description - -
    -
    -
    -
    -
    -
    -

    - - Unable to provide a description in an offline environment. -

    -
    -
    -
    -
    -
    -
    - -
    -
    - - -
    - - Screenshots - -
    -
    -
    - -
    -
    -
    -
    -
    -
    diff --git a/app/portainer/views/extensions/inspect/extensionController.js b/app/portainer/views/extensions/inspect/extensionController.js deleted file mode 100644 index 13e383d58..000000000 --- a/app/portainer/views/extensions/inspect/extensionController.js +++ /dev/null @@ -1,90 +0,0 @@ -angular.module('portainer.app').controller('ExtensionController', [ - '$q', - '$scope', - '$transition$', - '$state', - 'ExtensionService', - 'Notifications', - 'ModalService', - function ($q, $scope, $transition$, $state, ExtensionService, Notifications, ModalService) { - $scope.state = { - onlineUpdateInProgress: false, - offlineUpdateInProgress: false, - deleteInProgress: false, - offlineUpdate: false, - }; - - $scope.formValues = { - instances: 1, - extensionFile: null, - }; - - $scope.updateExtensionOnline = updateExtensionOnline; - $scope.updateExtensionOffline = updateExtensionOffline; - $scope.deleteExtension = deleteExtension; - $scope.enlargeImage = enlargeImage; - - function enlargeImage(image) { - ModalService.enlargeImage(image); - } - - function deleteExtension(extension) { - $scope.state.deleteInProgress = true; - ExtensionService.delete(extension.Id) - .then(function onSuccess() { - Notifications.success('Extension successfully deleted'); - $state.go('portainer.extensions'); - }) - .catch(function onError(err) { - Notifications.error('Failure', err, 'Unable to delete extension'); - }) - .finally(function final() { - $scope.state.deleteInProgress = false; - }); - } - - function updateExtensionOnline(extension) { - $scope.state.onlineUpdateInProgress = true; - ExtensionService.update(extension.Id, extension.Version) - .then(function onSuccess() { - Notifications.success('Extension successfully updated'); - $state.reload(); - }) - .catch(function onError(err) { - Notifications.error('Failure', err, 'Unable to update extension'); - }) - .finally(function final() { - $scope.state.onlineUpdateInProgress = false; - }); - } - - function updateExtensionOffline(extension) { - $scope.state.offlineUpdateInProgress = true; - const extensionFile = $scope.formValues.ExtensionFile; - - ExtensionService.enable(extension.License.LicenseKey, extensionFile) - .then(function onSuccess() { - Notifications.success('Extension successfully updated'); - $state.reload(); - }) - .catch(function onError(err) { - Notifications.error('Failure', err, 'Unable to update extension'); - }) - .finally(function final() { - $scope.state.offlineUpdateInProgress = false; - }); - } - - function initView() { - ExtensionService.extension($transition$.params().id) - .then(function onSuccess(extension) { - $scope.extension = extension; - }) - .catch(function onError(err) { - Notifications.error('Failure', err, 'Unable to retrieve extension information'); - }); - } - - initView(); - }, -]); diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js index 1f7826019..65ccb4bb5 100644 --- a/app/portainer/views/init/admin/initAdminController.js +++ b/app/portainer/views/init/admin/initAdminController.js @@ -8,8 +8,7 @@ angular.module('portainer.app').controller('InitAdminController', [ 'SettingsService', 'UserService', 'EndpointService', - 'ExtensionService', - function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, ExtensionService) { + function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService) { $scope.logo = StateManager.getState().application.logo; $scope.formValues = { @@ -23,18 +22,6 @@ angular.module('portainer.app').controller('InitAdminController', [ actionInProgress: false, }; - function retrieveAndSaveEnabledExtensions() { - return $async(retrieveAndSaveEnabledExtensionsAsync); - } - - async function retrieveAndSaveEnabledExtensionsAsync() { - try { - await ExtensionService.retrieveAndSaveEnabledExtensions(); - } catch (err) { - Notifications.error('Failure', err, 'Unable to retrieve enabled extensions'); - } - } - $scope.createAdminUser = function () { var username = $scope.formValues.Username; var password = $scope.formValues.Password; @@ -44,9 +31,6 @@ angular.module('portainer.app').controller('InitAdminController', [ .then(function success() { return Authentication.login(username, password); }) - .then(function success() { - return retrieveAndSaveEnabledExtensions(); - }) .then(function success() { StateManager.updateEnableTelemetry($scope.formValues.enableTelemetry); return SettingsService.update({ enableTelemetry: $scope.formValues.enableTelemetry }); diff --git a/app/portainer/views/settings/settings.html b/app/portainer/views/settings/settings.html index d5091ef43..f0a297d6b 100644 --- a/app/portainer/views/settings/settings.html +++ b/app/portainer/views/settings/settings.html @@ -101,10 +101,7 @@
    diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index 012e86ab4..2b974b4a3 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -111,9 +111,6 @@ -
    + +
    + Host and Filesystem +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    - Security + Docker Endpoint Security Options
    @@ -97,26 +122,6 @@
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index fa45a4a38..4fb313acc 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -49,7 +49,7 @@ function _apiPortsToPublishedPorts(pList, pRefs) { } class KubernetesApplicationConverter { - static applicationCommon(res, data, service, ingressRules) { + static applicationCommon(res, data, service, ingresses) { res.Id = data.metadata.uid; res.Name = data.metadata.name; res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-'; @@ -111,7 +111,7 @@ class KubernetesApplicationConverter { const portsRefs = _.concat(..._.map(data.spec.template.spec.containers, (container) => container.ports)); const ports = _apiPortsToPublishedPorts(service.spec.ports, portsRefs); - const rules = KubernetesIngressHelper.findSBoundServiceIngressesRules(ingressRules, service); + const rules = KubernetesIngressHelper.findSBoundServiceIngressesRules(ingresses, service.metadata.name); _.forEach(ports, (port) => (port.IngressRules = _.filter(rules, (rule) => rule.Port === port.Port))); res.PublishedPorts = ports; } @@ -210,9 +210,9 @@ class KubernetesApplicationConverter { ); } - static apiDeploymentToApplication(data, service, ingressRules) { + static apiDeploymentToApplication(data, service, ingresses) { const res = new KubernetesApplication(); - KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules); + KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses); res.ApplicationType = KubernetesApplicationTypes.DEPLOYMENT; res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED; res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED; @@ -221,9 +221,9 @@ class KubernetesApplicationConverter { return res; } - static apiDaemonSetToApplication(data, service, ingressRules) { + static apiDaemonSetToApplication(data, service, ingresses) { const res = new KubernetesApplication(); - KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules); + KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses); res.ApplicationType = KubernetesApplicationTypes.DAEMONSET; res.DeploymentType = KubernetesApplicationDeploymentTypes.GLOBAL; res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.SHARED; @@ -232,9 +232,9 @@ class KubernetesApplicationConverter { return res; } - static apiStatefulSetToapplication(data, service, ingressRules) { + static apiStatefulSetToapplication(data, service, ingresses) { const res = new KubernetesApplication(); - KubernetesApplicationConverter.applicationCommon(res, data, service, ingressRules); + KubernetesApplicationConverter.applicationCommon(res, data, service, ingresses); res.ApplicationType = KubernetesApplicationTypes.STATEFULSET; res.DeploymentType = KubernetesApplicationDeploymentTypes.REPLICATED; res.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED; @@ -261,15 +261,18 @@ class KubernetesApplicationConverter { res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations); res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler, res.ReplicaCount); + res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts); + const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length; if (app.ServiceType === KubernetesServiceTypes.LOAD_BALANCER) { res.PublishingType = KubernetesApplicationPublishingTypes.LOAD_BALANCER; - } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT) { + } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && !isIngress) { res.PublishingType = KubernetesApplicationPublishingTypes.CLUSTER; + } else if (app.ServiceType === KubernetesServiceTypes.NODE_PORT && isIngress) { + res.PublishingType = KubernetesApplicationPublishingTypes.INGRESS; } else { res.PublishingType = KubernetesApplicationPublishingTypes.INTERNAL; } - res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts); return res; } @@ -313,6 +316,7 @@ class KubernetesApplicationConverter { if (!service.Ports.length) { service = undefined; } + return [app, headlessService, service, claims]; } } diff --git a/app/kubernetes/converters/service.js b/app/kubernetes/converters/service.js index a04c04ef0..44755f04b 100644 --- a/app/kubernetes/converters/service.js +++ b/app/kubernetes/converters/service.js @@ -1,4 +1,4 @@ -import _ from 'lodash-es'; +import * as _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; import { KubernetesServiceCreatePayload } from 'Kubernetes/models/service/payloads'; @@ -11,8 +11,9 @@ import { KubernetesServiceHeadlessClusterIP, KubernetesService, KubernetesServic import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models'; import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; -class KubernetesServiceConverter { - static publishedPortToServicePort(name, publishedPort, type) { +function _publishedPortToServicePort(formValues, publishedPort, type) { + if (publishedPort.IsNew || !publishedPort.NeedsDeletion) { + const name = formValues.Name; const res = new KubernetesServicePort(); res.name = _.toLower(name + '-' + publishedPort.ContainerPort + '-' + publishedPort.Protocol); res.port = type === KubernetesServiceTypes.LOAD_BALANCER ? publishedPort.LoadBalancerPort : publishedPort.ContainerPort; @@ -27,7 +28,9 @@ class KubernetesServiceConverter { } return res; } +} +class KubernetesServiceConverter { /** * Generate KubernetesService from KubernetesApplicationFormValues * @param {KubernetesApplicationFormValues} formValues @@ -39,12 +42,13 @@ class KubernetesServiceConverter { res.StackName = formValues.StackName ? formValues.StackName : formValues.Name; res.ApplicationOwner = formValues.ApplicationOwner; res.ApplicationName = formValues.Name; - if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER) { + if (formValues.PublishingType === KubernetesApplicationPublishingTypes.CLUSTER || formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { res.Type = KubernetesServiceTypes.NODE_PORT; } else if (formValues.PublishingType === KubernetesApplicationPublishingTypes.LOAD_BALANCER) { res.Type = KubernetesServiceTypes.LOAD_BALANCER; } - res.Ports = _.map(formValues.PublishedPorts, (item) => KubernetesServiceConverter.publishedPortToServicePort(formValues.Name, item, res.Type)); + const ports = _.map(formValues.PublishedPorts, (item) => _publishedPortToServicePort(formValues, item, res.Type)); + res.Ports = _.uniqBy(_.without(ports, undefined), (p) => p.targetPort + p.protocol); return res; } diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index 11ab8c2fd..f14d493db 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -249,8 +249,14 @@ class KubernetesApplicationHelper { } static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) { - const finalRes = _.map(publishedPorts, (port) => { + const generatePort = (port, rule) => { const res = new KubernetesApplicationPublishedPortFormValue(); + res.IsNew = false; + if (rule) { + res.IngressName = rule.IngressName; + res.IngressRoute = rule.Path; + res.IngressHost = rule.Host; + } res.Protocol = port.Protocol; res.ContainerPort = port.TargetPort; if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) { @@ -260,6 +266,13 @@ class KubernetesApplicationHelper { res.NodePort = port.NodePort; } return res; + }; + + const finalRes = _.flatMap(publishedPorts, (port) => { + if (port.IngressRules.length) { + return _.map(port.IngressRules, (rule) => generatePort(port, rule)); + } + return generatePort(port); }); return finalRes; } diff --git a/app/kubernetes/ingress/constants.js b/app/kubernetes/ingress/constants.js new file mode 100644 index 000000000..22af57a87 --- /dev/null +++ b/app/kubernetes/ingress/constants.js @@ -0,0 +1,4 @@ +export const KubernetesIngressClassAnnotation = 'kubernetes.io/ingress.class'; +export const KubernetesIngressClassMandatoryAnnotations = Object.freeze({ + nginx: { 'nginx.ingress.kubernetes.io/rewrite-target': '/$1' }, +}); diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js index f931fd3ab..311d22e08 100644 --- a/app/kubernetes/ingress/converter.js +++ b/app/kubernetes/ingress/converter.js @@ -1,19 +1,113 @@ import * as _ from 'lodash-es'; -import { KubernetesIngressRule } from './models'; +import * as JsonPatch from 'fast-json-patch'; + +import KubernetesCommonHelper from 'Kubernetes/helpers/commonHelper'; +import { KubernetesIngressRule, KubernetesIngress } from './models'; +import { KubernetesIngressCreatePayload, KubernetesIngressRuleCreatePayload, KubernetesIngressRulePathCreatePayload } from './payloads'; +import { KubernetesIngressClassAnnotation, KubernetesIngressClassMandatoryAnnotations } from './constants'; export class KubernetesIngressConverter { + // TODO: refactor @LP + // currently only allows the first non-empty host to be used as the "configured" host. + // As we currently only allow a single host to be used for a Portianer-managed ingress + // it's working as the only non-empty host will be the one defined by the admin + // but it will take a random existing host for non Portainer ingresses (CLI deployed) + // Also won't support multiple hosts if we make it available in the future static apiToModel(data) { - const rules = _.flatMap(data.spec.rules, (rule) => { - return _.map(rule.http.paths, (path) => { - const ingRule = new KubernetesIngressRule(); - ingRule.ServiceName = path.backend.serviceName; - ingRule.Host = rule.host; - ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined; - ingRule.Port = path.backend.servicePort; - ingRule.Path = path.path; - return ingRule; - }); + let host = undefined; + const paths = _.flatMap(data.spec.rules, (rule) => { + host = host || rule.host; // TODO: refactor @LP - read above + return !rule.http + ? [] + : _.map(rule.http.paths, (path) => { + const ingRule = new KubernetesIngressRule(); + ingRule.IngressName = data.metadata.name; + ingRule.ServiceName = path.backend.serviceName; + ingRule.Host = rule.host || ''; + ingRule.IP = data.status.loadBalancer.ingress ? data.status.loadBalancer.ingress[0].ip : undefined; + ingRule.Port = path.backend.servicePort; + ingRule.Path = path.path; + return ingRule; + }); }); - return rules; + + const res = new KubernetesIngress(); + res.Name = data.metadata.name; + res.Namespace = data.metadata.namespace; + res.Annotations = data.metadata.annotations || {}; + res.IngressClassName = + data.metadata.annotations && data.metadata.annotations[KubernetesIngressClassAnnotation] + ? data.metadata.annotations[KubernetesIngressClassAnnotation] + : data.spec.ingressClassName; + res.Paths = paths; + res.Host = host; + return res; + } + + static applicationFormValuesToIngresses(formValues, serviceName) { + const ingresses = angular.copy(formValues.OriginalIngresses); + _.forEach(formValues.PublishedPorts, (p) => { + const ingress = _.find(ingresses, { Name: p.IngressName }); + if (ingress && p.NeedsDeletion) { + const path = _.find(ingress.Paths, { Port: p.ContainerPort, ServiceName: serviceName, Path: p.IngressRoute }); + _.remove(ingress.Paths, path); + } else if (ingress && p.IsNew) { + const rule = new KubernetesIngressRule(); + rule.IngressName = ingress.Name; + rule.ServiceName = serviceName; + rule.Port = p.ContainerPort; + rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute; + rule.Host = p.IngressHost; + ingress.Paths.push(rule); + } + }); + return ingresses; + } + + static createPayload(data) { + const res = new KubernetesIngressCreatePayload(); + res.metadata.name = data.Name; + res.metadata.namespace = data.Namespace; + res.metadata.annotations = data.Annotations || {}; + res.metadata.annotations[KubernetesIngressClassAnnotation] = data.IngressClassName; + const annotations = KubernetesIngressClassMandatoryAnnotations[data.Name]; + if (annotations) { + _.extend(res.metadata.annotations, annotations); + } + if (data.Paths && data.Paths.length) { + const groups = _.groupBy(data.Paths, 'Host'); + const rules = _.map(groups, (paths, host) => { + const rule = new KubernetesIngressRuleCreatePayload(); + + if (host === 'undefined' || _.isEmpty(host)) { + host = data.Host; + } + if (host === data.PreviousHost && host !== data.Host) { + host = data.Host; + } + KubernetesCommonHelper.assignOrDeleteIfEmpty(rule, 'host', host); + rule.http.paths = _.map(paths, (p) => { + const path = new KubernetesIngressRulePathCreatePayload(); + path.path = p.Path; + path.backend.serviceName = p.ServiceName; + path.backend.servicePort = p.Port; + return path; + }); + return rule; + }); + KubernetesCommonHelper.assignOrDeleteIfEmpty(res, 'spec.rules', rules); + } else if (data.Host) { + res.spec.rules = [{ host: data.Host }]; + } else { + delete res.spec.rules; + } + return res; + } + + static patchPayload(oldData, newData) { + const oldPayload = KubernetesIngressConverter.createPayload(oldData); + const newPayload = KubernetesIngressConverter.createPayload(newData); + const payload = JsonPatch.compare(oldPayload, newPayload); + return payload; } } diff --git a/app/kubernetes/ingress/helper.js b/app/kubernetes/ingress/helper.js index 7090e4596..b40178733 100644 --- a/app/kubernetes/ingress/helper.js +++ b/app/kubernetes/ingress/helper.js @@ -1,7 +1,8 @@ import * as _ from 'lodash-es'; export class KubernetesIngressHelper { - static findSBoundServiceIngressesRules(ingressRules, service) { - return _.filter(ingressRules, (r) => r.ServiceName === service.metadata.name); + static findSBoundServiceIngressesRules(ingresses, serviceName) { + const rules = _.flatMap(ingresses, 'Paths'); + return _.filter(rules, { ServiceName: serviceName }); } } diff --git a/app/kubernetes/ingress/models.js b/app/kubernetes/ingress/models.js index a8486ce94..e7181a48f 100644 --- a/app/kubernetes/ingress/models.js +++ b/app/kubernetes/ingress/models.js @@ -1,16 +1,26 @@ -/** - * KubernetesIngressRule Model - */ -const _KubernetesIngressRule = Object.freeze({ - ServiceName: '', - Host: '', - IP: '', - Port: '', - Path: '', -}); - -export class KubernetesIngressRule { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressRule))); - } +export function KubernetesIngress() { + return { + Name: '', + Namespace: '', + Annotations: {}, + Host: undefined, + PreviousHost: undefined, // only use for RP ingress host edit + Paths: [], + IngressClassName: '', + }; +} + +// TODO: refactor @LP +// rename this model to KubernetesIngressPath (and all it's references) +// as it's conceptually not an ingress rule (element of ingress.spec.rules) +// but a path (element of ingress.spec.rules[].paths) +export function KubernetesIngressRule() { + return { + IngressName: '', + ServiceName: '', + Host: '', + IP: '', + Port: '', + Path: '', + }; } diff --git a/app/kubernetes/ingress/payloads.js b/app/kubernetes/ingress/payloads.js new file mode 100644 index 000000000..23e5ff317 --- /dev/null +++ b/app/kubernetes/ingress/payloads.js @@ -0,0 +1,33 @@ +import { KubernetesCommonMetadataPayload } from 'Kubernetes/models/common/payloads'; + +export function KubernetesIngressCreatePayload() { + return { + metadata: new KubernetesCommonMetadataPayload(), + spec: { + backend: { + serviceName: 'portainer-empty-default-backend', + servicePort: 1, + }, + rules: [], + }, + }; +} + +export function KubernetesIngressRuleCreatePayload() { + return { + host: '', + http: { + paths: [], + }, + }; +} + +export function KubernetesIngressRulePathCreatePayload() { + return { + backend: { + serviceName: '', + servicePort: 0, + }, + path: '', + }; +} diff --git a/app/kubernetes/ingress/rest.js b/app/kubernetes/ingress/rest.js index 42b87a80c..98f2eea97 100644 --- a/app/kubernetes/ingress/rest.js +++ b/app/kubernetes/ingress/rest.js @@ -1,50 +1,47 @@ import { rawResponse } from 'Kubernetes/rest/response/transform'; -angular.module('portainer.kubernetes').factory('KubernetesIngresses', [ - '$resource', - 'API_ENDPOINT_ENDPOINTS', - 'EndpointProvider', - function KubernetesIngressesFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { - 'use strict'; - return function (namespace) { - const url = API_ENDPOINT_ENDPOINTS + '/:endpointId/kubernetes/apis/networking.k8s.io/v1beta1' + (namespace ? '/namespaces/:namespace' : '') + '/ingresses/:id/:action'; - return $resource( - url, - { - endpointId: EndpointProvider.endpointID, - namespace: namespace, +angular.module('portainer.kubernetes').factory('KubernetesIngresses', factory); + +function factory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { + 'use strict'; + return function (namespace) { + const url = `${API_ENDPOINT_ENDPOINTS}/:endpointId/kubernetes/apis/networking.k8s.io/v1beta1${namespace ? '/namespaces/:namespace' : ''}/ingresses/:id/:action`; + return $resource( + url, + { + endpointId: EndpointProvider.endpointID, + namespace: namespace, + }, + { + get: { + method: 'GET', + timeout: 15000, + ignoreLoadingBar: true, }, - { - get: { - method: 'GET', - timeout: 15000, - ignoreLoadingBar: true, + getYaml: { + method: 'GET', + headers: { + Accept: 'application/yaml', }, - getYaml: { - method: 'GET', - headers: { - Accept: 'application/yaml', - }, - transformResponse: rawResponse, - ignoreLoadingBar: true, + transformResponse: rawResponse, + ignoreLoadingBar: true, + }, + create: { method: 'POST' }, + update: { method: 'PUT' }, + patch: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', }, - create: { method: 'POST' }, - update: { method: 'PUT' }, - patch: { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json-patch+json', - }, + }, + rollback: { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json-patch+json', }, - rollback: { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json-patch+json', - }, - }, - delete: { method: 'DELETE' }, - } - ); - }; - }, -]); + }, + delete: { method: 'DELETE' }, + } + ); + }; +} diff --git a/app/kubernetes/ingress/service.js b/app/kubernetes/ingress/service.js index 72ce0ef5d..c31f30585 100644 --- a/app/kubernetes/ingress/service.js +++ b/app/kubernetes/ingress/service.js @@ -12,6 +12,9 @@ class KubernetesIngressService { this.getAsync = this.getAsync.bind(this); this.getAllAsync = this.getAllAsync.bind(this); + this.createAsync = this.createAsync.bind(this); + this.patchAsync = this.patchAsync.bind(this); + this.deleteAsync = this.deleteAsync.bind(this); } /** @@ -48,6 +51,66 @@ class KubernetesIngressService { } return this.$async(this.getAllAsync, namespace); } + + /** + * CREATE + */ + async createAsync(formValues) { + try { + const params = {}; + const payload = KubernetesIngressConverter.createPayload(formValues); + const namespace = payload.metadata.namespace; + const data = await this.KubernetesIngresses(namespace).create(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to create ingress', err); + } + } + + create(formValues) { + return this.$async(this.createAsync, formValues); + } + + /** + * PATCH + */ + async patchAsync(oldIngress, newIngress) { + try { + const params = new KubernetesCommonParams(); + params.id = newIngress.Name; + const namespace = newIngress.Namespace; + const payload = KubernetesIngressConverter.patchPayload(oldIngress, newIngress); + if (!payload.length) { + return; + } + const data = await this.KubernetesIngresses(namespace).patch(params, payload).$promise; + return data; + } catch (err) { + throw new PortainerError('Unable to patch ingress', err); + } + } + + patch(oldIngress, newIngress) { + return this.$async(this.patchAsync, oldIngress, newIngress); + } + + /** + * DELETE + */ + async deleteAsync(ingress) { + try { + const params = new KubernetesCommonParams(); + params.id = ingress.Name; + const namespace = ingress.Namespace; + await this.KubernetesIngresses(namespace).delete(params).$promise; + } catch (err) { + throw new PortainerError('Unable to delete ingress', err); + } + } + + delete(ingress) { + return this.$async(this.deleteAsync, ingress); + } } export default KubernetesIngressService; diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 7ca9ca1b9..8b658141a 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -22,6 +22,7 @@ const _KubernetesApplicationFormValues = Object.freeze({ DataAccessPolicy: KubernetesApplicationDataAccessPolicies.SHARED, Configurations: [], // KubernetesApplicationConfigurationFormValue list AutoScaler: {}, + OriginalIngresses: undefined, }); export class KubernetesApplicationFormValues { @@ -106,18 +107,19 @@ export class KubernetesApplicationPersistedFolderFormValue { /** * KubernetesApplicationPublishedPortFormValue Model */ -const _KubernetesApplicationPublishedPortFormValue = Object.freeze({ - ContainerPort: '', - NodePort: '', - LoadBalancerPort: '', - LoadBalancerNodePort: undefined, // only filled to save existing loadbalancer nodePort and drop it when moving app exposure from LB to Internal/NodePort - Protocol: 'TCP', -}); - -export class KubernetesApplicationPublishedPortFormValue { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPublishedPortFormValue))); - } +export function KubernetesApplicationPublishedPortFormValue() { + return { + NeedsDeletion: false, + IsNew: true, + ContainerPort: '', + NodePort: '', + LoadBalancerPort: '', + LoadBalancerNodePort: undefined, // only filled to save existing loadbalancer nodePort and drop it when moving app exposure from LB to Internal/NodePort + Protocol: 'TCP', + IngressName: undefined, + IngressRoute: undefined, + IngressHost: undefined, + }; } /** diff --git a/app/kubernetes/models/application/models/constants.js b/app/kubernetes/models/application/models/constants.js index f8cfa0ff1..3be2fcdfb 100644 --- a/app/kubernetes/models/application/models/constants.js +++ b/app/kubernetes/models/application/models/constants.js @@ -24,6 +24,7 @@ export const KubernetesApplicationPublishingTypes = Object.freeze({ INTERNAL: 1, CLUSTER: 2, LOAD_BALANCER: 3, + INGRESS: 4, }); export const KubernetesApplicationQuotaDefaults = { diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js new file mode 100644 index 000000000..f59446cee --- /dev/null +++ b/app/kubernetes/models/resource-pool/formValues.js @@ -0,0 +1,22 @@ +export function KubernetesResourcePoolFormValues(defaults) { + return { + MemoryLimit: defaults.MemoryLimit, + CpuLimit: defaults.CpuLimit, + HasQuota: true, + IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue + }; +} + +/** + * @param {string} ingressClassName + */ +export function KubernetesResourcePoolIngressClassFormValue(ingressClassName) { + return { + Name: ingressClassName, + IngressClassName: ingressClassName, + Host: undefined, + Selected: false, + WasSelected: false, + Namespace: undefined, // will be filled inside ResourcePoolService.create + }; +} diff --git a/app/kubernetes/models/resource-pool/models.js b/app/kubernetes/models/resource-pool/models.js index 57fa71df9..0e7a67d62 100644 --- a/app/kubernetes/models/resource-pool/models.js +++ b/app/kubernetes/models/resource-pool/models.js @@ -3,18 +3,13 @@ export const KubernetesPortainerResourcePoolNameLabel = 'io.portainer.kubernetes export const KubernetesPortainerResourcePoolOwnerLabel = 'io.portainer.kubernetes.resourcepool.owner'; /** - * KubernetesResourcePool Model (Composite) - * ResourcePool is a composite model that includes - * A Namespace and a Quota + * KubernetesResourcePool Model */ -const _KubernetesResourcePool = Object.freeze({ - Namespace: {}, // KubernetesNamespace - Quota: undefined, // KubernetesResourceQuota - Yaml: '', -}); - -export class KubernetesResourcePool { - constructor() { - Object.assign(this, JSON.parse(JSON.stringify(_KubernetesResourcePool))); - } +export function KubernetesResourcePool() { + return { + Namespace: {}, // KubernetesNamespace + Quota: undefined, // KubernetesResourceQuota, + Ingresses: [], // KubernetesIngress[] + Yaml: '', + }; } diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index 614267068..1afe88de5 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -1,8 +1,8 @@ -import _ from 'lodash-es'; +import * as _ from 'lodash-es'; import angular from 'angular'; import PortainerError from 'Portainer/error'; -import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; +import { KubernetesApplicationTypes, KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import KubernetesApplicationRollbackHelper from 'Kubernetes/helpers/application/rollback'; import KubernetesApplicationConverter from 'Kubernetes/converters/application'; @@ -13,8 +13,10 @@ import { KubernetesApplication } from 'Kubernetes/models/application/models'; import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper'; import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter'; +import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter'; class KubernetesApplicationService { + /* #region CONSTRUCTOR */ /* @ngInject */ constructor( $async, @@ -53,10 +55,9 @@ class KubernetesApplicationService { this.rollbackAsync = this.rollbackAsync.bind(this); this.deleteAsync = this.deleteAsync.bind(this); } + /* #endregion */ - /** - * UTILS - */ + /* #region UTILS */ _getApplicationApiService(app) { let apiService; if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) { @@ -71,9 +72,15 @@ class KubernetesApplicationService { return apiService; } - /** - * GET - */ + _generateIngressPatchPromises(oldIngresses, newIngresses) { + return _.map(newIngresses, (newIng) => { + const oldIng = _.find(oldIngresses, { Name: newIng.Name }); + return this.KubernetesIngressService.patch(oldIng, newIng); + }); + } + /* #endregion */ + + /* #region GET */ async getAsync(namespace, name) { try { const [deployment, daemonSet, statefulSet, pods, autoScalers, ingresses] = await Promise.allSettled([ @@ -121,7 +128,7 @@ class KubernetesApplicationService { if (scaler && scaler.Yaml) { application.Yaml += '---\n' + scaler.Yaml; } - // TODO: refactor + // TODO: refactor @LP // append ingress yaml ? return application; } catch (err) { @@ -185,10 +192,9 @@ class KubernetesApplicationService { } return this.$async(this.getAllAsync, namespace); } + /* #endregion */ - /** - * CREATE - */ + /* #region CREATE */ // TODO: review // resource creation flow // should we keep formValues > Resource_1 || Resource_2 @@ -199,6 +205,10 @@ class KubernetesApplicationService { if (service) { await this.KubernetesServiceService.create(service); + if (formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, service.Name); + await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); + } } const apiService = this._getApplicationApiService(app); @@ -231,10 +241,9 @@ class KubernetesApplicationService { create(formValues) { return this.$async(this.createAsync, formValues); } + /* #endregion */ - /** - * PATCH - */ + /* #region PATCH */ // this function accepts KubernetesApplicationFormValues as parameters async patchAsync(oldFormValues, newFormValues) { try { @@ -269,10 +278,23 @@ class KubernetesApplicationService { if (oldService && newService) { await this.KubernetesServiceService.patch(oldService, newService); + if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name); + const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name); + await Promise.all(this._generateIngressPatchPromises(oldIngresses, newIngresses)); + } } else if (!oldService && newService) { await this.KubernetesServiceService.create(newService); + if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name); + await Promise.all(this._generateIngressPatchPromises(newFormValues.OriginalIngresses, ingresses)); + } } else if (oldService && !newService) { await this.KubernetesServiceService.delete(oldService); + if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { + const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name); + await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); + } } const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp); @@ -327,10 +349,9 @@ class KubernetesApplicationService { } return this.$async(this.patchAsync, oldValues, newValues); } + /* #endregion */ - /** - * DELETE - */ + /* #region DELETE */ async deleteAsync(application) { try { const payload = { @@ -351,8 +372,18 @@ class KubernetesApplicationService { if (application.ServiceType) { await this.KubernetesServiceService.delete(servicePayload); + const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length; + if (isIngress) { + const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace); + const formValues = { + OriginalIngresses: originalIngresses, + PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts), + }; + _.forEach(formValues.PublishedPorts, (p) => (p.NeedsDeletion = true)); + const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, servicePayload.Name); + await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); + } } - if (!_.isEmpty(application.AutoScaler)) { await this.KubernetesHorizontalPodAutoScalerService.delete(application.AutoScaler); } @@ -364,10 +395,9 @@ class KubernetesApplicationService { delete(application) { return this.$async(this.deleteAsync, application); } + /* #endregion */ - /** - * ROLLBACK - */ + /* #region ROLLBACK */ async rollbackAsync(application, targetRevision) { try { const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision); @@ -381,6 +411,7 @@ class KubernetesApplicationService { rollback(application, targetRevision) { return this.$async(this.rollbackAsync, application, targetRevision); } + /* #endregion */ } export default KubernetesApplicationService; diff --git a/app/kubernetes/services/resourcePoolService.js b/app/kubernetes/services/resourcePoolService.js index 7dbfb933b..7de236f21 100644 --- a/app/kubernetes/services/resourcePoolService.js +++ b/app/kubernetes/services/resourcePoolService.js @@ -1,17 +1,19 @@ -import _ from 'lodash-es'; +import * as _ from 'lodash-es'; import { KubernetesResourceQuota } from 'Kubernetes/models/resource-quota/models'; import angular from 'angular'; import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool'; import KubernetesResourceQuotaHelper from 'Kubernetes/helpers/resourceQuotaHelper'; import { KubernetesNamespace } from 'Kubernetes/models/namespace/models'; +import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; class KubernetesResourcePoolService { /* @ngInject */ - constructor($async, KubernetesNamespaceService, KubernetesResourceQuotaService) { + constructor($async, KubernetesNamespaceService, KubernetesResourceQuotaService, KubernetesIngressService) { this.$async = $async; this.KubernetesNamespaceService = KubernetesNamespaceService; this.KubernetesResourceQuotaService = KubernetesResourceQuotaService; + this.KubernetesIngressService = KubernetesIngressService; this.getAsync = this.getAsync.bind(this); this.getAllAsync = this.getAllAsync.bind(this); @@ -67,30 +69,37 @@ class KubernetesResourcePoolService { /** * CREATE + * @param {KubernetesResourcePoolFormValues} formValues */ - // TODO: review LimitRange future - async createAsync(name, owner, hasQuota, cpuLimit, memoryLimit) { + async createAsync(formValues) { try { const namespace = new KubernetesNamespace(); - namespace.Name = name; - namespace.ResourcePoolName = name; - namespace.ResourcePoolOwner = owner; + namespace.Name = formValues.Name; + namespace.ResourcePoolName = formValues.Name; + namespace.ResourcePoolOwner = formValues.Owner; await this.KubernetesNamespaceService.create(namespace); - if (hasQuota) { - const quota = new KubernetesResourceQuota(name); - quota.CpuLimit = cpuLimit; - quota.MemoryLimit = memoryLimit; - quota.ResourcePoolName = name; - quota.ResourcePoolOwner = owner; + if (formValues.HasQuota) { + const quota = new KubernetesResourceQuota(formValues.Name); + quota.CpuLimit = formValues.CpuLimit; + quota.MemoryLimit = KubernetesResourceReservationHelper.bytesValue(formValues.MemoryLimit); + quota.ResourcePoolName = formValues.Name; + quota.ResourcePoolOwner = formValues.Owner; await this.KubernetesResourceQuotaService.create(quota); } + const ingressPromises = _.map(formValues.IngressClasses, (c) => { + if (c.Selected) { + c.Namespace = namespace.Name; + return this.KubernetesIngressService.create(c); + } + }); + await Promise.all(ingressPromises); } catch (err) { throw err; } } - create(name, owner, hasQuota, cpuLimit, memoryLimit) { - return this.$async(this.createAsync, name, owner, hasQuota, cpuLimit, memoryLimit); + create(formValues) { + return this.$async(this.createAsync, formValues); } /** diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 540c450d7..f30fa9a95 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -16,7 +16,7 @@
    - +
    @@ -48,9 +48,9 @@ >
    - + - +
    @@ -64,13 +64,12 @@
    - +
    Resource pool
    - - +
    @@ -91,12 +90,12 @@ resource pool.
    - +
    Stack
    - +
    @@ -105,7 +104,6 @@
    -
    @@ -121,13 +119,12 @@ />
    - +
    Environment
    - - +
    @@ -146,7 +143,7 @@ name="environment_variable_name_{{ $index }}" class="form-control" ng-model="envVar.Name" - ng-change="ctrl.onChangeEnvironmentName($index)" + ng-change="ctrl.onChangeEnvironmentName()" ng-pattern="/^[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?$/" placeholder="foo" required @@ -155,7 +152,9 @@

    Environment variable name is required.

    @@ -164,7 +163,7 @@ character, and end with an alphanumeric character (e.g. 'my-var', or 'MY_VAR123').

    -

    This environment variable is already defined.

    @@ -177,22 +176,21 @@
    - +
    Configurations
    - - +
    @@ -231,7 +229,7 @@ - +
    @@ -277,13 +275,13 @@ style="margin-top: 5px;" ng-show=" kubernetesApplicationCreationForm['overriden_key_path_' + index + '_' + keyIndex].$invalid || - ctrl.state.duplicateConfigurationPaths[index + '_' + keyIndex] !== undefined + ctrl.state.duplicates.configurationPaths.refs[index + '_' + keyIndex] !== undefined " >

    Path is required.

    -

    This path is already used.

    @@ -302,12 +300,12 @@
    - +
    Persisting data
    - +
    @@ -315,7 +313,6 @@
    -
    @@ -333,7 +330,7 @@ class="form-control" name="persisted_folder_path_{{ $index }}" ng-model="persistedFolder.ContainerPath" - ng-change="ctrl.onChangePersistedFolderPath($index)" + ng-change="ctrl.onChangePersistedFolderPath()" ng-disabled="ctrl.isEditAndExistingPersistedFolder($index)" placeholder="/data" required @@ -360,7 +357,7 @@ uib-btn-radio="false" ng-change="ctrl.useExistingVolume($index)" ng-disabled="ctrl.availableVolumes.length === 0 || ctrl.application.ApplicationType === ctrl.ApplicationTypes.STATEFULSET" - >Use an existing volumeExisting volume
    @@ -422,10 +419,10 @@
    @@ -434,22 +431,22 @@

    Path is required.

    -

    This path is already defined.

    @@ -466,12 +463,12 @@

    Volume is required.

    -

    This volume is already used.

    @@ -483,8 +480,9 @@
    - + +
    @@ -579,11 +577,12 @@
    +
    Resource reservations
    - +
    @@ -668,11 +667,12 @@
    +
    Deployment
    - +
    Select how you want to deploy your application inside the cluster. @@ -775,8 +775,9 @@ >. You will not be able to scale that application.
    + - +
    Auto-scaling
    @@ -884,12 +885,12 @@
    - +
    Publishing the application
    - +
    Select how you want to publish your application. @@ -899,9 +900,36 @@
    -
    - -