diff --git a/.codeclimate.yml b/.codeclimate.yml index 84d9c8eda..845dacc08 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,5 +1,42 @@ ---- -engines: +version: "2" +checks: + argument-count: + enabled: true + config: + threshold: 4 + complex-logic: + enabled: true + config: + threshold: 4 + file-lines: + enabled: true + config: + threshold: 300 + method-complexity: + enabled: false + method-count: + enabled: true + config: + threshold: 20 + method-lines: + enabled: true + config: + threshold: 50 + nested-control-flow: + enabled: true + config: + threshold: 4 + return-statements: + enabled: false + similar-code: + enabled: true + config: + threshold: #language-specific defaults. overrides affect all languages. + identical-code: + enabled: true + config: + threshold: #language-specific defaults. overrides affect all languages. +plugins: gofmt: enabled: true golint: @@ -20,10 +57,5 @@ engines: config: .eslintrc.yml fixme: enabled: true -ratings: - paths: - - "**.css" - - "**.js" - - "**.go" -exclude_paths: +exclude_patterns: - test/ diff --git a/.codefresh/codefresh_branch.yml b/.codefresh/codefresh_branch.yml deleted file mode 100644 index 0d6e52555..000000000 --- a/.codefresh/codefresh_branch.yml +++ /dev/null @@ -1,46 +0,0 @@ -version: '1.0' -steps: - - build_backend: - image: portainer/golang-builder:ci - working_directory: ${{main_clone}} - commands: - - mkdir -p /go/src/github.com/${{CF_REPO_OWNER}} - - ln -s /codefresh/volume/${{CF_REPO_NAME}}/api /go/src/github.com/${{CF_REPO_OWNER}}/${{CF_REPO_NAME}} - - /build.sh api/cmd/portainer - - build_frontend: - image: portainer/angular-builder:latest - working_directory: ${{build_backend}} - commands: - - yarn - - yarn grunt build-webapp - - mv api/cmd/portainer/portainer dist/ - - get_docker_version: - image: alpine:3.7 - working_directory: ${{build_frontend}} - commands: - - cf_export DOCKER_VERSION=`cat gruntfile.js | grep -m 1 'shippedDockerVersion' | cut -d\' -f2` - - download_docker_binary: - image: busybox - working_directory: ${{build_frontend}} - commands: - - echo ${{DOCKER_VERSION}} - - wget -O /tmp/docker-binaries.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${{DOCKER_VERSION}}.tgz - - tar -xf /tmp/docker-binaries.tgz -C /tmp - - mv /tmp/docker/docker dist/ - - build_image: - type: build - working_directory: ${{download_docker_binary}} - dockerfile: ./build/linux/Dockerfile - image_name: portainer/portainer - tag: ${{CF_BRANCH}} - - push_image: - type: push - candidate: '${{build_image}}' - tag: '${{CF_BRANCH}}' - registry: dockerhub diff --git a/.codefresh/codefresh_pullrequest.yml b/.codefresh/codefresh_pullrequest.yml deleted file mode 100644 index d48b9870f..000000000 --- a/.codefresh/codefresh_pullrequest.yml +++ /dev/null @@ -1,46 +0,0 @@ -version: '1.0' -steps: - - build_backend: - image: portainer/golang-builder:ci - working_directory: ${{main_clone}} - commands: - - mkdir -p /go/src/github.com/${{CF_REPO_OWNER}} - - ln -s /codefresh/volume/${{CF_REPO_NAME}}/api /go/src/github.com/${{CF_REPO_OWNER}}/${{CF_REPO_NAME}} - - /build.sh api/cmd/portainer - - build_frontend: - image: portainer/angular-builder:latest - working_directory: ${{build_backend}} - commands: - - yarn - - yarn grunt build-webapp - - mv api/cmd/portainer/portainer dist/ - - get_docker_version: - image: alpine:3.7 - working_directory: ${{build_frontend}} - commands: - - cf_export DOCKER_VERSION=`cat gruntfile.js | grep -m 1 'shippedDockerVersion' | cut -d\' -f2` - - download_docker_binary: - image: busybox - working_directory: ${{build_frontend}} - commands: - - echo ${{DOCKER_VERSION}} - - wget -O /tmp/docker-binaries.tgz https://download.docker.com/linux/static/stable/x86_64/docker-${{DOCKER_VERSION}}.tgz - - tar -xf /tmp/docker-binaries.tgz -C /tmp - - mv /tmp/docker/docker dist/ - - build_image: - type: build - working_directory: ${{download_docker_binary}} - dockerfile: ./build/linux/Dockerfile - image_name: portainer/portainer - tag: ${{CF_BRANCH}} - - push_image: - type: push - candidate: '${{build_image}}' - tag: 'pr${{CF_PULL_REQUEST_NUMBER}}' - registry: dockerhub diff --git a/.eslintrc.yml b/.eslintrc.yml index 47be470b1..bc803c2e5 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -141,7 +141,10 @@ rules: no-undef-init: error no-undef: off no-undefined: off - no-unused-vars: off + no-unused-vars: + - warn + - + vars: local no-use-before-define: off # Node.js and CommonJS diff --git a/.gitignore b/.gitignore index 244386e1e..43b170ab0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist portainer-checksum.txt api/cmd/portainer/portainer* .tmp +.vscode \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1af8bc0be..3ddc75e8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Some basic conventions for contributing to this project. -### General +## General 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. @@ -13,7 +13,7 @@ When creating a new branch, prefix it with the *type* of the change (see section For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`. -### Issues open to contribution +## Issues open to contribution Want to contribute but don't know where to start? @@ -24,14 +24,14 @@ Some of the open issues are labeled with prefix `exp/`, this is used to mark the either AngularJS or Golang * **advanced**: a task that require a deep understanding of the project codebase -You can have a use Github filters to list these issues: +You can use Github filters to list these issues: * beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner * intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate * advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced -### Commit Message Format +## Commit Message Format Each commit message should include a **type**, a **scope** and a **subject**: @@ -47,7 +47,7 @@ Lines should not exceed 100 characters. This allows the message to be easier to #269 style(dashboard): update dashboard with new layout ``` -#### Type +### Type Must be one of the following: @@ -61,16 +61,30 @@ Must be one of the following: * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation -#### Scope +### Scope The scope could be anything specifying place of the commit change. For example `networks`, `containers`, `images` etc... You can use the **area** label tag associated on the issue here (for `area/containers` use `containers` as a scope...) -#### Subject +### Subject 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 + +## Contribution process + +Our contribution process is described below. Some of the steps can be visualized inside Github via specific `contrib/` labels, such as `contrib/func-review-in-progress` or `contrib/tech-review-approved`. + +### Bug report + +![portainer_bugreport_workflow](https://user-images.githubusercontent.com/5485061/43569306-5571b3a0-9637-11e8-8559-786cfc82a14f.png) + +### Feature request + +The feature request process is similar to the bug report process but has an extra functional validation before the technical validation. + +![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/43569315-5d30a308-9637-11e8-8292-3c62b5612925.png) diff --git a/README.md b/README.md index 1ee305f3f..7b2e9e096 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/) [![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size") [![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable) -[![Codefresh build status]( https://g.codefresh.io/api/badges/build?repoOwner=portainer&repoName=portainer&branch=develop&pipelineName=portainer-ci&accountName=deviantony&type=cf-1)]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci) +[![Build Status](https://semaphoreci.com/api/v1/portainer/portainer/branches/develop/badge.svg)](https://semaphoreci.com/portainer/portainer) [![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer) [![Slack](https://portainer.io/slack/badge.svg)](https://portainer.io/slack/) [![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index a6a921acf..192bc0271 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -21,6 +21,7 @@ import ( "github.com/portainer/portainer/bolt/template" "github.com/portainer/portainer/bolt/user" "github.com/portainer/portainer/bolt/version" + "github.com/portainer/portainer/bolt/webhook" ) const ( @@ -47,6 +48,7 @@ type Store struct { TemplateService *template.Service UserService *user.Service VersionService *version.Service + WebhookService *webhook.Service } // NewStore initializes a new Store and the associated services @@ -232,5 +234,11 @@ func (store *Store) initServices() error { } store.VersionService = versionService + webhookService, err := webhook.NewService(store.db) + if err != nil { + return err + } + store.WebhookService = webhookService + return nil } diff --git a/api/bolt/migrator/migrate_dbversion13.go b/api/bolt/migrator/migrate_dbversion13.go new file mode 100644 index 000000000..5434d00e2 --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion13.go @@ -0,0 +1,19 @@ +package migrator + +func (m *Migrator) updateResourceControlsToDBVersion14() error { + resourceControls, err := m.resourceControlService.ResourceControls() + if err != nil { + return err + } + + for _, resourceControl := range resourceControls { + if resourceControl.AdministratorsOnly == true { + err = m.resourceControlService.DeleteResourceControl(resourceControl.ID) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 9c61fc2c7..4d05820aa 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -178,5 +178,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 1.19.2 + if m.currentDBVersion < 14 { + err := m.updateResourceControlsToDBVersion14() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/bolt/webhook/webhook.go b/api/bolt/webhook/webhook.go new file mode 100644 index 000000000..94ebe61c5 --- /dev/null +++ b/api/bolt/webhook/webhook.go @@ -0,0 +1,151 @@ +package webhook + +import ( + "github.com/portainer/portainer" + "github.com/portainer/portainer/bolt/internal" + + "github.com/boltdb/bolt" +) + +const ( + // BucketName represents the name of the bucket where this service stores data. + BucketName = "webhooks" +) + +// Service represents a service for managing webhook 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 +} + +//Webhooks returns an array of all webhooks +func (service *Service) Webhooks() ([]portainer.Webhook, error) { + var webhooks = make([]portainer.Webhook, 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 webhook portainer.Webhook + err := internal.UnmarshalObject(v, &webhook) + if err != nil { + return err + } + webhooks = append(webhooks, webhook) + } + + return nil + }) + + return webhooks, err +} + +// Webhook returns a webhook by ID. +func (service *Service) Webhook(ID portainer.WebhookID) (*portainer.Webhook, error) { + var webhook portainer.Webhook + identifier := internal.Itob(int(ID)) + + err := internal.GetObject(service.db, BucketName, identifier, &webhook) + if err != nil { + return nil, err + } + + return &webhook, nil +} + +// WebhookByResourceID returns a webhook by the ResourceID it is associated with. +func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, error) { + var webhook *portainer.Webhook + + 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 w portainer.Webhook + err := internal.UnmarshalObject(v, &w) + if err != nil { + return err + } + + if w.ResourceID == ID { + webhook = &w + break + } + } + + if webhook == nil { + return portainer.ErrObjectNotFound + } + + return nil + }) + + return webhook, err +} + +// WebhookByToken returns a webhook by the random token it is associated with. +func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error) { + var webhook *portainer.Webhook + + 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 w portainer.Webhook + err := internal.UnmarshalObject(v, &w) + if err != nil { + return err + } + + if w.Token == token { + webhook = &w + break + } + } + + if webhook == nil { + return portainer.ErrObjectNotFound + } + + return nil + }) + + return webhook, err +} + +// DeleteWebhook deletes a webhook. +func (service *Service) DeleteWebhook(ID portainer.WebhookID) error { + identifier := internal.Itob(int(ID)) + return internal.DeleteObject(service.db, BucketName, identifier) +} + +// CreateWebhook assign an ID to a new webhook and saves it. +func (service *Service) CreateWebhook(webhook *portainer.Webhook) error { + return service.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + id, _ := bucket.NextSequence() + webhook.ID = portainer.WebhookID(id) + + data, err := internal.MarshalObject(webhook) + if err != nil { + return err + } + + return bucket.Put(internal.Itob(int(webhook.ID)), data) + }) +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 470c5d136..cecf986b7 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -178,6 +178,10 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL SnapshotInterval: *flags.SnapshotInterval, } + if *flags.Templates != "" { + settings.TemplatesURL = *flags.Templates + } + if *flags.Labels != nil { settings.BlackListedLabels = *flags.Labels } else { @@ -193,6 +197,10 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL } 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 { @@ -204,32 +212,14 @@ func initTemplates(templateService portainer.TemplateService, fileService portai return nil } - var templatesJSON []byte - if templateURL == "" { - return loadTemplatesFromFile(fileService, templateService, templateFile) - } - - templatesJSON, err = client.Get(templateURL) - if err != nil { - log.Println("Unable to retrieve templates via HTTP") - return err - } - - return unmarshalAndPersistTemplates(templateService, templatesJSON) -} - -func loadTemplatesFromFile(fileService portainer.FileService, templateService portainer.TemplateService, templateFile string) error { templatesJSON, err := fileService.GetFileContent(templateFile) if err != nil { - log.Println("Unable to retrieve template via filesystem") + log.Println("Unable to retrieve template definitions via filesystem") return err } - return unmarshalAndPersistTemplates(templateService, templatesJSON) -} -func unmarshalAndPersistTemplates(templateService portainer.TemplateService, templateData []byte) error { var templates []portainer.Template - err := json.Unmarshal(templateData, &templates) + err = json.Unmarshal(templatesJSON, &templates) if err != nil { log.Println("Unable to parse templates file. Please review your template definition file.") return err @@ -241,6 +231,7 @@ func unmarshalAndPersistTemplates(templateService portainer.TemplateService, tem return err } } + return nil } @@ -514,6 +505,7 @@ func main() { StackService: store.StackService, TagService: store.TagService, TemplateService: store.TemplateService, + WebhookService: store.WebhookService, SwarmStackManager: swarmStackManager, ComposeStackManager: composeStackManager, CryptoService: cryptoService, @@ -527,6 +519,7 @@ func main() { SSL: *flags.SSL, SSLCert: *flags.SSLCert, SSLKey: *flags.SSLKey, + DockerClientFactory: clientFactory, } log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) diff --git a/api/crypto/ecdsa.go b/api/crypto/ecdsa.go index 1b453dd3e..003547531 100644 --- a/api/crypto/ecdsa.go +++ b/api/crypto/ecdsa.go @@ -3,7 +3,6 @@ package crypto import ( "crypto/ecdsa" "crypto/elliptic" - "crypto/md5" "crypto/rand" "crypto/x509" "encoding/base64" @@ -97,9 +96,7 @@ func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) { // that hash. // It then encodes the generated signature in base64. func (service *ECDSAService) Sign(message string) (string, error) { - digest := md5.New() - digest.Write([]byte(message)) - hash := digest.Sum(nil) + hash := HashFromBytes([]byte(message)) r := big.NewInt(0) s := big.NewInt(0) diff --git a/api/crypto/md5.go b/api/crypto/md5.go new file mode 100644 index 000000000..42ca24602 --- /dev/null +++ b/api/crypto/md5.go @@ -0,0 +1,10 @@ +package crypto + +import "crypto/md5" + +// HashFromBytes returns the hash of the specified data +func HashFromBytes(data []byte) []byte { + digest := md5.New() + digest.Write(data) + return digest.Sum(nil) +} diff --git a/api/docker/snapshot.go b/api/docker/snapshot.go index 5f37ad985..d8c481fb7 100644 --- a/api/docker/snapshot.go +++ b/api/docker/snapshot.go @@ -30,6 +30,11 @@ func snapshot(cli *client.Client) (*portainer.Snapshot, error) { if err != nil { return nil, err } + + err = snapshotNodes(snapshot, cli) + if err != nil { + return nil, err + } } err = snapshotContainers(snapshot, cli) @@ -64,6 +69,22 @@ func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error { return nil } +func snapshotNodes(snapshot *portainer.Snapshot, cli *client.Client) error { + nodes, err := cli.NodeList(context.Background(), types.NodeListOptions{}) + if err != nil { + return err + } + var nanoCpus int64 + var totalMem int64 + for _, node := range nodes { + nanoCpus += node.Description.Resources.NanoCPUs + totalMem += node.Description.Resources.MemoryBytes + } + snapshot.TotalCPU = int(nanoCpus / 1e9) + snapshot.TotalMemory = totalMem + return nil +} + func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) error { stacks := make(map[string]struct{}) diff --git a/api/docker/snapshotter.go b/api/docker/snapshotter.go index b8f571d37..34cb35def 100644 --- a/api/docker/snapshotter.go +++ b/api/docker/snapshotter.go @@ -22,6 +22,7 @@ func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*p if err != nil { return nil, err } + defer cli.Close() return snapshot(cli) } diff --git a/api/errors.go b/api/errors.go index 37552f104..e348aaf48 100644 --- a/api/errors.go +++ b/api/errors.go @@ -93,3 +93,9 @@ 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/http/client/client.go b/api/http/client/client.go index 29ebb7d88..541ec8257 100644 --- a/api/http/client/client.go +++ b/api/http/client/client.go @@ -13,6 +13,10 @@ import ( "github.com/portainer/portainer" ) +const ( + errInvalidResponseStatus = portainer.Error("Invalid response status (expecting 200)") +) + // HTTPClient represents a client to send HTTP requests. type HTTPClient struct { *http.Client @@ -75,6 +79,10 @@ func Get(url string) ([]byte, error) { } defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return nil, errInvalidResponseStatus + } + body, err := ioutil.ReadAll(response.Body) if err != nil { return nil, err diff --git a/api/http/error/error.go b/api/http/error/error.go deleted file mode 100644 index b9153a8a6..000000000 --- a/api/http/error/error.go +++ /dev/null @@ -1,41 +0,0 @@ -package error - -import ( - "encoding/json" - "log" - "net/http" -) - -type ( - // LoggerHandler defines a HTTP handler that includes a HandlerError return pointer - LoggerHandler func(http.ResponseWriter, *http.Request) *HandlerError - // HandlerError represents an error raised inside a HTTP handler - HandlerError struct { - StatusCode int - Message string - Err error - } - errorResponse struct { - Err string `json:"err,omitempty"` - } -) - -func (handler LoggerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - err := handler(rw, r) - if err != nil { - writeErrorResponse(rw, err) - } -} - -func writeErrorResponse(rw http.ResponseWriter, err *HandlerError) { - log.Printf("http error: %s (err=%s) (code=%d)\n", err.Message, err.Err, err.StatusCode) - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(err.StatusCode) - json.NewEncoder(rw).Encode(&errorResponse{Err: err.Message}) -} - -// WriteError is a convenience function that creates a new HandlerError before calling writeErrorResponse. -// For use outside of the standard http handlers. -func WriteError(rw http.ResponseWriter, code int, message string, err error) { - writeErrorResponse(rw, &HandlerError{code, message, err}) -} diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index cf928357b..d5562edd3 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -6,10 +6,10 @@ import ( "strings" "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type authenticatePayload struct { diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index 87be85429..1f0769e08 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/dockerhub/dockerhub_inspect.go b/api/http/handler/dockerhub/dockerhub_inspect.go index 25be6617c..b149a2a35 100644 --- a/api/http/handler/dockerhub/dockerhub_inspect.go +++ b/api/http/handler/dockerhub/dockerhub_inspect.go @@ -3,8 +3,8 @@ package dockerhub import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" ) // GET request on /api/dockerhub diff --git a/api/http/handler/dockerhub/dockerhub_update.go b/api/http/handler/dockerhub/dockerhub_update.go index 7bd37bce5..69d82ee50 100644 --- a/api/http/handler/dockerhub/dockerhub_update.go +++ b/api/http/handler/dockerhub/dockerhub_update.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type dockerhubUpdatePayload struct { diff --git a/api/http/handler/dockerhub/handler.go b/api/http/handler/dockerhub/handler.go index cd2f5ae50..9ad93232d 100644 --- a/api/http/handler/dockerhub/handler.go +++ b/api/http/handler/dockerhub/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index e2d7bd0c0..cc7a23054 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type endpointGroupCreatePayload struct { diff --git a/api/http/handler/endpointgroups/endpointgroup_delete.go b/api/http/handler/endpointgroups/endpointgroup_delete.go index 01123d850..b436b4f2f 100644 --- a/api/http/handler/endpointgroups/endpointgroup_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_delete.go @@ -3,10 +3,10 @@ package endpointgroups import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // DELETE request on /api/endpoint_groups/:id diff --git a/api/http/handler/endpointgroups/endpointgroup_inspect.go b/api/http/handler/endpointgroups/endpointgroup_inspect.go index 168d8cb8b..46a6895e8 100644 --- a/api/http/handler/endpointgroups/endpointgroup_inspect.go +++ b/api/http/handler/endpointgroups/endpointgroup_inspect.go @@ -3,10 +3,10 @@ package endpointgroups import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // GET request on /api/endpoint_groups/:id diff --git a/api/http/handler/endpointgroups/endpointgroup_list.go b/api/http/handler/endpointgroups/endpointgroup_list.go index fa7a35ec4..7acb696a8 100644 --- a/api/http/handler/endpointgroups/endpointgroup_list.go +++ b/api/http/handler/endpointgroups/endpointgroup_list.go @@ -3,8 +3,8 @@ package endpointgroups import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index ee31e3066..ec24b801f 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -3,10 +3,10 @@ package endpointgroups import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type endpointGroupUpdatePayload struct { diff --git a/api/http/handler/endpointgroups/endpointgroup_update_access.go b/api/http/handler/endpointgroups/endpointgroup_update_access.go index 7a3b3038e..6e859a4df 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update_access.go +++ b/api/http/handler/endpointgroups/endpointgroup_update_access.go @@ -3,10 +3,10 @@ package endpointgroups import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type endpointGroupUpdateAccessPayload struct { diff --git a/api/http/handler/endpointgroups/handler.go b/api/http/handler/endpointgroups/handler.go index f31a40a27..3b731e911 100644 --- a/api/http/handler/endpointgroups/handler.go +++ b/api/http/handler/endpointgroups/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index cd17e0733..eaef36b1b 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -2,8 +2,8 @@ package endpointproxy import ( "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go index dc46bfb3f..dcbe96f4b 100644 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -3,9 +3,9 @@ package endpointproxy import ( "strconv" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" "net/http" ) diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index 01a56e017..f03ca8e67 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -3,9 +3,9 @@ package endpointproxy import ( "strconv" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" "net/http" ) diff --git a/api/http/handler/endpointproxy/proxy_storidge.go b/api/http/handler/endpointproxy/proxy_storidge.go index a582b561d..86375f091 100644 --- a/api/http/handler/endpointproxy/proxy_storidge.go +++ b/api/http/handler/endpointproxy/proxy_storidge.go @@ -3,9 +3,9 @@ package endpointproxy import ( "strconv" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" "net/http" ) diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index a369194ab..a3eeeb114 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -6,12 +6,12 @@ import ( "runtime" "strconv" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" "github.com/portainer/portainer/crypto" "github.com/portainer/portainer/http/client" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type endpointCreatePayload struct { @@ -35,7 +35,7 @@ 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 stack name") + return portainer.Error("Invalid endpoint name") } payload.Name = name @@ -71,7 +71,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { payload.TLSSkipClientVerify = skipTLSClientVerification if !payload.TLSSkipVerify { - caCert, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile") + caCert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile") if err != nil { return portainer.Error("Invalid CA certificate file. Ensure that the file is uploaded correctly") } @@ -79,13 +79,13 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } if !payload.TLSSkipClientVerify { - cert, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile") + cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile") if err != nil { return portainer.Error("Invalid certificate file. Ensure that the file is uploaded correctly") } payload.TLSCertFile = cert - key, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile") + key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile") if err != nil { return portainer.Error("Invalid key file. Ensure that the file is uploaded correctly") } @@ -174,7 +174,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po endpoint := &portainer.Endpoint{ ID: portainer.EndpointID(endpointID), Name: payload.Name, - URL: payload.URL, + URL: "https://management.azure.com", Type: portainer.AzureEnvironment, GroupID: portainer.EndpointGroupID(payload.GroupID), PublicURL: payload.PublicURL, diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 40f18f348..865c2055b 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -4,10 +4,10 @@ import ( "net/http" "strconv" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // DELETE request on /api/endpoints/:id diff --git a/api/http/handler/endpoints/endpoint_extension_add.go b/api/http/handler/endpoints/endpoint_extension_add.go index 009083190..9a9eebbda 100644 --- a/api/http/handler/endpoints/endpoint_extension_add.go +++ b/api/http/handler/endpoints/endpoint_extension_add.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type endpointExtensionAddPayload struct { diff --git a/api/http/handler/endpoints/endpoint_extension_remove.go b/api/http/handler/endpoints/endpoint_extension_remove.go index 2e238a89e..8b265dc6c 100644 --- a/api/http/handler/endpoints/endpoint_extension_remove.go +++ b/api/http/handler/endpoints/endpoint_extension_remove.go @@ -3,10 +3,10 @@ package endpoints import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // DELETE request on /api/endpoints/:id/extensions/:extensionType diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index e382b3f16..dd0d3485b 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -3,10 +3,10 @@ package endpoints import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // GET request on /api/endpoints/:id diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 13268b48d..5c1436ac9 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -3,8 +3,8 @@ package endpoints import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer/http/security" ) @@ -27,8 +27,9 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext) - for _, endpoint := range filteredEndpoints { - hideFields(&endpoint) + for idx := range filteredEndpoints { + hideFields(&filteredEndpoints[idx]) } + return response.JSON(w, filteredEndpoints) } diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 51a18293e..720f4c072 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -4,9 +4,9 @@ import ( "log" "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" ) // POST request on /api/endpoints/snapshot diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 769267449..5244cdc98 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -4,11 +4,11 @@ import ( "net/http" "strconv" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" "github.com/portainer/portainer/http/client" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type endpointUpdatePayload struct { diff --git a/api/http/handler/endpoints/endpoint_update_access.go b/api/http/handler/endpoints/endpoint_update_access.go index bedf559ff..47a7d414b 100644 --- a/api/http/handler/endpoints/endpoint_update_access.go +++ b/api/http/handler/endpoints/endpoint_update_access.go @@ -3,10 +3,10 @@ package endpoints import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type endpointUpdateAccessPayload struct { diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index fc1bc1e6c..779cd9390 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -1,8 +1,8 @@ package endpoints import ( + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" "github.com/portainer/portainer/http/security" diff --git a/api/http/handler/file/handler.go b/api/http/handler/file/handler.go index 15ec1417f..464062be1 100644 --- a/api/http/handler/file/handler.go +++ b/api/http/handler/file/handler.go @@ -33,5 +33,9 @@ func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } else { w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") } + + w.Header().Add("X-Frame-Options", "DENY") + w.Header().Add("X-XSS-Protection", "1; mode=block") + w.Header().Add("X-Content-Type-Options", "nosniff") handler.Handler.ServeHTTP(w, r) } diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index dd15e1212..40ae9f57d 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -10,6 +10,7 @@ import ( "github.com/portainer/portainer/http/handler/endpointproxy" "github.com/portainer/portainer/http/handler/endpoints" "github.com/portainer/portainer/http/handler/file" + "github.com/portainer/portainer/http/handler/motd" "github.com/portainer/portainer/http/handler/registries" "github.com/portainer/portainer/http/handler/resourcecontrols" "github.com/portainer/portainer/http/handler/settings" @@ -21,6 +22,7 @@ import ( "github.com/portainer/portainer/http/handler/templates" "github.com/portainer/portainer/http/handler/upload" "github.com/portainer/portainer/http/handler/users" + "github.com/portainer/portainer/http/handler/webhooks" "github.com/portainer/portainer/http/handler/websocket" ) @@ -33,6 +35,7 @@ type Handler struct { EndpointHandler *endpoints.Handler EndpointProxyHandler *endpointproxy.Handler FileHandler *file.Handler + MOTDHandler *motd.Handler RegistryHandler *registries.Handler ResourceControlHandler *resourcecontrols.Handler SettingsHandler *settings.Handler @@ -45,6 +48,7 @@ type Handler struct { UploadHandler *upload.Handler UserHandler *users.Handler WebSocketHandler *websocket.Handler + WebhookHandler *webhooks.Handler } // ServeHTTP delegates a request to the appropriate subhandler. @@ -67,6 +71,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) } + case strings.HasPrefix(r.URL.Path, "/api/motd"): + http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/registries"): http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/resource_controls"): @@ -91,6 +97,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/websocket"): http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/webhooks"): + http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/"): h.FileHandler.ServeHTTP(w, r) } diff --git a/api/http/handler/motd/handler.go b/api/http/handler/motd/handler.go new file mode 100644 index 000000000..429731aa4 --- /dev/null +++ b/api/http/handler/motd/handler.go @@ -0,0 +1,24 @@ +package motd + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/portainer/portainer/http/security" +) + +// Handler is the HTTP handler used to handle MOTD operations. +type Handler struct { + *mux.Router +} + +// NewHandler returns a new Handler +func NewHandler(bouncer *security.RequestBouncer) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + } + h.Handle("/motd", + bouncer.AuthenticatedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet) + + return h +} diff --git a/api/http/handler/motd/motd.go b/api/http/handler/motd/motd.go new file mode 100644 index 000000000..fbe8b5acd --- /dev/null +++ b/api/http/handler/motd/motd.go @@ -0,0 +1,27 @@ +package motd + +import ( + "net/http" + + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer" + "github.com/portainer/portainer/crypto" + "github.com/portainer/portainer/http/client" +) + +type motdResponse struct { + Message string `json:"Message"` + Hash []byte `json:"Hash"` +} + +func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) { + + motd, err := client.Get(portainer.MessageOfTheDayURL) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + hash := crypto.HashFromBytes(motd) + response.JSON(w, &motdResponse{Message: string(motd), Hash: hash}) +} diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go index 7a21561db..33a161932 100644 --- a/api/http/handler/registries/handler.go +++ b/api/http/handler/registries/handler.go @@ -1,8 +1,8 @@ package registries import ( + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" "net/http" diff --git a/api/http/handler/registries/registry_create.go b/api/http/handler/registries/registry_create.go index 781f172bf..ad4acb58a 100644 --- a/api/http/handler/registries/registry_create.go +++ b/api/http/handler/registries/registry_create.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type registryCreatePayload struct { diff --git a/api/http/handler/registries/registry_delete.go b/api/http/handler/registries/registry_delete.go index 2e968539b..ac463af5f 100644 --- a/api/http/handler/registries/registry_delete.go +++ b/api/http/handler/registries/registry_delete.go @@ -3,10 +3,10 @@ package registries import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // DELETE request on /api/registries/:id diff --git a/api/http/handler/registries/registry_inspect.go b/api/http/handler/registries/registry_inspect.go index a60f24288..96cdf84ac 100644 --- a/api/http/handler/registries/registry_inspect.go +++ b/api/http/handler/registries/registry_inspect.go @@ -3,10 +3,10 @@ package registries import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // GET request on /api/registries/:id diff --git a/api/http/handler/registries/registry_list.go b/api/http/handler/registries/registry_list.go index 158f8e3fe..7f75427cc 100644 --- a/api/http/handler/registries/registry_list.go +++ b/api/http/handler/registries/registry_list.go @@ -3,8 +3,8 @@ package registries import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer/http/security" ) @@ -22,8 +22,9 @@ func (handler *Handler) registryList(w http.ResponseWriter, r *http.Request) *ht filteredRegistries := security.FilterRegistries(registries, securityContext) - for _, registry := range filteredRegistries { - hideFields(®istry) + for idx := range filteredRegistries { + hideFields(&filteredRegistries[idx]) } - return response.JSON(w, registries) + + return response.JSON(w, filteredRegistries) } diff --git a/api/http/handler/registries/registry_update.go b/api/http/handler/registries/registry_update.go index 1a3743fb5..cd3a7ae67 100644 --- a/api/http/handler/registries/registry_update.go +++ b/api/http/handler/registries/registry_update.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type registryUpdatePayload struct { diff --git a/api/http/handler/registries/registry_update_access.go b/api/http/handler/registries/registry_update_access.go index a43ccb2f9..77f6fe08e 100644 --- a/api/http/handler/registries/registry_update_access.go +++ b/api/http/handler/registries/registry_update_access.go @@ -3,10 +3,10 @@ package registries import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type registryUpdateAccessPayload struct { diff --git a/api/http/handler/resourcecontrols/handler.go b/api/http/handler/resourcecontrols/handler.go index 4ad474f6b..60757ef28 100644 --- a/api/http/handler/resourcecontrols/handler.go +++ b/api/http/handler/resourcecontrols/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/resourcecontrols/resourcecontrol_create.go b/api/http/handler/resourcecontrols/resourcecontrol_create.go index baaee3360..97c3d5ef1 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_create.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_create.go @@ -4,20 +4,20 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) type resourceControlCreatePayload struct { - ResourceID string - Type string - AdministratorsOnly bool - Users []int - Teams []int - SubResourceIDs []string + ResourceID string + Type string + Public bool + Users []int + Teams []int + SubResourceIDs []string } func (payload *resourceControlCreatePayload) Validate(r *http.Request) error { @@ -29,8 +29,8 @@ func (payload *resourceControlCreatePayload) Validate(r *http.Request) error { return portainer.Error("Invalid type") } - if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.AdministratorsOnly { - return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or AdministratorOnly") + if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public { + return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public") } return nil } @@ -90,12 +90,12 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req } resourceControl := portainer.ResourceControl{ - ResourceID: payload.ResourceID, - SubResourceIDs: payload.SubResourceIDs, - Type: resourceControlType, - AdministratorsOnly: payload.AdministratorsOnly, - UserAccesses: userAccesses, - TeamAccesses: teamAccesses, + ResourceID: payload.ResourceID, + SubResourceIDs: payload.SubResourceIDs, + Type: resourceControlType, + Public: payload.Public, + UserAccesses: userAccesses, + TeamAccesses: teamAccesses, } securityContext, err := security.RetrieveRestrictedRequestContext(r) diff --git a/api/http/handler/resourcecontrols/resourcecontrol_delete.go b/api/http/handler/resourcecontrols/resourcecontrol_delete.go index 48fd8a972..04ba8d5d4 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_delete.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_delete.go @@ -3,10 +3,10 @@ package resourcecontrols import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/resourcecontrols/resourcecontrol_update.go b/api/http/handler/resourcecontrols/resourcecontrol_update.go index 3f3b25799..c46247c5d 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_update.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_update.go @@ -3,22 +3,22 @@ package resourcecontrols import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) type resourceControlUpdatePayload struct { - AdministratorsOnly bool - Users []int - Teams []int + Public bool + Users []int + Teams []int } func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error { - if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.AdministratorsOnly { - return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or AdministratorOnly") + if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public { + return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public") } return nil } @@ -52,7 +52,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} } - resourceControl.AdministratorsOnly = payload.AdministratorsOnly + resourceControl.Public = payload.Public var userAccesses = make([]portainer.UserResourceAccess, 0) for _, v := range payload.Users { diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index 58c020877..9440aedd5 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/settings/settings_inspect.go b/api/http/handler/settings/settings_inspect.go index 48da08612..c922e1f47 100644 --- a/api/http/handler/settings/settings_inspect.go +++ b/api/http/handler/settings/settings_inspect.go @@ -3,8 +3,8 @@ package settings import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" ) // GET request on /api/settings diff --git a/api/http/handler/settings/settings_ldap_check.go b/api/http/handler/settings/settings_ldap_check.go index 80d058e33..a8466e285 100644 --- a/api/http/handler/settings/settings_ldap_check.go +++ b/api/http/handler/settings/settings_ldap_check.go @@ -3,11 +3,11 @@ package settings import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" "github.com/portainer/portainer/filesystem" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type settingsLDAPCheckPayload struct { diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index c2ee2a616..549cf999e 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -3,9 +3,9 @@ package settings import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" ) type publicSettingsResponse struct { @@ -13,6 +13,7 @@ type publicSettingsResponse struct { AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + ExternalTemplates bool `json:"ExternalTemplates"` } // GET request on /api/settings/public @@ -27,6 +28,11 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AuthenticationMethod: settings.AuthenticationMethod, AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + ExternalTemplates: false, + } + + if settings.TemplatesURL != "" { + publicSettings.ExternalTemplates = true } return response.JSON(w, publicSettings) diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 827818fa7..18513931f 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -4,11 +4,11 @@ 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" "github.com/portainer/portainer/filesystem" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type settingsUpdatePayload struct { @@ -19,6 +19,7 @@ type settingsUpdatePayload struct { AllowBindMountsForRegularUsers *bool AllowPrivilegedModeForRegularUsers *bool SnapshotInterval *string + TemplatesURL *string } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -28,6 +29,9 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error { if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) { return portainer.Error("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 nil } @@ -52,6 +56,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.LogoURL = *payload.LogoURL } + if payload.TemplatesURL != nil { + settings.TemplatesURL = *payload.TemplatesURL + } + if payload.BlackListedLabels != nil { settings.BlackListedLabels = payload.BlackListedLabels } diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index a343c96fa..a6a8ddcfd 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -6,11 +6,11 @@ import ( "strings" "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" "github.com/portainer/portainer/filesystem" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) @@ -194,7 +194,7 @@ func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) erro } payload.Name = name - composeFileContent, err := request.RetrieveMultiPartFormFile(r, "file") + composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly") } diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 87c94e6ef..017122026 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -6,11 +6,11 @@ import ( "strings" "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" "github.com/portainer/portainer/filesystem" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) @@ -211,7 +211,7 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error } payload.SwarmID = swarmID - composeFileContent, err := request.RetrieveMultiPartFormFile(r, "file") + composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly") } diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index ba231ebfe..7f2fd4bc0 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -5,8 +5,8 @@ import ( "sync" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index c0b9d9ce0..0fe7e07ad 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -1,12 +1,13 @@ package stacks import ( + "errors" "log" "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" ) func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error { @@ -57,7 +58,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return handler.createComposeStack(w, r, method, endpoint) } - return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", request.ErrInvalidQueryParameter} + 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)} } func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError { @@ -71,7 +72,7 @@ func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Reques return handler.createComposeStackFromFileUpload(w, r, endpoint) } - return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", request.ErrInvalidQueryParameter} + 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) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError { @@ -84,5 +85,5 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, return handler.createSwarmStackFromFileUpload(w, r, endpoint) } - return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", request.ErrInvalidQueryParameter} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)} } diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index 3844a4d7e..8f8fc25e8 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -4,11 +4,11 @@ import ( "net/http" "strconv" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) @@ -48,8 +48,8 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} } - if resourceControl != nil { - if !securityContext.IsAdmin && !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { + if !securityContext.IsAdmin { + if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} } } diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index f8d25af8d..b0b247cef 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -4,11 +4,11 @@ import ( "net/http" "path" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) @@ -41,6 +41,10 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe } extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}} + if !securityContext.IsAdmin && resourceControl == nil { + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + } + if resourceControl != nil { if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { extendedStack.ResourceControl = *resourceControl diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index 28d5030ac..e5377f654 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -3,11 +3,11 @@ package stacks import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) @@ -36,6 +36,10 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht } extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}} + if !securityContext.IsAdmin && resourceControl == nil { + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + } + if resourceControl != nil { if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { extendedStack.ResourceControl = *resourceControl diff --git a/api/http/handler/stacks/stack_list.go b/api/http/handler/stacks/stack_list.go index fc4732e06..337b204a7 100644 --- a/api/http/handler/stacks/stack_list.go +++ b/api/http/handler/stacks/stack_list.go @@ -3,11 +3,11 @@ package stacks import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index beb579a34..8a0ec0c69 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -3,11 +3,11 @@ package stacks import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) @@ -53,8 +53,8 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} } - if resourceControl != nil { - if !securityContext.IsAdmin && !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { + if !securityContext.IsAdmin { + if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} } } diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index de1c560ae..03999a557 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -5,11 +5,11 @@ import ( "strconv" "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) @@ -62,8 +62,8 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} } - if resourceControl != nil { - if !securityContext.IsAdmin && !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { + if !securityContext.IsAdmin { + if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} } } diff --git a/api/http/handler/status/handler.go b/api/http/handler/status/handler.go index 692c64130..4d26fcd3a 100644 --- a/api/http/handler/status/handler.go +++ b/api/http/handler/status/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/status/status_inspect.go b/api/http/handler/status/status_inspect.go index 93d379179..1892ddd7d 100644 --- a/api/http/handler/status/status_inspect.go +++ b/api/http/handler/status/status_inspect.go @@ -3,8 +3,8 @@ package status import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" ) // GET request on /api/status diff --git a/api/http/handler/tags/handler.go b/api/http/handler/tags/handler.go index a700f7c3e..b38bd28f3 100644 --- a/api/http/handler/tags/handler.go +++ b/api/http/handler/tags/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/tags/tag_create.go b/api/http/handler/tags/tag_create.go index f75c050b9..50b9261ba 100644 --- a/api/http/handler/tags/tag_create.go +++ b/api/http/handler/tags/tag_create.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type tagCreatePayload struct { diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index eed7f6410..e74b5ef12 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -3,10 +3,10 @@ package tags import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // DELETE request on /api/tags/:id diff --git a/api/http/handler/tags/tag_list.go b/api/http/handler/tags/tag_list.go index b572add68..a19aa48e7 100644 --- a/api/http/handler/tags/tag_list.go +++ b/api/http/handler/tags/tag_list.go @@ -3,8 +3,8 @@ package tags import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" ) // GET request on /api/tags diff --git a/api/http/handler/teammemberships/handler.go b/api/http/handler/teammemberships/handler.go index c50773a85..10cd63a83 100644 --- a/api/http/handler/teammemberships/handler.go +++ b/api/http/handler/teammemberships/handler.go @@ -1,8 +1,8 @@ package teammemberships import ( + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" "net/http" diff --git a/api/http/handler/teammemberships/teammembership_create.go b/api/http/handler/teammemberships/teammembership_create.go index 49783d767..37216df6a 100644 --- a/api/http/handler/teammemberships/teammembership_create.go +++ b/api/http/handler/teammemberships/teammembership_create.go @@ -3,10 +3,10 @@ package teammemberships import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/teammemberships/teammembership_delete.go b/api/http/handler/teammemberships/teammembership_delete.go index a1263745f..846fb7892 100644 --- a/api/http/handler/teammemberships/teammembership_delete.go +++ b/api/http/handler/teammemberships/teammembership_delete.go @@ -3,10 +3,10 @@ package teammemberships import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/teammemberships/teammembership_list.go b/api/http/handler/teammemberships/teammembership_list.go index 0f9267a10..4136053d6 100644 --- a/api/http/handler/teammemberships/teammembership_list.go +++ b/api/http/handler/teammemberships/teammembership_list.go @@ -3,9 +3,9 @@ package teammemberships import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/teammemberships/teammembership_update.go b/api/http/handler/teammemberships/teammembership_update.go index 6d08bc90a..0e400e975 100644 --- a/api/http/handler/teammemberships/teammembership_update.go +++ b/api/http/handler/teammemberships/teammembership_update.go @@ -3,10 +3,10 @@ package teammemberships import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/teams/handler.go b/api/http/handler/teams/handler.go index 2b8cd7c3b..946a071d7 100644 --- a/api/http/handler/teams/handler.go +++ b/api/http/handler/teams/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/teams/team_create.go b/api/http/handler/teams/team_create.go index d865e56c5..16b230ec4 100644 --- a/api/http/handler/teams/team_create.go +++ b/api/http/handler/teams/team_create.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type teamCreatePayload struct { diff --git a/api/http/handler/teams/team_delete.go b/api/http/handler/teams/team_delete.go index 623c29c4c..00146d698 100644 --- a/api/http/handler/teams/team_delete.go +++ b/api/http/handler/teams/team_delete.go @@ -3,10 +3,10 @@ package teams import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // DELETE request on /api/teams/:id diff --git a/api/http/handler/teams/team_inspect.go b/api/http/handler/teams/team_inspect.go index 4030a391e..b8aa83da1 100644 --- a/api/http/handler/teams/team_inspect.go +++ b/api/http/handler/teams/team_inspect.go @@ -3,10 +3,10 @@ package teams import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/teams/team_list.go b/api/http/handler/teams/team_list.go index 7c4268e13..4acc5eabb 100644 --- a/api/http/handler/teams/team_list.go +++ b/api/http/handler/teams/team_list.go @@ -3,8 +3,8 @@ package teams import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/teams/team_memberships.go b/api/http/handler/teams/team_memberships.go index d09abe7d5..450458e35 100644 --- a/api/http/handler/teams/team_memberships.go +++ b/api/http/handler/teams/team_memberships.go @@ -3,10 +3,10 @@ package teams import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/teams/team_update.go b/api/http/handler/teams/team_update.go index 8c0961c31..dea180b6a 100644 --- a/api/http/handler/teams/team_update.go +++ b/api/http/handler/teams/team_update.go @@ -3,10 +3,10 @@ package teams import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type teamUpdatePayload struct { diff --git a/api/http/handler/templates/handler.go b/api/http/handler/templates/handler.go index db65830c2..660991d8d 100644 --- a/api/http/handler/templates/handler.go +++ b/api/http/handler/templates/handler.go @@ -4,15 +4,20 @@ import ( "net/http" "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/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 } // NewHandler returns a new instance of Handler. @@ -20,15 +25,32 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { h := &Handler{ Router: mux.NewRouter(), } + h.Handle("/templates", bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) h.Handle("/templates", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.templateCreate))).Methods(http.MethodPost) + bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost) h.Handle("/templates/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.templateInspect))).Methods(http.MethodGet) + bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet) h.Handle("/templates/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.templateUpdate))).Methods(http.MethodPut) + bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut) h.Handle("/templates/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.templateDelete))).Methods(http.MethodDelete) + bouncer.AdministratorAccess(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 index f27cd494a..ef9c0c58f 100644 --- a/api/http/handler/templates/template_create.go +++ b/api/http/handler/templates/template_create.go @@ -4,11 +4,11 @@ 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" "github.com/portainer/portainer/filesystem" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type templateCreatePayload struct { diff --git a/api/http/handler/templates/template_delete.go b/api/http/handler/templates/template_delete.go index c23c2d237..f5793bb8c 100644 --- a/api/http/handler/templates/template_delete.go +++ b/api/http/handler/templates/template_delete.go @@ -3,10 +3,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // DELETE request on /api/templates/:id diff --git a/api/http/handler/templates/template_inspect.go b/api/http/handler/templates/template_inspect.go index 6b1b4c6a1..70b6ada02 100644 --- a/api/http/handler/templates/template_inspect.go +++ b/api/http/handler/templates/template_inspect.go @@ -3,10 +3,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // GET request on /api/templates/:id diff --git a/api/http/handler/templates/template_list.go b/api/http/handler/templates/template_list.go index 7c31e1c10..24ca93bbd 100644 --- a/api/http/handler/templates/template_list.go +++ b/api/http/handler/templates/template_list.go @@ -1,18 +1,40 @@ package templates import ( + "encoding/json" "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer" + "github.com/portainer/portainer/http/client" "github.com/portainer/portainer/http/security" ) // GET request on /api/templates func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - templates, err := handler.TemplateService.Templates() + settings, err := handler.SettingsService.Settings() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve templates from the database", err} + 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) + 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) @@ -21,6 +43,5 @@ func (handler *Handler) templateList(w http.ResponseWriter, r *http.Request) *ht } filteredTemplates := security.FilterTemplates(templates, securityContext) - return response.JSON(w, filteredTemplates) } diff --git a/api/http/handler/templates/template_update.go b/api/http/handler/templates/template_update.go index 2eff9701f..123ea0f6a 100644 --- a/api/http/handler/templates/template_update.go +++ b/api/http/handler/templates/template_update.go @@ -3,10 +3,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type templateUpdatePayload struct { diff --git a/api/http/handler/upload/handler.go b/api/http/handler/upload/handler.go index 6ce36df77..b0068979b 100644 --- a/api/http/handler/upload/handler.go +++ b/api/http/handler/upload/handler.go @@ -1,8 +1,8 @@ package upload import ( + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" "net/http" diff --git a/api/http/handler/upload/upload_tls.go b/api/http/handler/upload/upload_tls.go index aebab6813..964a7d29b 100644 --- a/api/http/handler/upload/upload_tls.go +++ b/api/http/handler/upload/upload_tls.go @@ -3,10 +3,10 @@ package upload import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // POST request on /api/upload/tls/{certificate:(?:ca|cert|key)}?folder= @@ -21,7 +21,7 @@ func (handler *Handler) uploadTLS(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: folder", err} } - file, err := request.RetrieveMultiPartFormFile(r, "file") + file, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid certificate file. Ensure that the certificate file is uploaded correctly", err} } diff --git a/api/http/handler/users/admin_check.go b/api/http/handler/users/admin_check.go index 4d7ba233a..5fcd1d33d 100644 --- a/api/http/handler/users/admin_check.go +++ b/api/http/handler/users/admin_check.go @@ -3,9 +3,9 @@ package users import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" ) // GET request on /api/users/admin/check diff --git a/api/http/handler/users/admin_init.go b/api/http/handler/users/admin_init.go index 2ad394803..fb2172686 100644 --- a/api/http/handler/users/admin_init.go +++ b/api/http/handler/users/admin_init.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) type adminInitPayload struct { @@ -39,7 +39,7 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe } if len(users) != 0 { - return &httperror.HandlerError{http.StatusConflict, "Unable to retrieve users from the database", portainer.ErrAdminAlreadyInitialized} + return &httperror.HandlerError{http.StatusConflict, "Unable to create administrator user", portainer.ErrAdminAlreadyInitialized} } user := &portainer.User{ diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index f6e4df727..ae43ef521 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -1,8 +1,8 @@ package users import ( + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" "net/http" @@ -26,7 +26,7 @@ type Handler struct { } // NewHandler creates a handler to manage user operations. -func NewHandler(bouncer *security.RequestBouncer) *Handler { +func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter) *Handler { h := &Handler{ Router: mux.NewRouter(), } @@ -43,7 +43,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { h.Handle("/users/{id}/memberships", bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet) h.Handle("/users/{id}/passwd", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userPassword))).Methods(http.MethodPost) + rateLimiter.LimitAccess(bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut) h.Handle("/users/admin/check", bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet) h.Handle("/users/admin/init", diff --git a/api/http/handler/users/user_create.go b/api/http/handler/users/user_create.go index 9fb4cddda..7948ec2b6 100644 --- a/api/http/handler/users/user_create.go +++ b/api/http/handler/users/user_create.go @@ -4,10 +4,10 @@ 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" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/users/user_delete.go b/api/http/handler/users/user_delete.go index 90a5b52cc..1c500bfc1 100644 --- a/api/http/handler/users/user_delete.go +++ b/api/http/handler/users/user_delete.go @@ -3,10 +3,10 @@ package users import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/users/user_inspect.go b/api/http/handler/users/user_inspect.go index 9583c833c..a2e185bdf 100644 --- a/api/http/handler/users/user_inspect.go +++ b/api/http/handler/users/user_inspect.go @@ -3,10 +3,10 @@ package users import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" ) // GET request on /api/users/:id diff --git a/api/http/handler/users/user_list.go b/api/http/handler/users/user_list.go index 760c4ec54..9b46ff5eb 100644 --- a/api/http/handler/users/user_list.go +++ b/api/http/handler/users/user_list.go @@ -3,8 +3,8 @@ package users import ( "net/http" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/response" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer/http/security" ) @@ -22,8 +22,9 @@ func (handler *Handler) userList(w http.ResponseWriter, r *http.Request) *httper filteredUsers := security.FilterUsers(users, securityContext) - for _, user := range filteredUsers { - hideFields(&user) + for idx := range filteredUsers { + hideFields(&filteredUsers[idx]) } + return response.JSON(w, filteredUsers) } diff --git a/api/http/handler/users/user_memberships.go b/api/http/handler/users/user_memberships.go index dfbb355ab..2ba837aec 100644 --- a/api/http/handler/users/user_memberships.go +++ b/api/http/handler/users/user_memberships.go @@ -3,10 +3,10 @@ package users import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/users/user_password.go b/api/http/handler/users/user_password.go deleted file mode 100644 index d50f88879..000000000 --- a/api/http/handler/users/user_password.go +++ /dev/null @@ -1,57 +0,0 @@ -package users - -import ( - "net/http" - - "github.com/asaskevich/govalidator" - "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" -) - -type userPasswordPayload struct { - Password string -} - -func (payload *userPasswordPayload) Validate(r *http.Request) error { - if govalidator.IsNull(payload.Password) { - return portainer.Error("Invalid password") - } - return nil -} - -type userPasswordResponse struct { - Valid bool `json:"valid"` -} - -// POST request on /api/users/:id/passwd -func (handler *Handler) userPassword(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - userID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err} - } - - var payload userPasswordPayload - err = request.DecodeAndValidateJSONPayload(r, &payload) - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} - } - - var password = payload.Password - - u, err := handler.UserService.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 { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err} - } - - valid := true - err = handler.CryptoService.CompareHashAndData(u.Password, password) - if err != nil { - valid = false - } - - return response.JSON(w, &userPasswordResponse{Valid: valid}) -} diff --git a/api/http/handler/users/user_update.go b/api/http/handler/users/user_update.go index a8b6c8b1a..4ae5e50ab 100644 --- a/api/http/handler/users/user_update.go +++ b/api/http/handler/users/user_update.go @@ -3,10 +3,10 @@ package users import ( "net/http" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" - "github.com/portainer/portainer/http/response" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/users/user_update_password.go b/api/http/handler/users/user_update_password.go new file mode 100644 index 000000000..e5a65c8e3 --- /dev/null +++ b/api/http/handler/users/user_update_password.go @@ -0,0 +1,74 @@ +package users + +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" + "github.com/portainer/portainer/http/security" +) + +type userUpdatePasswordPayload struct { + Password string + NewPassword string +} + +func (payload *userUpdatePasswordPayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.Password) { + return portainer.Error("Invalid current password") + } + if govalidator.IsNull(payload.NewPassword) { + return portainer.Error("Invalid new password") + } + return nil +} + +// PUT request on /api/users/:id/passwd +func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + userID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err} + } + + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err} + } + + if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update user", portainer.ErrUnauthorized} + } + + var payload userUpdatePasswordPayload + err = request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + user, err := handler.UserService.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 { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a user with the specified identifier inside the database", err} + } + + 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} + } + + user.Password, err = handler.CryptoService.Hash(payload.NewPassword) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to hash user password", portainer.ErrCryptoHashFailure} + } + + err = handler.UserService.UpdateUser(user.ID, user) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user changes inside the database", err} + } + + return response.Empty(w) +} diff --git a/api/http/handler/webhooks/handler.go b/api/http/handler/webhooks/handler.go new file mode 100644 index 000000000..d9b583535 --- /dev/null +++ b/api/http/handler/webhooks/handler.go @@ -0,0 +1,35 @@ +package webhooks + +import ( + "net/http" + + "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" + portainer "github.com/portainer/portainer" + "github.com/portainer/portainer/docker" + "github.com/portainer/portainer/http/security" +) + +// Handler is the HTTP handler used to handle webhook operations. +type Handler struct { + *mux.Router + WebhookService portainer.WebhookService + EndpointService portainer.EndpointService + DockerClientFactory *docker.ClientFactory +} + +// NewHandler creates a handler to manage settings operations. +func NewHandler(bouncer *security.RequestBouncer) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + } + h.Handle("/webhooks", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost) + h.Handle("/webhooks", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookList))).Methods(http.MethodGet) + h.Handle("/webhooks/{id}", + bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete) + h.Handle("/webhooks/{token}", + bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost) + return h +} diff --git a/api/http/handler/webhooks/webhook_create.go b/api/http/handler/webhooks/webhook_create.go new file mode 100644 index 000000000..288435889 --- /dev/null +++ b/api/http/handler/webhooks/webhook_create.go @@ -0,0 +1,66 @@ +package webhooks + +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" + "github.com/satori/go.uuid" +) + +type webhookCreatePayload struct { + ResourceID string + EndpointID int + WebhookType int +} + +func (payload *webhookCreatePayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.ResourceID) { + return portainer.Error("Invalid ResourceID") + } + if payload.EndpointID == 0 { + return portainer.Error("Invalid EndpointID") + } + if payload.WebhookType != 1 { + return portainer.Error("Invalid WebhookType") + } + return nil +} + +func (handler *Handler) webhookCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload webhookCreatePayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + webhook, err := handler.WebhookService.WebhookByResourceID(payload.ResourceID) + if err != nil && err != portainer.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} + } + + token, err := uuid.NewV4() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Error creating unique token", err} + } + + webhook = &portainer.Webhook{ + Token: token.String(), + ResourceID: payload.ResourceID, + EndpointID: portainer.EndpointID(payload.EndpointID), + WebhookType: portainer.WebhookType(payload.WebhookType), + } + + err = handler.WebhookService.CreateWebhook(webhook) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the webhook inside the database", err} + } + + return response.JSON(w, webhook) +} diff --git a/api/http/handler/webhooks/webhook_delete.go b/api/http/handler/webhooks/webhook_delete.go new file mode 100644 index 000000000..6df0e4156 --- /dev/null +++ b/api/http/handler/webhooks/webhook_delete.go @@ -0,0 +1,25 @@ +package webhooks + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer" +) + +// DELETE request on /api/webhook/:serviceID +func (handler *Handler) webhookDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + id, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid webhook id", err} + } + + err = handler.WebhookService.DeleteWebhook(portainer.WebhookID(id)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the webhook from the database", err} + } + + return response.Empty(w) +} diff --git a/api/http/handler/webhooks/webhook_execute.go b/api/http/handler/webhooks/webhook_execute.go new file mode 100644 index 000000000..f43045899 --- /dev/null +++ b/api/http/handler/webhooks/webhook_execute.go @@ -0,0 +1,71 @@ +package webhooks + +import ( + "context" + "net/http" + "strings" + + dockertypes "github.com/docker/docker/api/types" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer" +) + +// Acts on a passed in token UUID to restart the docker service +func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + + webhookToken, err := request.RetrieveRouteVariableValue(r, "token") + + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Invalid service id parameter", err} + } + + webhook, err := handler.WebhookService.WebhookByToken(webhookToken) + + if err == portainer.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} + } + + resourceID := webhook.ResourceID + endpointID := webhook.EndpointID + webhookType := webhook.WebhookType + + 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} + } + switch webhookType { + case portainer.ServiceWebhook: + return handler.executeServiceWebhook(w, endpoint, resourceID) + default: + return &httperror.HandlerError{http.StatusInternalServerError, "Unsupported webhook type", portainer.ErrUnsupportedWebhookType} + } + +} + +func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *portainer.Endpoint, resourceID string) *httperror.HandlerError { + dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Error creating docker client", err} + } + defer dockerClient.Close() + + service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), resourceID, dockertypes.ServiceInspectOptions{InsertDefaults: true}) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Error looking up service", err} + } + + service.Spec.TaskTemplate.ForceUpdate++ + + service.Spec.TaskTemplate.ContainerSpec.Image = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, "@sha")[0] + _, err = dockerClient.ServiceUpdate(context.Background(), resourceID, service.Version, service.Spec, dockertypes.ServiceUpdateOptions{QueryRegistry: true}) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Error updating service", err} + } + return response.Empty(w) +} diff --git a/api/http/handler/webhooks/webhook_list.go b/api/http/handler/webhooks/webhook_list.go new file mode 100644 index 000000000..7ed0df213 --- /dev/null +++ b/api/http/handler/webhooks/webhook_list.go @@ -0,0 +1,47 @@ +package webhooks + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer" +) + +type webhookListOperationFilters struct { + ResourceID string `json:"ResourceID"` + EndpointID int `json:"EndpointID"` +} + +// GET request on /api/webhooks?(filters=) +func (handler *Handler) webhookList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var filters webhookListOperationFilters + err := request.RetrieveJSONQueryParameter(r, "filters", &filters, true) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: filters", err} + } + + webhooks, err := handler.WebhookService.Webhooks() + webhooks = filterWebhooks(webhooks, &filters) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve webhooks from the database", err} + } + + return response.JSON(w, webhooks) +} + +func filterWebhooks(webhooks []portainer.Webhook, filters *webhookListOperationFilters) []portainer.Webhook { + if filters.EndpointID == 0 && filters.ResourceID == "" { + return webhooks + } + + filteredWebhooks := make([]portainer.Webhook, 0, len(webhooks)) + for _, webhook := range webhooks { + if webhook.EndpointID == portainer.EndpointID(filters.EndpointID) && webhook.ResourceID == string(filters.ResourceID) { + filteredWebhooks = append(filteredWebhooks, webhook) + } + } + + return filteredWebhooks +} diff --git a/api/http/handler/websocket/handler.go b/api/http/handler/websocket/handler.go index 20364f52b..3de1cbc60 100644 --- a/api/http/handler/websocket/handler.go +++ b/api/http/handler/websocket/handler.go @@ -3,8 +3,8 @@ package websocket import ( "github.com/gorilla/mux" "github.com/gorilla/websocket" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "github.com/portainer/portainer/http/security" ) diff --git a/api/http/handler/websocket/websocket_exec.go b/api/http/handler/websocket/websocket_exec.go index 80c62b9d9..d797e020a 100644 --- a/api/http/handler/websocket/websocket_exec.go +++ b/api/http/handler/websocket/websocket_exec.go @@ -15,10 +15,10 @@ import ( "github.com/asaskevich/govalidator" "github.com/gorilla/websocket" "github.com/koding/websocketproxy" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" "github.com/portainer/portainer" "github.com/portainer/portainer/crypto" - httperror "github.com/portainer/portainer/http/error" - "github.com/portainer/portainer/http/request" ) type webSocketExecRequestParams struct { @@ -252,7 +252,7 @@ func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net func streamFromTCPConnToWebsocketConn(websocketConn *websocket.Conn, br *bufio.Reader, errorChan chan error) { for { - out := make([]byte, 1024) + out := make([]byte, 2048) _, err := br.Read(out) if err != nil { errorChan <- err diff --git a/api/http/proxy/access_control.go b/api/http/proxy/access_control.go index 331b9d0da..e63dc6346 100644 --- a/api/http/proxy/access_control.go +++ b/api/http/proxy/access_control.go @@ -1,6 +1,8 @@ package proxy -import "github.com/portainer/portainer" +import ( + "github.com/portainer/portainer" +) type ( // ExtendedStack represents a stack combined with its associated access control @@ -15,7 +17,7 @@ type ( // It will retrieve an identifier from the labels object. If an identifier exists, it will check for // an existing resource control associated to it. // Returns a decorated object and authorized access (true) when a resource control is found and the user can access the resource. -// Returns the original object and authorized access (true) when no resource control is found. +// Returns the original object and denied access (false) when no resource control is found. // Returns the original object and denied access (false) when a resource control is found and the user cannot access the resource. func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string, context *restrictedOperationContext) (map[string]interface{}, bool) { @@ -24,32 +26,31 @@ func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string resourceIdentifier := labelsObject[labelIdentifier].(string) return applyResourceAccessControl(resourceObject, resourceIdentifier, context) } - return resourceObject, true + return resourceObject, false } // applyResourceAccessControl returns an optionally decorated object as the first return value and the // access level for the user (granted or denied) as the second return value. // Returns a decorated object and authorized access (true) when a resource control is found to the specified resource // identifier and the user can access the resource. -// Returns the original object and authorized access (true) when no resource control is found for the specified +// Returns the original object and authorized access (false) when no resource control is found for the specified // resource identifier. // Returns the original object and denied access (false) when a resource control is associated to the resource // and the user cannot access the resource. func applyResourceAccessControl(resourceObject map[string]interface{}, resourceIdentifier string, context *restrictedOperationContext) (map[string]interface{}, bool) { - authorizedAccess := true - resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls) - if resourceControl != nil { - if context.isAdmin || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) { - resourceObject = decorateObject(resourceObject, resourceControl) - } else { - authorizedAccess = false - } + if resourceControl == nil { + return resourceObject, context.isAdmin } - return resourceObject, authorizedAccess + if context.isAdmin || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) { + resourceObject = decorateObject(resourceObject, resourceControl) + return resourceObject, true + } + + return resourceObject, false } // decorateResourceWithAccessControlFromLabel will retrieve an identifier from the labels object. If an identifier exists, @@ -94,7 +95,7 @@ func canUserAccessResource(userID portainer.UserID, userTeamIDs []portainer.Team } } - return false + return resourceControl.Public } func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} { @@ -123,6 +124,10 @@ func getResourceControlByResourceID(resourceID string, resourceControls []portai // CanAccessStack checks if a user can access a stack func CanAccessStack(stack *portainer.Stack, resourceControl *portainer.ResourceControl, userID portainer.UserID, memberships []portainer.TeamMembership) bool { + if resourceControl == nil { + return false + } + userTeamIDs := make([]portainer.TeamID, 0) for _, membership := range memberships { userTeamIDs = append(userTeamIDs, membership.TeamID) @@ -132,7 +137,7 @@ func CanAccessStack(stack *portainer.Stack, resourceControl *portainer.ResourceC return true } - return false + return resourceControl.Public } // FilterStacks filters stacks based on user role and resource controls. @@ -149,9 +154,9 @@ func FilterStacks(stacks []portainer.Stack, resourceControls []portainer.Resourc for _, stack := range stacks { extendedStack := ExtendedStack{stack, portainer.ResourceControl{}} resourceControl := getResourceControlByResourceID(stack.Name, resourceControls) - if resourceControl == nil { + if resourceControl == nil && isAdmin { filteredStacks = append(filteredStacks, extendedStack) - } else if resourceControl != nil && (isAdmin || canUserAccessResource(userID, userTeamIDs, resourceControl)) { + } else if resourceControl != nil && (isAdmin || resourceControl.Public || canUserAccessResource(userID, userTeamIDs, resourceControl)) { extendedStack.ResourceControl = *resourceControl filteredStacks = append(filteredStacks, extendedStack) } diff --git a/api/http/proxy/containers.go b/api/http/proxy/containers.go index 9da66e21f..4e6c835ff 100644 --- a/api/http/proxy/containers.go +++ b/api/http/proxy/containers.go @@ -62,27 +62,27 @@ func containerInspectOperation(response *http.Response, executor *operationExecu containerID := responseObject[containerIdentifier].(string) responseObject, access := applyResourceAccessControl(responseObject, containerID, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject) responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForServiceIdentifier, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForSwarmStackIdentifier, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForComposeStackIdentifier, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } - return rewriteResponse(response, responseObject, http.StatusOK) + return rewriteAccessDeniedResponse(response) } // extractContainerLabelsFromContainerInspectObject retrieve the Labels of the container if present. @@ -148,19 +148,20 @@ func filterContainerList(containerData []interface{}, context *restrictedOperati containerID := containerObject[containerIdentifier].(string) containerObject, access := applyResourceAccessControl(containerObject, containerID, context) - if access { + if !access { containerLabels := extractContainerLabelsFromContainerListObject(containerObject) containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForComposeStackIdentifier, context) - if access { + if !access { containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForServiceIdentifier, context) - if access { + if !access { containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForSwarmStackIdentifier, context) - if access { - filteredContainerData = append(filteredContainerData, containerObject) - } } } } + + if access { + filteredContainerData = append(filteredContainerData, containerObject) + } } return filteredContainerData, nil diff --git a/api/http/proxy/local.go b/api/http/proxy/local.go index 8a7f5842d..7686768ad 100644 --- a/api/http/proxy/local.go +++ b/api/http/proxy/local.go @@ -5,7 +5,7 @@ import ( "log" "net/http" - httperror "github.com/portainer/portainer/http/error" + httperror "github.com/portainer/libhttp/error" ) type localProxy struct { diff --git a/api/http/proxy/networks.go b/api/http/proxy/networks.go index 7e40d385a..a131ab2ab 100644 --- a/api/http/proxy/networks.go +++ b/api/http/proxy/networks.go @@ -53,17 +53,17 @@ func networkInspectOperation(response *http.Response, executor *operationExecuto networkID := responseObject[networkIdentifier].(string) responseObject, access := applyResourceAccessControl(responseObject, networkID, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } networkLabels := extractNetworkLabelsFromNetworkInspectObject(responseObject) responseObject, access = applyResourceAccessControlFromLabel(networkLabels, responseObject, networkLabelForStackIdentifier, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } - return rewriteResponse(response, responseObject, http.StatusOK) + return rewriteAccessDeniedResponse(response) } // extractNetworkLabelsFromNetworkInspectObject retrieve the Labels of the network if present. @@ -121,12 +121,13 @@ func filterNetworkList(networkData []interface{}, context *restrictedOperationCo networkID := networkObject[networkIdentifier].(string) networkObject, access := applyResourceAccessControl(networkObject, networkID, context) - if access { + if !access { networkLabels := extractNetworkLabelsFromNetworkListObject(networkObject) networkObject, access = applyResourceAccessControlFromLabel(networkLabels, networkObject, networkLabelForStackIdentifier, context) - if access { - filteredNetworkData = append(filteredNetworkData, networkObject) - } + } + + if access { + filteredNetworkData = append(filteredNetworkData, networkObject) } } diff --git a/api/http/proxy/services.go b/api/http/proxy/services.go index 9934cf381..b11208b58 100644 --- a/api/http/proxy/services.go +++ b/api/http/proxy/services.go @@ -53,17 +53,17 @@ func serviceInspectOperation(response *http.Response, executor *operationExecuto serviceID := responseObject[serviceIdentifier].(string) responseObject, access := applyResourceAccessControl(responseObject, serviceID, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } serviceLabels := extractServiceLabelsFromServiceInspectObject(responseObject) responseObject, access = applyResourceAccessControlFromLabel(serviceLabels, responseObject, serviceLabelForStackIdentifier, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } - return rewriteResponse(response, responseObject, http.StatusOK) + return rewriteAccessDeniedResponse(response) } // extractServiceLabelsFromServiceInspectObject retrieve the Labels of the service if present. @@ -129,12 +129,13 @@ func filterServiceList(serviceData []interface{}, context *restrictedOperationCo serviceID := serviceObject[serviceIdentifier].(string) serviceObject, access := applyResourceAccessControl(serviceObject, serviceID, context) - if access { + if !access { serviceLabels := extractServiceLabelsFromServiceListObject(serviceObject) serviceObject, access = applyResourceAccessControlFromLabel(serviceLabels, serviceObject, serviceLabelForStackIdentifier, context) - if access { - filteredServiceData = append(filteredServiceData, serviceObject) - } + } + + if access { + filteredServiceData = append(filteredServiceData, serviceObject) } } diff --git a/api/http/proxy/tasks.go b/api/http/proxy/tasks.go index daea6b926..d75ce54ec 100644 --- a/api/http/proxy/tasks.go +++ b/api/http/proxy/tasks.go @@ -65,12 +65,13 @@ func filterTaskList(taskData []interface{}, context *restrictedOperationContext) serviceID := taskObject[taskServiceIdentifier].(string) taskObject, access := applyResourceAccessControl(taskObject, serviceID, context) - if access { + if !access { taskLabels := extractTaskLabelsFromTaskListObject(taskObject) taskObject, access = applyResourceAccessControlFromLabel(taskLabels, taskObject, taskLabelForStackIdentifier, context) - if access { - filteredTaskData = append(filteredTaskData, taskObject) - } + } + + if access { + filteredTaskData = append(filteredTaskData, taskObject) } } diff --git a/api/http/proxy/volumes.go b/api/http/proxy/volumes.go index c0582a6bd..82f1b5cf6 100644 --- a/api/http/proxy/volumes.go +++ b/api/http/proxy/volumes.go @@ -62,17 +62,17 @@ func volumeInspectOperation(response *http.Response, executor *operationExecutor volumeID := responseObject[volumeIdentifier].(string) responseObject, access := applyResourceAccessControl(responseObject, volumeID, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } volumeLabels := extractVolumeLabelsFromVolumeInspectObject(responseObject) responseObject, access = applyResourceAccessControlFromLabel(volumeLabels, responseObject, volumeLabelForStackIdentifier, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) + if access { + return rewriteResponse(response, responseObject, http.StatusOK) } - return rewriteResponse(response, responseObject, http.StatusOK) + return rewriteAccessDeniedResponse(response) } // extractVolumeLabelsFromVolumeInspectObject retrieve the Labels of the volume if present. @@ -130,12 +130,13 @@ func filterVolumeList(volumeData []interface{}, context *restrictedOperationCont volumeID := volumeObject[volumeIdentifier].(string) volumeObject, access := applyResourceAccessControl(volumeObject, volumeID, context) - if access { + if !access { volumeLabels := extractVolumeLabelsFromVolumeListObject(volumeObject) volumeObject, access = applyResourceAccessControlFromLabel(volumeLabels, volumeObject, volumeLabelForStackIdentifier, context) - if access { - filteredVolumeData = append(filteredVolumeData, volumeObject) - } + } + + if access { + filteredVolumeData = append(filteredVolumeData, volumeObject) } } diff --git a/api/http/request/request.go b/api/http/request/request.go deleted file mode 100644 index ee56b0d05..000000000 --- a/api/http/request/request.go +++ /dev/null @@ -1,163 +0,0 @@ -package request - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "strconv" - - "github.com/gorilla/mux" - "github.com/portainer/portainer" -) - -const ( - // ErrInvalidQueryParameter defines an error raised when a mandatory query parameter has an invalid value. - ErrInvalidQueryParameter = portainer.Error("Invalid query parameter") - // errInvalidRequestURL defines an error raised when the data sent in the query or the URL is invalid - errInvalidRequestURL = portainer.Error("Invalid request URL") - // errMissingQueryParameter defines an error raised when a mandatory query parameter is missing. - errMissingQueryParameter = portainer.Error("Missing query parameter") - // errMissingFormDataValue defines an error raised when a mandatory form data value is missing. - errMissingFormDataValue = portainer.Error("Missing form data value") -) - -// PayloadValidation is an interface used to validate the payload of a request. -type PayloadValidation interface { - Validate(request *http.Request) error -} - -// DecodeAndValidateJSONPayload decodes the body of the request into an object -// implementing the PayloadValidation interface. -// It also triggers a validation of object content. -func DecodeAndValidateJSONPayload(request *http.Request, v PayloadValidation) error { - if err := json.NewDecoder(request.Body).Decode(v); err != nil { - return err - } - return v.Validate(request) -} - -// RetrieveMultiPartFormFile returns the content of an uploaded file (form data) as bytes. -func RetrieveMultiPartFormFile(request *http.Request, requestParameter string) ([]byte, error) { - file, _, err := request.FormFile(requestParameter) - if err != nil { - return nil, err - } - defer file.Close() - - fileContent, err := ioutil.ReadAll(file) - if err != nil { - return nil, err - } - return fileContent, nil -} - -// RetrieveMultiPartFormJSONValue decodes the value of some form data as a JSON object into the target parameter. -// If optional is set to true, will not return an error when the form data value is not found. -func RetrieveMultiPartFormJSONValue(request *http.Request, name string, target interface{}, optional bool) error { - value, err := RetrieveMultiPartFormValue(request, name, optional) - if err != nil { - return err - } - if value == "" { - return nil - } - return json.Unmarshal([]byte(value), target) -} - -// RetrieveMultiPartFormValue returns the value of some form data as a string. -// If optional is set to true, will not return an error when the form data value is not found. -func RetrieveMultiPartFormValue(request *http.Request, name string, optional bool) (string, error) { - value := request.FormValue(name) - if value == "" && !optional { - return "", errMissingFormDataValue - } - return value, nil -} - -// RetrieveNumericMultiPartFormValue returns the value of some form data as an integer. -// If optional is set to true, will not return an error when the form data value is not found. -func RetrieveNumericMultiPartFormValue(request *http.Request, name string, optional bool) (int, error) { - value, err := RetrieveMultiPartFormValue(request, name, optional) - if err != nil { - return 0, err - } - return strconv.Atoi(value) -} - -// RetrieveBooleanMultiPartFormValue returns the value of some form data as a boolean. -// If optional is set to true, will not return an error when the form data value is not found. -func RetrieveBooleanMultiPartFormValue(request *http.Request, name string, optional bool) (bool, error) { - value, err := RetrieveMultiPartFormValue(request, name, optional) - if err != nil { - return false, err - } - return value == "true", nil -} - -// RetrieveRouteVariableValue returns the value of a route variable as a string. -func RetrieveRouteVariableValue(request *http.Request, name string) (string, error) { - routeVariables := mux.Vars(request) - if routeVariables == nil { - return "", errInvalidRequestURL - } - routeVar := routeVariables[name] - if routeVar == "" { - return "", errInvalidRequestURL - } - return routeVar, nil -} - -// RetrieveNumericRouteVariableValue returns the value of a route variable as an integer. -func RetrieveNumericRouteVariableValue(request *http.Request, name string) (int, error) { - routeVar, err := RetrieveRouteVariableValue(request, name) - if err != nil { - return 0, err - } - return strconv.Atoi(routeVar) -} - -// RetrieveQueryParameter returns the value of a query parameter as a string. -// If optional is set to true, will not return an error when the query parameter is not found. -func RetrieveQueryParameter(request *http.Request, name string, optional bool) (string, error) { - queryParameter := request.FormValue(name) - if queryParameter == "" && !optional { - return "", errMissingQueryParameter - } - return queryParameter, nil -} - -// RetrieveNumericQueryParameter returns the value of a query parameter as an integer. -// If optional is set to true, will not return an error when the query parameter is not found. -func RetrieveNumericQueryParameter(request *http.Request, name string, optional bool) (int, error) { - queryParameter, err := RetrieveQueryParameter(request, name, optional) - if err != nil { - return 0, err - } - if queryParameter == "" && optional { - return 0, nil - } - return strconv.Atoi(queryParameter) -} - -// RetrieveBooleanQueryParameter returns the value of a query parameter as a boolean. -// If optional is set to true, will not return an error when the query parameter is not found. -func RetrieveBooleanQueryParameter(request *http.Request, name string, optional bool) (bool, error) { - queryParameter, err := RetrieveQueryParameter(request, name, optional) - if err != nil { - return false, err - } - return queryParameter == "true", nil -} - -// RetrieveJSONQueryParameter decodes the value of a query paramater as a JSON object into the target parameter. -// If optional is set to true, will not return an error when the query parameter is not found. -func RetrieveJSONQueryParameter(request *http.Request, name string, target interface{}, optional bool) error { - queryParameter, err := RetrieveQueryParameter(request, name, optional) - if err != nil { - return err - } - if queryParameter == "" { - return nil - } - return json.Unmarshal([]byte(queryParameter), target) -} diff --git a/api/http/response/response.go b/api/http/response/response.go deleted file mode 100644 index 2451abd24..000000000 --- a/api/http/response/response.go +++ /dev/null @@ -1,25 +0,0 @@ -package response - -import ( - "encoding/json" - "net/http" - - httperror "github.com/portainer/portainer/http/error" -) - -// JSON encodes data to rw in JSON format. Returns a pointer to a -// HandlerError if encoding fails. -func JSON(rw http.ResponseWriter, data interface{}) *httperror.HandlerError { - rw.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(rw).Encode(data) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to write JSON response", err} - } - return nil -} - -// Empty merely sets the response code to NoContent (204). -func Empty(rw http.ResponseWriter) *httperror.HandlerError { - rw.WriteHeader(http.StatusNoContent) - return nil -} diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go index 7fa7a6f31..6ffa3a0e4 100644 --- a/api/http/security/authorization.go +++ b/api/http/security/authorization.go @@ -1,21 +1,19 @@ package security -import "github.com/portainer/portainer" +import ( + "github.com/portainer/portainer" +) // AuthorizedResourceControlDeletion ensure that the user can delete a resource control object. // A non-administrator user cannot delete a resource control where: -// * the AdministratorsOnly flag is set +// * the Public flag is false // * he is not one of the users in the user accesses // * he is not a member of any team within the team accesses func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { - if context.IsAdmin { + if context.IsAdmin || resourceControl.Public { return true } - if resourceControl.AdministratorsOnly { - return false - } - userAccessesCount := len(resourceControl.UserAccesses) teamAccessesCount := len(resourceControl.TeamAccesses) @@ -42,39 +40,25 @@ func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceContro // AuthorizedResourceControlAccess checks whether the user can alter an existing resource control. func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { - if context.IsAdmin { + if context.IsAdmin || resourceControl.Public { return true } - if resourceControl.AdministratorsOnly { - return false - } - - authorizedTeamAccess := false for _, access := range resourceControl.TeamAccesses { for _, membership := range context.UserMemberships { if membership.TeamID == access.TeamID { - authorizedTeamAccess = true - break + return true } } } - if !authorizedTeamAccess { - return false - } - authorizedUserAccess := false for _, access := range resourceControl.UserAccesses { if context.UserID == access.UserID { - authorizedUserAccess = true - break + return true } } - if !authorizedUserAccess { - return false - } - return true + return false } // AuthorizedResourceControlUpdate ensure that the user can update a resource control object. @@ -92,20 +76,16 @@ func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, // AuthorizedResourceControlCreation ensure that the user can create a resource control object. // A non-administrator user cannot create a resource control where: -// * the AdministratorsOnly flag is set +// * the Public flag is set false // * he wants to create a resource control without any user/team accesses // * he wants to add more than one user in the user accesses // * he wants tp add a user in the user accesses that is not corresponding to its id // * he wants to add a team he is not a member of func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { - if context.IsAdmin { + if context.IsAdmin || resourceControl.Public { return true } - if resourceControl.AdministratorsOnly { - return false - } - userAccessesCount := len(resourceControl.UserAccesses) teamAccessesCount := len(resourceControl.TeamAccesses) @@ -126,19 +106,15 @@ func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceContro if teamAccessesCount > 0 { for _, access := range resourceControl.TeamAccesses { - isMember := false for _, membership := range context.UserMemberships { if membership.TeamID == access.TeamID { - isMember = true + return true } } - if !isMember { - return false - } } } - return true + return false } // AuthorizedTeamManagement ensure that access to the management of the specified team is granted. diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 798d37f7b..0b25bd389 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -1,8 +1,8 @@ package security import ( + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" "net/http" "strings" @@ -114,8 +114,9 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain // mwSecureHeaders provides secure headers middleware for handlers. func mwSecureHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("X-Content-Type-Options", "nosniff") w.Header().Add("X-Frame-Options", "DENY") + w.Header().Add("X-XSS-Protection", "1; mode=block") + w.Header().Add("X-Content-Type-Options", "nosniff") next.ServeHTTP(w, r) }) } diff --git a/api/http/security/rate_limiter.go b/api/http/security/rate_limiter.go index 27ab2523a..307329723 100644 --- a/api/http/security/rate_limiter.go +++ b/api/http/security/rate_limiter.go @@ -6,8 +6,8 @@ import ( "time" "github.com/g07cha/defender" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/portainer" - httperror "github.com/portainer/portainer/http/error" ) // RateLimiter represents an entity that manages request rate limiting diff --git a/api/http/server.go b/api/http/server.go index 4babc109b..2258e86d3 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -4,6 +4,7 @@ import ( "time" "github.com/portainer/portainer" + "github.com/portainer/portainer/docker" "github.com/portainer/portainer/http/handler" "github.com/portainer/portainer/http/handler/auth" "github.com/portainer/portainer/http/handler/dockerhub" @@ -11,6 +12,7 @@ import ( "github.com/portainer/portainer/http/handler/endpointproxy" "github.com/portainer/portainer/http/handler/endpoints" "github.com/portainer/portainer/http/handler/file" + "github.com/portainer/portainer/http/handler/motd" "github.com/portainer/portainer/http/handler/registries" "github.com/portainer/portainer/http/handler/resourcecontrols" "github.com/portainer/portainer/http/handler/settings" @@ -22,6 +24,7 @@ import ( "github.com/portainer/portainer/http/handler/templates" "github.com/portainer/portainer/http/handler/upload" "github.com/portainer/portainer/http/handler/users" + "github.com/portainer/portainer/http/handler/webhooks" "github.com/portainer/portainer/http/handler/websocket" "github.com/portainer/portainer/http/proxy" "github.com/portainer/portainer/http/security" @@ -59,10 +62,12 @@ type Server struct { TeamMembershipService portainer.TeamMembershipService TemplateService portainer.TemplateService UserService portainer.UserService + WebhookService portainer.WebhookService Handler *handler.Handler SSL bool SSLCert string SSLKey string + DockerClientFactory *docker.ClientFactory } // Start starts the HTTP server @@ -115,6 +120,8 @@ func (server *Server) Start() error { var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) + var motdHandler = motd.NewHandler(requestBouncer) + var registryHandler = registries.NewHandler(requestBouncer) registryHandler.RegistryService = server.RegistryService @@ -151,11 +158,12 @@ func (server *Server) Start() error { var templatesHandler = templates.NewHandler(requestBouncer) templatesHandler.TemplateService = server.TemplateService + templatesHandler.SettingsService = server.SettingsService var uploadHandler = upload.NewHandler(requestBouncer) uploadHandler.FileService = server.FileService - var userHandler = users.NewHandler(requestBouncer) + var userHandler = users.NewHandler(requestBouncer, rateLimiter) userHandler.UserService = server.UserService userHandler.TeamService = server.TeamService userHandler.TeamMembershipService = server.TeamMembershipService @@ -167,6 +175,11 @@ func (server *Server) Start() error { websocketHandler.EndpointService = server.EndpointService websocketHandler.SignatureService = server.SignatureService + var webhookHandler = webhooks.NewHandler(requestBouncer) + webhookHandler.WebhookService = server.WebhookService + webhookHandler.EndpointService = server.EndpointService + webhookHandler.DockerClientFactory = server.DockerClientFactory + server.Handler = &handler.Handler{ AuthHandler: authHandler, DockerHubHandler: dockerHubHandler, @@ -174,6 +187,7 @@ func (server *Server) Start() error { EndpointHandler: endpointHandler, EndpointProxyHandler: endpointProxyHandler, FileHandler: fileHandler, + MOTDHandler: motdHandler, RegistryHandler: registryHandler, ResourceControlHandler: resourceControlHandler, SettingsHandler: settingsHandler, @@ -186,6 +200,7 @@ func (server *Server) Start() error { UploadHandler: uploadHandler, UserHandler: userHandler, WebSocketHandler: websocketHandler, + WebhookHandler: webhookHandler, } if server.SSL { diff --git a/api/portainer.go b/api/portainer.go index ac5cda1a1..6367b14e0 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -7,7 +7,7 @@ type ( Value string `json:"value"` } - // CLIFlags represents the available flags on the CLI. + // CLIFlags represents the available flags on the CLI CLIFlags struct { Addr *string AdminPassword *string @@ -35,7 +35,7 @@ type ( SnapshotInterval *string } - // Status represents the application status. + // Status represents the application status Status struct { Authentication bool `json:"Authentication"` EndpointManagement bool `json:"EndpointManagement"` @@ -44,7 +44,7 @@ type ( Version string `json:"Version"` } - // LDAPSettings represents the settings used to connect to a LDAP server. + // LDAPSettings represents the settings used to connect to a LDAP server LDAPSettings struct { ReaderDN string `json:"ReaderDN"` Password string `json:"Password"` @@ -56,7 +56,7 @@ type ( AutoCreateUsers bool `json:"AutoCreateUsers"` } - // TLSConfiguration represents a TLS configuration. + // TLSConfiguration represents a TLS configuration TLSConfiguration struct { TLS bool `json:"TLS"` TLSSkipVerify bool `json:"TLSSkipVerify"` @@ -65,21 +65,21 @@ type ( TLSKeyPath string `json:"TLSKey,omitempty"` } - // LDAPSearchSettings represents settings used to search for users in a LDAP server. + // LDAPSearchSettings represents settings used to search for users in a LDAP server LDAPSearchSettings struct { BaseDN string `json:"BaseDN"` Filter string `json:"Filter"` UserNameAttribute string `json:"UserNameAttribute"` } - // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server. + // LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server LDAPGroupSearchSettings struct { GroupBaseDN string `json:"GroupBaseDN"` GroupFilter string `json:"GroupFilter"` GroupAttribute string `json:"GroupAttribute"` } - // Settings represents the application settings. + // Settings represents the application settings Settings struct { LogoURL string `json:"LogoURL"` BlackListedLabels []Pair `json:"BlackListedLabels"` @@ -88,14 +88,14 @@ type ( AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` // Deprecated fields DisplayDonationHeader bool DisplayExternalContributors bool - TemplatesURL string } - // User represents a user account. + // User represents a user account User struct { ID UserID `json:"Id"` Username string `json:"Username"` @@ -110,10 +110,10 @@ type ( // or a regular user UserRole int - // AuthenticationMethod represents the authentication method used to authenticate a user. + // AuthenticationMethod represents the authentication method used to authenticate a user AuthenticationMethod int - // Team represents a list of user accounts. + // Team represents a list of user accounts Team struct { ID TeamID `json:"Id"` Name string `json:"Name"` @@ -136,20 +136,20 @@ type ( // MembershipRole represents the role of a user within a team MembershipRole int - // TokenData represents the data embedded in a JWT token. + // TokenData represents the data embedded in a JWT token TokenData struct { ID UserID Username string Role UserRole } - // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier). + // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier) StackID int - // StackType represents the type of the stack (compose v2, stack deploy v3). + // StackType represents the type of the stack (compose v2, stack deploy v3) StackType int - // Stack represents a Docker stack created via docker stack deploy. + // Stack represents a Docker stack created via docker stack deploy Stack struct { ID StackID `json:"Id"` Name string `json:"Name"` @@ -161,11 +161,11 @@ type ( ProjectPath string } - // RegistryID represents a registry identifier. + // RegistryID represents a registry identifier RegistryID int // Registry represents a Docker registry with all the info required - // to connect to it. + // to connect to it Registry struct { ID RegistryID `json:"Id"` Name string `json:"Name"` @@ -178,24 +178,24 @@ type ( } // DockerHub represents all the required information to connect and use the - // Docker Hub. + // Docker Hub DockerHub struct { Authentication bool `json:"Authentication"` Username string `json:"Username"` Password string `json:"Password,omitempty"` } - // EndpointID represents an endpoint identifier. + // EndpointID represents an endpoint identifier EndpointID int - // EndpointType represents the type of an endpoint. + // EndpointType represents the type of an endpoint EndpointType int // EndpointStatus represents the status of an endpoint EndpointStatus int // Endpoint represents a Docker endpoint with all the info required - // to connect to it. + // to connect to it Endpoint struct { ID EndpointID `json:"Id"` Name string `json:"Name"` @@ -220,6 +220,21 @@ type ( TLSKeyPath string `json:"TLSKey,omitempty"` } + // WebhookID represents an webhook identifier. + WebhookID int + + // WebhookType represents the type of resource a webhook is related to + WebhookType int + + // Webhook represents a url webhook that can be used to update a service + Webhook struct { + ID WebhookID `json:"Id"` + Token string `json:"Token"` + ResourceID string `json:"ResourceId"` + EndpointID EndpointID `json:"EndpointId"` + WebhookType WebhookType `json:"Type"` + } + // AzureCredentials represents the credentials used to connect to an Azure // environment. AzureCredentials struct { @@ -243,10 +258,10 @@ type ( StackCount int `json:"StackCount"` } - // EndpointGroupID represents an endpoint group identifier. + // EndpointGroupID represents an endpoint group identifier EndpointGroupID int - // EndpointGroup represents a group of endpoints. + // EndpointGroup represents a group of endpoints EndpointGroup struct { ID EndpointGroupID `json:"Id"` Name string `json:"Name"` @@ -259,66 +274,69 @@ type ( Labels []Pair `json:"Labels"` } - // EndpointExtension represents a extension associated to an endpoint. + // EndpointExtension represents a extension associated to an endpoint EndpointExtension struct { Type EndpointExtensionType `json:"Type"` URL string `json:"URL"` } // EndpointExtensionType represents the type of an endpoint extension. Only - // one extension of each type can be associated to an endpoint. + // one extension of each type can be associated to an endpoint EndpointExtensionType int - // ResourceControlID represents a resource control identifier. + // ResourceControlID represents a resource control identifier ResourceControlID int // ResourceControl represent a reference to a Docker resource with specific access controls ResourceControl struct { - ID ResourceControlID `json:"Id"` - ResourceID string `json:"ResourceId"` - SubResourceIDs []string `json:"SubResourceIds"` - Type ResourceControlType `json:"Type"` - AdministratorsOnly bool `json:"AdministratorsOnly"` - UserAccesses []UserResourceAccess `json:"UserAccesses"` - TeamAccesses []TeamResourceAccess `json:"TeamAccesses"` + ID ResourceControlID `json:"Id"` + ResourceID string `json:"ResourceId"` + SubResourceIDs []string `json:"SubResourceIds"` + Type ResourceControlType `json:"Type"` + UserAccesses []UserResourceAccess `json:"UserAccesses"` + TeamAccesses []TeamResourceAccess `json:"TeamAccesses"` + Public bool `json:"Public"` // Deprecated fields // Deprecated in DBVersion == 2 OwnerID UserID `json:"OwnerId,omitempty"` AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"` + + // Deprecated in DBVersion == 14 + AdministratorsOnly bool `json:"AdministratorsOnly,omitempty"` } - // ResourceControlType represents the type of resource associated to the resource control (volume, container, service...). + // ResourceControlType represents the type of resource associated to the resource control (volume, container, service...) ResourceControlType int - // UserResourceAccess represents the level of control on a resource for a specific user. + // UserResourceAccess represents the level of control on a resource for a specific user UserResourceAccess struct { UserID UserID `json:"UserId"` AccessLevel ResourceAccessLevel `json:"AccessLevel"` } - // TeamResourceAccess represents the level of control on a resource for a specific team. + // TeamResourceAccess represents the level of control on a resource for a specific team TeamResourceAccess struct { TeamID TeamID `json:"TeamId"` AccessLevel ResourceAccessLevel `json:"AccessLevel"` } - // TagID represents a tag identifier. + // TagID represents a tag identifier TagID int - // Tag represents a tag that can be associated to a resource. + // Tag represents a tag that can be associated to a resource Tag struct { ID TagID Name string `json:"Name"` } - // TemplateID represents a template identifier. + // TemplateID represents a template identifier TemplateID int - // TemplateType represents the type of a template. + // TemplateType represents the type of a template TemplateType int - // Template represents an application template. + // Template represents an application template Template struct { // Mandatory container/stack fields ID TemplateID `json:"Id"` @@ -354,7 +372,7 @@ type ( Hostname string `json:"hostname,omitempty"` } - // TemplateEnv represents a template environment variable configuration. + // TemplateEnv represents a template environment variable configuration TemplateEnv struct { Name string `json:"name"` Label string `json:"label,omitempty"` @@ -364,41 +382,41 @@ type ( Select []TemplateEnvSelect `json:"select,omitempty"` } - // TemplateVolume represents a template volume configuration. + // TemplateVolume represents a template volume configuration TemplateVolume struct { Container string `json:"container"` Bind string `json:"bind,omitempty"` ReadOnly bool `json:"readonly,omitempty"` } - // TemplateRepository represents the git repository configuration for a template. + // TemplateRepository represents the git repository configuration for a template TemplateRepository struct { URL string `json:"url"` StackFile string `json:"stackfile"` } // TemplateEnvSelect represents text/value pair that will be displayed as a choice for the - // template user. + // template user TemplateEnvSelect struct { Text string `json:"text"` Value string `json:"value"` Default bool `json:"default"` } - // ResourceAccessLevel represents the level of control associated to a resource. + // ResourceAccessLevel represents the level of control associated to a resource ResourceAccessLevel int // TLSFileType represents a type of TLS file required to connect to a Docker endpoint. - // It can be either a TLS CA file, a TLS certificate file or a TLS key file. + // It can be either a TLS CA file, a TLS certificate file or a TLS key file TLSFileType int - // CLIService represents a service for managing CLI. + // 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 defines the interface to manage the data DataStore interface { Open() error Init() error @@ -406,12 +424,12 @@ type ( MigrateData() error } - // Server defines the interface to serve the API. + // Server defines the interface to serve the API Server interface { Start() error } - // UserService represents a service for managing user data. + // UserService represents a service for managing user data UserService interface { User(ID UserID) (*User, error) UserByUsername(username string) (*User, error) @@ -422,7 +440,7 @@ type ( DeleteUser(ID UserID) error } - // TeamService represents a service for managing user data. + // TeamService represents a service for managing user data TeamService interface { Team(ID TeamID) (*Team, error) TeamByName(name string) (*Team, error) @@ -432,7 +450,7 @@ type ( DeleteTeam(ID TeamID) error } - // TeamMembershipService represents a service for managing team membership data. + // TeamMembershipService represents a service for managing team membership data TeamMembershipService interface { TeamMembership(ID TeamMembershipID) (*TeamMembership, error) TeamMemberships() ([]TeamMembership, error) @@ -445,7 +463,7 @@ type ( DeleteTeamMembershipByTeamID(teamID TeamID) error } - // EndpointService represents a service for managing endpoint data. + // EndpointService represents a service for managing endpoint data EndpointService interface { Endpoint(ID EndpointID) (*Endpoint, error) Endpoints() ([]Endpoint, error) @@ -456,7 +474,7 @@ type ( GetNextIdentifier() int } - // EndpointGroupService represents a service for managing endpoint group data. + // EndpointGroupService represents a service for managing endpoint group data EndpointGroupService interface { EndpointGroup(ID EndpointGroupID) (*EndpointGroup, error) EndpointGroups() ([]EndpointGroup, error) @@ -465,7 +483,7 @@ type ( DeleteEndpointGroup(ID EndpointGroupID) error } - // RegistryService represents a service for managing registry data. + // RegistryService represents a service for managing registry data RegistryService interface { Registry(ID RegistryID) (*Registry, error) Registries() ([]Registry, error) @@ -474,7 +492,7 @@ type ( DeleteRegistry(ID RegistryID) error } - // StackService represents a service for managing stack data. + // StackService represents a service for managing stack data StackService interface { Stack(ID StackID) (*Stack, error) StackByName(name string) (*Stack, error) @@ -485,25 +503,35 @@ type ( GetNextIdentifier() int } - // DockerHubService represents a service for managing the DockerHub object. + // DockerHubService represents a service for managing the DockerHub object DockerHubService interface { DockerHub() (*DockerHub, error) UpdateDockerHub(registry *DockerHub) error } - // SettingsService represents a service for managing application settings. + // SettingsService represents a service for managing application settings SettingsService interface { Settings() (*Settings, error) UpdateSettings(settings *Settings) error } - // VersionService represents a service for managing version data. + // VersionService represents a service for managing version data VersionService interface { DBVersion() (int, error) StoreDBVersion(version int) error } - // ResourceControlService represents a service for managing resource control data. + // WebhookService represents a service for managing webhook data. + WebhookService interface { + Webhooks() ([]Webhook, error) + Webhook(ID WebhookID) (*Webhook, error) + CreateWebhook(portainer *Webhook) error + WebhookByResourceID(resourceID string) (*Webhook, error) + WebhookByToken(token string) (*Webhook, error) + DeleteWebhook(serviceID WebhookID) error + } + + // ResourceControlService represents a service for managing resource control data ResourceControlService interface { ResourceControl(ID ResourceControlID) (*ResourceControl, error) ResourceControlByResourceID(resourceID string) (*ResourceControl, error) @@ -513,14 +541,14 @@ type ( DeleteResourceControl(ID ResourceControlID) error } - // TagService represents a service for managing tag data. + // TagService represents a service for managing tag data TagService interface { Tags() ([]Tag, error) CreateTag(tag *Tag) error DeleteTag(ID TagID) error } - // TemplateService represents a service for managing template data. + // TemplateService represents a service for managing template data TemplateService interface { Templates() ([]Template, error) Template(ID TemplateID) (*Template, error) @@ -529,13 +557,13 @@ type ( DeleteTemplate(ID TemplateID) error } - // CryptoService represents a service for encrypting/hashing data. + // CryptoService represents a service for encrypting/hashing data CryptoService interface { Hash(data string) (string, error) CompareHashAndData(hash string, data string) error } - // DigitalSignatureService represents a service to manage digital signatures. + // DigitalSignatureService represents a service to manage digital signatures DigitalSignatureService interface { ParseKeyPair(private, public []byte) error GenerateKeyPair() ([]byte, []byte, error) @@ -544,13 +572,13 @@ type ( Sign(message string) (string, error) } - // JWTService represents a service for managing JWT tokens. + // JWTService represents a service for managing JWT tokens JWTService interface { GenerateToken(data *TokenData) (string, error) ParseAndVerifyToken(token string) (*TokenData, error) } - // FileService represents a service for managing files. + // FileService represents a service for managing files FileService interface { GetFileContent(filePath string) ([]byte, error) Rename(oldPath, newPath string) error @@ -568,13 +596,13 @@ type ( FileExists(path string) (bool, error) } - // GitService represents a service for managing Git. + // GitService represents a service for managing Git GitService interface { ClonePublicRepository(repositoryURL, referenceName string, destination string) error ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error } - // JobScheduler represents a service to run jobs on a periodic basis. + // JobScheduler represents a service to run jobs on a periodic basis JobScheduler interface { ScheduleEndpointSyncJob(endpointFilePath, interval string) error ScheduleSnapshotJob(interval string) error @@ -582,19 +610,19 @@ type ( Start() } - // Snapshotter represents a service used to create endpoint snapshots. + // Snapshotter represents a service used to create endpoint snapshots Snapshotter interface { CreateSnapshot(endpoint *Endpoint) (*Snapshot, error) } - // LDAPService represents a service used to authenticate users against a LDAP/AD. + // LDAPService represents a service used to authenticate users against a LDAP/AD LDAPService interface { AuthenticateUser(username, password string, settings *LDAPSettings) error TestConnectivity(settings *LDAPSettings) error GetUserGroups(username string, settings *LDAPSettings) ([]string, error) } - // SwarmStackManager represents a service to manage Swarm stacks. + // SwarmStackManager represents a service to manage Swarm stacks SwarmStackManager interface { Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) Logout(endpoint *Endpoint) error @@ -602,7 +630,7 @@ type ( Remove(stack *Stack, endpoint *Endpoint) error } - // ComposeStackManager represents a service to manage Compose stacks. + // ComposeStackManager represents a service to manage Compose stacks ComposeStackManager interface { Up(stack *Stack, endpoint *Endpoint) error Down(stack *Stack, endpoint *Endpoint) error @@ -610,13 +638,15 @@ type ( ) const ( - // APIVersion is the version number of the Portainer API. - APIVersion = "1.19.1" - // DBVersion is the version number of the Portainer database. - DBVersion = 13 + // APIVersion is the version number of the Portainer API + APIVersion = "1.19.2" + // DBVersion is the version number of the Portainer database + DBVersion = 14 + // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved + MessageOfTheDayURL = "https://raw.githubusercontent.com/portainer/motd/master/message.html" // PortainerAgentHeader represents the name of the header available in any agent response PortainerAgentHeader = "Portainer-Agent" - // PortainerAgentTargetHeader represent the name of the header containing the target node name. + // 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 PortainerAgentSignatureHeader = "X-PortainerAgent-Signature" @@ -625,16 +655,16 @@ const ( // PortainerAgentSignatureMessage represents the message used to create a digital signature // to be used when communicating with an agent PortainerAgentSignatureMessage = "Portainer-App" - // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer. + // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer SupportedDockerAPIVersion = "1.24" ) const ( - // TLSFileCA represents a TLS CA certificate file. + // TLSFileCA represents a TLS CA certificate file TLSFileCA TLSFileType = iota - // TLSFileCert represents a TLS certificate file. + // TLSFileCert represents a TLS certificate file TLSFileCert - // TLSFileKey represents a TLS key file. + // TLSFileKey represents a TLS key file TLSFileKey ) @@ -727,3 +757,9 @@ const ( // EndpointStatusDown is used to represent an unavailable endpoint EndpointStatusDown ) + +const ( + _ WebhookType = iota + // ServiceWebhook is a webhook for restarting a docker service + ServiceWebhook +) diff --git a/api/swagger.yaml b/api/swagger.yaml index 0e4e9426c..890c5fc2a 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.19.1" + version: "1.19.2" title: "Portainer API" contact: email: "info@portainer.io" @@ -1336,7 +1336,7 @@ paths: in: "formData" type: "string" description: "Swarm cluster identifier. Required when method equals file and type equals 1." - - name: "StackFileContent" + - name: "file" in: "formData" type: "file" description: "Stack file. Required when method equals file." @@ -2816,7 +2816,7 @@ definitions: description: "Is analytics enabled" Version: type: "string" - example: "1.19.1" + example: "1.19.2" description: "Portainer API version" PublicSettingsInspectResponse: type: "object" @@ -3268,11 +3268,10 @@ definitions: example: "container" description: "Type of Docker resource. Valid values are: container, volume\ \ service, secret, config or stack" - AdministratorsOnly: + Public: type: "boolean" example: true - description: "Restrict access to the associated resource to administrators\ - \ only" + description: "Permit access to the associated resource to any user" Users: type: "array" description: "List of user identifiers with access to the associated resource" @@ -3491,11 +3490,10 @@ definitions: example: "container" description: "Type of Docker resource. Valid values are: container, volume\ \ service, secret, config or stack" - AdministratorsOnly: + Public: type: "boolean" example: true - description: "Restrict access to the associated resource to administrators\ - \ only" + description: "Permit access to the associated resource to any user" Users: type: "array" description: "List of user identifiers with access to the associated resource" @@ -3520,11 +3518,10 @@ definitions: ResourceControlUpdateRequest: type: "object" properties: - AdministratorsOnly: + Public: type: "boolean" example: false - description: "Restrict access to the associated resource to administrators\ - \ only" + description: "Permit access to the associated resource to any user" Users: type: "array" description: "List of user identifiers with access to the associated resource" diff --git a/app/app.js b/app/app.js index 0ed1166fe..a32633fe2 100644 --- a/app/app.js +++ b/app/app.js @@ -48,7 +48,7 @@ function initAnalytics(Analytics, $rootScope) { Analytics.offline(false); Analytics.registerScriptTags(); Analytics.registerTrackers(); - $rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) { + $rootScope.$on('$stateChangeSuccess', function (event, toState) { Analytics.trackPage(toState.url); Analytics.pageView(); }); diff --git a/app/azure/models/container_group.js b/app/azure/models/container_group.js index 0ba06a690..9a855129f 100644 --- a/app/azure/models/container_group.js +++ b/app/azure/models/container_group.js @@ -15,7 +15,7 @@ function ContainerGroupDefaultModel() { this.Memory = 1; } -function ContainerGroupViewModel(data, subscriptionId, resourceGroupName) { +function ContainerGroupViewModel(data) { this.Id = data.id; this.Name = data.name; this.Location = data.location; diff --git a/app/azure/services/azureService.js b/app/azure/services/azureService.js index 8d7def765..d0fa0468c 100644 --- a/app/azure/services/azureService.js +++ b/app/azure/services/azureService.js @@ -30,7 +30,7 @@ function AzureServiceFactory($q, Azure, SubscriptionService, ResourceGroupServic service.aggregate = function(resourcesBySubcription) { var aggregatedResources = []; - Object.keys(resourcesBySubcription).forEach(function(key, index) { + Object.keys(resourcesBySubcription).forEach(function(key) { aggregatedResources = aggregatedResources.concat(resourcesBySubcription[key]); }); return aggregatedResources; diff --git a/app/azure/views/containerinstances/create/createContainerInstanceController.js b/app/azure/views/containerinstances/create/createContainerInstanceController.js index b3a7ed173..a7ecab9e8 100644 --- a/app/azure/views/containerinstances/create/createContainerInstanceController.js +++ b/app/azure/views/containerinstances/create/createContainerInstanceController.js @@ -31,7 +31,7 @@ function ($q, $scope, $state, AzureService, Notifications) { $scope.state.actionInProgress = true; AzureService.createContainerGroup(model, subscriptionId, resourceGroupName) - .then(function success(data) { + .then(function success() { Notifications.success('Container successfully created', model.Name); $state.go('azure.containerinstances'); }) diff --git a/app/constants.js b/app/constants.js index c4f911034..b20d44649 100644 --- a/app/constants.js +++ b/app/constants.js @@ -3,6 +3,7 @@ angular.module('portainer') .constant('API_ENDPOINT_DOCKERHUB', 'api/dockerhub') .constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints') .constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups') +.constant('API_ENDPOINT_MOTD', 'api/motd') .constant('API_ENDPOINT_REGISTRIES', 'api/registries') .constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls') .constant('API_ENDPOINT_SETTINGS', 'api/settings') @@ -13,6 +14,8 @@ angular.module('portainer') .constant('API_ENDPOINT_TEAMS', 'api/teams') .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('APPLICATION_CACHE_VALIDITY', 3600) +.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.'); diff --git a/app/docker/__module.js b/app/docker/__module.js index 32253b43a..ffd451fbd 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -32,7 +32,7 @@ angular.module('portainer.docker', ['portainer.app']) var configCreation = { name: 'docker.configs.new', - url: '/new', + url: '/new?id', views: { 'content@': { templateUrl: 'app/docker/views/configs/create/createconfig.html', diff --git a/app/docker/components/container-capabilities/container-capabilities.js b/app/docker/components/container-capabilities/container-capabilities.js new file mode 100644 index 000000000..6d572a464 --- /dev/null +++ b/app/docker/components/container-capabilities/container-capabilities.js @@ -0,0 +1,6 @@ +angular.module('portainer.docker').component('containerCapabilities', { + templateUrl: 'app/docker/components/container-capabilities/containerCapabilities.html', + bindings: { + capabilities: '=' + } +}); diff --git a/app/docker/components/container-capabilities/containerCapabilities.html b/app/docker/components/container-capabilities/containerCapabilities.html new file mode 100644 index 000000000..5d1f8401e --- /dev/null +++ b/app/docker/components/container-capabilities/containerCapabilities.html @@ -0,0 +1,22 @@ +
+
+ Container capabilities +
+
+
+
+ +
+
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/app/docker/components/container-restart-policy/container-restart-policy-controller.js b/app/docker/components/container-restart-policy/container-restart-policy-controller.js new file mode 100644 index 000000000..64a8604fc --- /dev/null +++ b/app/docker/components/container-restart-policy/container-restart-policy-controller.js @@ -0,0 +1,26 @@ +angular +.module('portainer.docker') +.controller('ContainerRestartPolicyController', [function ContainerRestartPolicyController() { + var ctrl = this; + + this.state = { + editModel : {} + }; + + ctrl.save = save; + + function save() { + if (ctrl.state.editModel.name === ctrl.name && ctrl.state.editModel.maximumRetryCount === ctrl.maximumRetryCount) { + return; + } + ctrl.updateRestartPolicy(ctrl.state.editModel); + } + + this.$onInit = function() { + ctrl.state.editModel = { + name: ctrl.name ? ctrl.name : 'no', + maximumRetryCount: ctrl.maximumRetryCount + }; + }; +} +]); diff --git a/app/docker/components/container-restart-policy/container-restart-policy.html b/app/docker/components/container-restart-policy/container-restart-policy.html new file mode 100644 index 000000000..41b87c2fc --- /dev/null +++ b/app/docker/components/container-restart-policy/container-restart-policy.html @@ -0,0 +1,26 @@ +
+ + + + + + + + + + +
+ Name + + + + +
Maximum Retry Count + +
+
diff --git a/app/docker/components/container-restart-policy/container-restart-policy.js b/app/docker/components/container-restart-policy/container-restart-policy.js new file mode 100644 index 000000000..0a8d7edf2 --- /dev/null +++ b/app/docker/components/container-restart-policy/container-restart-policy.js @@ -0,0 +1,10 @@ +angular.module('portainer.docker') +.component('containerRestartPolicy', { + templateUrl: 'app/docker/components/container-restart-policy/container-restart-policy.html', + controller: 'ContainerRestartPolicyController', + bindings: { + 'name': '<', + 'maximumRetryCount': '<', + 'updateRestartPolicy': '&' + } +}); diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.html b/app/docker/components/datatables/configs-datatable/configsDatatable.html index 7c9e5078f..9b2f4364f 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.html +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.html @@ -11,7 +11,7 @@ ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"> Remove - @@ -63,7 +63,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index 2119d8f35..19cd2a0c0 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -244,7 +244,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} diff --git a/app/docker/components/datatables/containers-datatable/containersDatatableController.js b/app/docker/components/datatables/containers-datatable/containersDatatableController.js index f7125d7b8..7151e5d64 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatableController.js +++ b/app/docker/components/datatables/containers-datatable/containersDatatableController.js @@ -141,7 +141,7 @@ function (PaginationService, DatatableService, EndpointProvider) { PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); }; - this.applyFilters = function(value, index, array) { + this.applyFilters = function(value) { var container = value; var filters = ctrl.filters; for (var i = 0; i < filters.state.values.length; i++) { diff --git a/app/docker/components/datatables/images-datatable/imagesDatatableController.js b/app/docker/components/datatables/images-datatable/imagesDatatableController.js index c7850a63b..f5254a47b 100644 --- a/app/docker/components/datatables/images-datatable/imagesDatatableController.js +++ b/app/docker/components/datatables/images-datatable/imagesDatatableController.js @@ -52,7 +52,7 @@ function (PaginationService, DatatableService) { PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); }; - this.applyFilters = function(value, index, array) { + this.applyFilters = function(value) { var image = value; var filters = ctrl.filters; if ((image.ContainerCount === 0 && filters.usage.showUnusedImages) diff --git a/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html b/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html new file mode 100644 index 000000000..5b8b3660e --- /dev/null +++ b/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html @@ -0,0 +1,106 @@ +
+ + +
+
+ {{ $ctrl.titleText }} +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Role + + + + + + Engine + + + + + + IP Address + + + + + + Status + + + +
+ + + + + {{ item.Hostname }} + {{ item.Hostname }} + {{ item.Role }}{{ item.EngineVersion }}{{ item.Addr }} + {{ item.Status }} +
Loading...
No node available.
+
+ +
+
+
\ No newline at end of file diff --git a/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.js b/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.js new file mode 100644 index 000000000..d18e47ab7 --- /dev/null +++ b/app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.js @@ -0,0 +1,15 @@ +angular.module('portainer.docker').component('macvlanNodesDatatable', { + templateUrl: 'app/docker/components/datatables/macvlan-nodes-datatable/macvlanNodesDatatable.html', + controller: 'GenericDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + showIpAddressColumn: '<', + accessToNodeDetails: '<', + state: '=' + } +}); diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.html b/app/docker/components/datatables/networks-datatable/networksDatatable.html index 98fb15791..afb808ce8 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.html +++ b/app/docker/components/datatables/networks-datatable/networksDatatable.html @@ -55,6 +55,20 @@ + + + Attachable + + + + + + + Internal + + + + IPAM Driver @@ -104,6 +118,8 @@ {{ item.StackName ? item.StackName : '-' }} {{ item.Scope }} {{ item.Driver }} + {{ item.Attachable }} + {{ item.Internal }} {{ item.IPAM.Driver }} {{ item.IPAM.Config[0].Subnet ? item.IPAM.Config[0].Subnet : '-' }} {{ item.IPAM.Config[0].Gateway ? item.IPAM.Config[0].Gateway : '-' }} @@ -111,7 +127,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} diff --git a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html index 4e6a6c148..ed5249557 100644 --- a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html +++ b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html @@ -63,7 +63,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html index 433be02d8..cfba0524f 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html @@ -65,7 +65,7 @@
- +
diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js index 33eecffc4..b0a8da0b2 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js @@ -15,7 +15,7 @@ function (DatatableService) { } }; - this.applyFilters = function(item, index, array) { + this.applyFilters = function(item) { var filters = ctrl.filters; for (var i = 0; i < filters.state.values.length; i++) { var filter = filters.state.values[i]; diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js index e2a394e58..2f5ce7ce4 100644 --- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js +++ b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActionsController.js @@ -1,12 +1,12 @@ angular.module('portainer.docker') -.controller('ServicesDatatableActionsController', ['$state', 'ServiceService', 'ServiceHelper', 'Notifications', 'ModalService', 'ImageHelper', -function ($state, ServiceService, ServiceHelper, Notifications, ModalService, ImageHelper) { +.controller('ServicesDatatableActionsController', ['$q', '$state', 'ServiceService', 'ServiceHelper', 'Notifications', 'ModalService', 'ImageHelper','WebhookService','EndpointProvider', +function ($q, $state, ServiceService, ServiceHelper, Notifications, ModalService, ImageHelper, WebhookService, EndpointProvider) { this.scaleAction = function scaleService(service) { var config = ServiceHelper.serviceToConfig(service.Model); config.Mode.Replicated.Replicas = service.Replicas; ServiceService.update(service, config) - .then(function success(data) { + .then(function success() { Notifications.success('Service successfully scaled', 'New replica count: ' + service.Replicas); $state.reload(); }) @@ -53,7 +53,7 @@ function ($state, ServiceService, ServiceHelper, Notifications, ModalService, Im // value or an increment of the counter value to force an update. config.TaskTemplate.ForceUpdate++; ServiceService.update(service, config) - .then(function success(data) { + .then(function success() { Notifications.success('Service successfully updated', service.Name); }) .catch(function error(err) { @@ -71,7 +71,14 @@ function ($state, ServiceService, ServiceHelper, Notifications, ModalService, Im function removeServices(services) { var actionCount = services.length; angular.forEach(services, function (service) { + ServiceService.remove(service) + .then(function success() { + return WebhookService.webhooks(service.Id, EndpointProvider.endpointID()); + }) + .then(function success(data) { + return $q.when(data.length !== 0 && WebhookService.deleteWebhook(data[0].Id)); + }) .then(function success() { Notifications.success('Service successfully removed', service.Name); }) diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html index 8d25b3bf1..ba3d870ad 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.html +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html @@ -96,7 +96,7 @@ {{ item.Image | hideshasum }} {{ item.Mode }} - {{ item.Tasks | runningtaskscount }} / {{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount) }} + {{ item.Tasks | runningtaskscount }} / {{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount:item) }} Scale @@ -118,7 +118,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatable.html b/app/docker/components/datatables/tasks-datatable/tasksDatatable.html index bbb0e7d18..db3d50ebb 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatable.html +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatable.html @@ -8,7 +8,7 @@
diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html index c93f506f4..60632f296 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html @@ -115,7 +115,7 @@ diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js b/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js index f146f107e..38b3ebbff 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatableController.js @@ -52,7 +52,7 @@ function (PaginationService, DatatableService) { PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); }; - this.applyFilters = function(value, index, array) { + this.applyFilters = function(value) { var volume = value; var filters = ctrl.filters; if ((volume.dangling && filters.usage.showUnusedVolumes) diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js index f85c20852..9de38f318 100644 --- a/app/docker/components/log-viewer/logViewerController.js +++ b/app/docker/components/log-viewer/logViewerController.js @@ -1,7 +1,6 @@ angular.module('portainer.docker') .controller('LogViewerController', ['clipboard', function (clipboard) { - var ctrl = this; this.state = { copySupported: clipboard.supported, diff --git a/app/docker/components/network-macvlan-form/network-macvlan-form.js b/app/docker/components/network-macvlan-form/network-macvlan-form.js new file mode 100644 index 000000000..f9981f1cd --- /dev/null +++ b/app/docker/components/network-macvlan-form/network-macvlan-form.js @@ -0,0 +1,8 @@ +angular.module('portainer.docker').component('networkMacvlanForm', { + templateUrl: 'app/docker/components/network-macvlan-form/networkMacvlanForm.html', + controller: 'NetworkMacvlanFormController', + bindings: { + data: '=', + applicationState: '<' + } +}); \ No newline at end of file diff --git a/app/docker/components/network-macvlan-form/networkMacvlanForm.html b/app/docker/components/network-macvlan-form/networkMacvlanForm.html new file mode 100644 index 000000000..b104bac9d --- /dev/null +++ b/app/docker/components/network-macvlan-form/networkMacvlanForm.html @@ -0,0 +1,108 @@ +
+
+ Macvlan configuration +
+ +
+ + + To create a MACVLAN network you need to create a configuration, then create the network from this configuration. + +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + + + +
+ +
+ +
+ +
+
+
+
+
+

+ Parent network card must be specified.

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

+ At least one node must be selected.

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

+ Select a configuration network.

+
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/app/docker/components/network-macvlan-form/networkMacvlanFormController.js b/app/docker/components/network-macvlan-form/networkMacvlanFormController.js new file mode 100644 index 000000000..5cf4231fb --- /dev/null +++ b/app/docker/components/network-macvlan-form/networkMacvlanFormController.js @@ -0,0 +1,51 @@ +angular.module('portainer.docker') + .controller('NetworkMacvlanFormController', ['$q', 'NodeService', 'NetworkService', 'Notifications', 'StateManager', 'Authentication', + function ($q, NodeService, NetworkService, Notifications, StateManager, Authentication) { + var ctrl = this; + + ctrl.requiredNodeSelection = function () { + if (ctrl.data.Scope !== 'local' || ctrl.data.DatatableState === undefined) { + return false; + } + return ctrl.data.DatatableState.selectedItemCount === 0; + }; + + ctrl.requiredConfigSelection = function () { + if (ctrl.data.Scope !== 'swarm') { + return false; + } + return !ctrl.data.SelectedNetworkConfig; + }; + + function initComponent() { + if (StateManager.getState().application.authentication) { + var userDetails = Authentication.getUserDetails(); + var isAdmin = userDetails.role === 1 ? true : false; + ctrl.isAdmin = isAdmin; + } + var provider = ctrl.applicationState.endpoint.mode.provider; + var apiVersion = ctrl.applicationState.endpoint.apiVersion; + $q.all({ + nodes: provider !== 'DOCKER_SWARM_MODE' || NodeService.nodes(), + networks: NetworkService.networks( + provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE', + false, + provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25 + ) + }) + .then(function success(data) { + if (data.nodes !== true) { + ctrl.nodes = data.nodes; + } + ctrl.availableNetworks = data.networks.filter(function (item) { + return item.ConfigOnly === true; + }); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve informations for macvlan'); + }); + } + + initComponent(); + } + ]); \ No newline at end of file diff --git a/app/docker/components/network-macvlan-form/networkMacvlanFormModel.js b/app/docker/components/network-macvlan-form/networkMacvlanFormModel.js new file mode 100644 index 000000000..10ed60510 --- /dev/null +++ b/app/docker/components/network-macvlan-form/networkMacvlanFormModel.js @@ -0,0 +1,8 @@ +function MacvlanFormData() { + this.Scope = 'local'; + this.SelectedNetworkConfig = ''; + this.DatatableState = { + selectedItems: [] + }; + this.ParentNetworkCard = ''; +} \ No newline at end of file diff --git a/app/docker/components/volumesNFSForm/volumes-nfs-form.js b/app/docker/components/volumesNFSForm/volumes-nfs-form.js new file mode 100644 index 000000000..423c3bed0 --- /dev/null +++ b/app/docker/components/volumesNFSForm/volumes-nfs-form.js @@ -0,0 +1,6 @@ +angular.module('portainer.docker').component('volumesNfsForm', { + templateUrl: 'app/docker/components/volumesNFSForm/volumesnfsForm.html', + bindings: { + data: '=' + } +}); diff --git a/app/docker/components/volumesNFSForm/volumesNFSFormModel.js b/app/docker/components/volumesNFSForm/volumesNFSFormModel.js new file mode 100644 index 000000000..a1204dccb --- /dev/null +++ b/app/docker/components/volumesNFSForm/volumesNFSFormModel.js @@ -0,0 +1,8 @@ +function VolumesNFSFormData() { + this.useNFS = false; + this.serverAddress = ''; + this.mountPoint = ''; + this.version = 'NFS4'; + this.options = 'rw,noatime,rsize=8192,wsize=8192,tcp,timeo=14'; + this.versions = ['NFS4', 'NFS']; +} \ No newline at end of file diff --git a/app/docker/components/volumesNFSForm/volumesnfsForm.html b/app/docker/components/volumesNFSForm/volumesnfsForm.html new file mode 100644 index 000000000..bd0d1014c --- /dev/null +++ b/app/docker/components/volumesNFSForm/volumesnfsForm.html @@ -0,0 +1,90 @@ +
+
+ + +
+ +
+ +
+ NFS Settings +
+ +
+ +
+ +
+
+
+
+
+

+ This field is required.

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

+ This field is required.

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

+ This field is required.

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

+ This field is required.

+
+
+
+ +
+
+ +
\ No newline at end of file diff --git a/app/docker/filters/filters.js b/app/docker/filters/filters.js index b6f9a71dd..a07f1be0d 100644 --- a/app/docker/filters/filters.js +++ b/app/docker/filters/filters.js @@ -192,43 +192,46 @@ angular.module('portainer.docker') return ''; }; }) -.filter('availablenodecount', function () { +.filter('availablenodecount', ['ConstraintsHelper', function (ConstraintsHelper) { 'use strict'; - return function (nodes) { + return function (nodes, service) { var availableNodes = 0; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; - if (node.Availability === 'active' && node.Status === 'ready') { + if (node.Availability === 'active' && node.Status === 'ready' && ConstraintsHelper.matchesServiceConstraints(service, node)) { availableNodes++; } } return availableNodes; }; -}) +}]) .filter('runningtaskscount', function () { 'use strict'; return function (tasks) { var runningTasks = 0; for (var i = 0; i < tasks.length; i++) { var task = tasks[i]; - if (task.Status.State === 'running') { + if (task.Status.State === 'running' && task.DesiredState === 'running') { runningTasks++; } } return runningTasks; }; }) -.filter('containerswithstatus', function () { +.filter('runningcontainers', function () { 'use strict'; - return function (containers, status) { - var containersWithStatus = 0; - for (var i = 0; i < containers.length; i++) { - var container = containers[i]; - if (container.Status === status) { - containersWithStatus++; - } - } - return containersWithStatus; + return function runningContainersFilter(containers) { + return containers.filter(function (container) { + return container.State === 'running'; + }).length; + }; +}) +.filter('stoppedcontainers', function () { + 'use strict'; + return function stoppedContainersFilter(containers) { + return containers.filter(function (container) { + return container.State === 'exited'; + }).length; }; }) .filter('imagestotalsize', function () { diff --git a/app/docker/helpers/constraintsHelper.js b/app/docker/helpers/constraintsHelper.js new file mode 100644 index 000000000..b2700deba --- /dev/null +++ b/app/docker/helpers/constraintsHelper.js @@ -0,0 +1,106 @@ +function ConstraintModel(op, key, value) { + this.op = op; + this.value = value; + this.key = key; +} + +var patterns = { + id: { + nodeId: 'node.id', + nodeHostname: 'node.hostname', + nodeRole: 'node.role', + nodeLabels: 'node.labels.', + engineLabels: 'engine.labels.' + }, + op: { + eq: '==', + neq: '!=' + } +}; + +function matchesConstraint(value, constraint) { + if (!constraint || + (constraint.op === patterns.op.eq && value === constraint.value) || + (constraint.op === patterns.op.neq && value !== constraint.value)) { + return true; + } + return false; +} + +function matchesLabel(labels, constraint) { + if (!constraint) { + return true; + } + var found = _.find(labels, function (label) { + return label.key === constraint.key && label.value === constraint.value; + }); + return found !== undefined; +} + +function extractValue(constraint, op) { + return constraint.split(op).pop().trim(); +} + +function extractCustomLabelKey(constraint, op, baseLabelKey) { + return constraint.split(op).shift().trim().replace(baseLabelKey, ''); +} + +angular.module('portainer.docker') + .factory('ConstraintsHelper', [function ConstraintsHelperFactory() { + 'use strict'; + return { + transformConstraints: function (constraints) { + var transform = {}; + for (var i = 0; i < constraints.length; i++) { + var constraint = constraints[i]; + + var op; + if (constraint.includes(patterns.op.eq)) { + op = patterns.op.eq; + } else if (constraint.includes(patterns.op.neq)) { + op = patterns.op.neq; + } + + var value = extractValue(constraint, op); + var key = ''; + switch (true) { + case constraint.includes(patterns.id.nodeId): + transform.nodeId = new ConstraintModel(op, key, value); + break; + case constraint.includes(patterns.id.nodeHostname): + transform.nodeHostname = new ConstraintModel(op, key, value); + break; + case constraint.includes(patterns.id.nodeRole): + transform.nodeRole = new ConstraintModel(op, key, value); + break; + case constraint.includes(patterns.id.nodeLabels): + key = extractCustomLabelKey(constraint, op, patterns.id.nodeLabels); + transform.nodeLabels = new ConstraintModel(op, key, value); + break; + case constraint.includes(patterns.id.engineLabels): + key = extractCustomLabelKey(constraint, op, patterns.id.engineLabels); + transform.engineLabels = new ConstraintModel(op, key, value); + break; + default: + break; + } + } + return transform; + }, + matchesServiceConstraints: function (service, node) { + if (service.Constraints === undefined || service.Constraints.length === 0) { + return true; + } + var constraints = this.transformConstraints(angular.copy(service.Constraints)); + if (matchesConstraint(node.Id, constraints.nodeId) && + matchesConstraint(node.Hostname, constraints.nodeHostname) && + matchesConstraint(node.Role, constraints.nodeRole) && + matchesLabel(node.Labels, constraints.nodeLabels) && + matchesLabel(node.EngineLabels, constraints.engineLabels) + ) { + return true; + } + return false; + } + }; + }]); \ No newline at end of file diff --git a/app/docker/helpers/containerHelper.js b/app/docker/helpers/containerHelper.js index c30af02c6..1e5d45ff6 100644 --- a/app/docker/helpers/containerHelper.js +++ b/app/docker/helpers/containerHelper.js @@ -35,12 +35,6 @@ angular.module('portainer.docker') for (var v in container.Mounts) { if ({}.hasOwnProperty.call(container.Mounts, v)) { var mount = container.Mounts[v]; - var volume = { - 'type': mount.Type, - 'name': mount.Name || mount.Source, - 'containerPath': mount.Destination, - 'readOnly': mount.RW === false - }; var name = mount.Name || mount.Source; var containerPath = mount.Destination; if (name && containerPath) { diff --git a/app/docker/models/containerCapabilities.js b/app/docker/models/containerCapabilities.js new file mode 100644 index 000000000..0ff22239e --- /dev/null +++ b/app/docker/models/containerCapabilities.js @@ -0,0 +1,90 @@ +var capDesc = { + 'SETPCAP': 'Modify process capabilities.', + 'MKNOD': 'Create special files using mknod(2).', + 'AUDIT_WRITE': 'Write records to kernel auditing log.', + 'CHOWN': 'Make arbitrary changes to file UIDs and GIDs (see chown(2)).', + 'NET_RAW': 'Use RAW and PACKET sockets.', + 'DAC_OVERRIDE': 'Bypass file read, write, and execute permission checks.', + 'FOWNER': 'Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.', + 'FSETID': 'Don’t clear set-user-ID and set-group-ID permission bits when a file is modified.', + 'KILL': 'Bypass permission checks for sending signals.', + 'SETGID': 'Make arbitrary manipulations of process GIDs and supplementary GID list.', + 'SETUID': 'Make arbitrary manipulations of process UIDs.', + 'NET_BIND_SERVICE': 'Bind a socket to internet domain privileged ports (port numbers less than 1024).', + 'SYS_CHROOT': 'Use chroot(2), change root directory.', + 'SETFCAP': 'Set file capabilities.', + 'SYS_MODULE': 'Load and unload kernel modules.', + 'SYS_RAWIO': 'Perform I/O port operations (iopl(2) and ioperm(2)).', + 'SYS_PACCT': 'Use acct(2), switch process accounting on or off.', + 'SYS_ADMIN': 'Perform a range of system administration operations.', + 'SYS_NICE': 'Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes.', + 'SYS_RESOURCE': 'Override resource Limits.', + 'SYS_TIME': 'Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock.', + 'SYS_TTY_CONFIG': 'Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals.', + 'AUDIT_CONTROL': 'Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules.', + 'MAC_ADMIN': 'Allow MAC configuration or state changes. Implemented for the Smack LSM.', + 'MAC_OVERRIDE': 'Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM).', + 'NET_ADMIN': 'Perform various network-related operations.', + 'SYSLOG': 'Perform privileged syslog(2) operations.', + 'DAC_READ_SEARCH': 'Bypass file read permission checks and directory read and execute permission checks.', + 'LINUX_IMMUTABLE': 'Set the FS_APPEND_FL and FS_IMMUTABLE_FL i-node flags.', + 'NET_BROADCAST': 'Make socket broadcasts, and listen to multicasts.', + 'IPC_LOCK': 'Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2)).', + 'IPC_OWNER': 'Bypass permission checks for operations on System V IPC objects.', + 'SYS_PTRACE': 'Trace arbitrary processes using ptrace(2).', + 'SYS_BOOT': 'Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution.', + 'LEASE': 'Establish leases on arbitrary files (see fcntl(2)).', + 'WAKE_ALARM': 'Trigger something that will wake up the system.', + 'BLOCK_SUSPEND': 'Employ features that can block system suspend.' +}; + +function ContainerCapabilities() { + // all capabilities can be found at https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities + return [ + new ContainerCapability('SETPCAP', true), + new ContainerCapability('MKNOD', true), + new ContainerCapability('AUDIT_WRITE', true), + new ContainerCapability('CHOWN', true), + new ContainerCapability('NET_RAW', true), + new ContainerCapability('DAC_OVERRIDE', true), + new ContainerCapability('FOWNER', true), + new ContainerCapability('FSETID', true), + new ContainerCapability('KILL', true), + new ContainerCapability('SETGID', true), + new ContainerCapability('SETUID', true), + new ContainerCapability('NET_BIND_SERVICE', true), + new ContainerCapability('SYS_CHROOT', true), + new ContainerCapability('SETFCAP', true), + new ContainerCapability('SYS_MODULE', false), + new ContainerCapability('SYS_RAWIO', false), + new ContainerCapability('SYS_PACCT', false), + new ContainerCapability('SYS_ADMIN', false), + new ContainerCapability('SYS_NICE', false), + new ContainerCapability('SYS_RESOURCE', false), + new ContainerCapability('SYS_TIME', false), + new ContainerCapability('SYS_TTY_CONFIG', false), + new ContainerCapability('AUDIT_CONTROL', false), + new ContainerCapability('MAC_ADMIN', false), + new ContainerCapability('MAC_OVERRIDE', false), + new ContainerCapability('NET_ADMIN', false), + new ContainerCapability('SYSLOG', false), + new ContainerCapability('DAC_READ_SEARCH', false), + new ContainerCapability('LINUX_IMMUTABLE', false), + new ContainerCapability('NET_BROADCAST', false), + new ContainerCapability('IPC_LOCK', false), + new ContainerCapability('IPC_OWNER', false), + new ContainerCapability('SYS_PTRACE', false), + new ContainerCapability('SYS_BOOT', false), + new ContainerCapability('LEASE', false), + new ContainerCapability('WAKE_ALARM', false), + new ContainerCapability('BLOCK_SUSPEND', false) + ].sort(function (a, b) { + return a.capability < b.capability ? -1 : 1; + }); +} + +function ContainerCapability(cap, allowed) { + this.capability = cap; + this.allowed = allowed; + this.description = capDesc[cap]; +} \ No newline at end of file diff --git a/app/docker/models/network.js b/app/docker/models/network.js index 402d4cd96..da60a10af 100644 --- a/app/docker/models/network.js +++ b/app/docker/models/network.js @@ -4,6 +4,7 @@ function NetworkViewModel(data) { this.Scope = data.Scope; this.Driver = data.Driver; this.Attachable = data.Attachable; + this.Internal = data.Internal; this.IPAM = data.IPAM; this.Containers = data.Containers; this.Options = data.Options; @@ -23,4 +24,7 @@ function NetworkViewModel(data) { this.NodeName = data.Portainer.Agent.NodeName; } } -} + + this.ConfigFrom = data.ConfigFrom; + this.ConfigOnly = data.ConfigOnly; +} \ No newline at end of file diff --git a/app/docker/models/service.js b/app/docker/models/service.js index 07a769156..a49392f2c 100644 --- a/app/docker/models/service.js +++ b/app/docker/models/service.js @@ -1,4 +1,4 @@ -function ServiceViewModel(data, runningTasks, allTasks, nodes) { +function ServiceViewModel(data, runningTasks, allTasks) { this.Model = data; this.Id = data.ID; this.Tasks = []; diff --git a/app/docker/models/task.js b/app/docker/models/task.js index e28390483..5714c1509 100644 --- a/app/docker/models/task.js +++ b/app/docker/models/task.js @@ -5,6 +5,7 @@ function TaskViewModel(data) { this.Slot = data.Slot; this.Spec = data.Spec; this.Status = data.Status; + this.DesiredState = data.DesiredState; this.ServiceId = data.ServiceID; this.NodeId = data.NodeID; if (data.Status && data.Status.ContainerStatus && data.Status.ContainerStatus.ContainerID) { diff --git a/app/docker/rest/container.js b/app/docker/rest/container.js index d100d5a1c..d46d9e474 100644 --- a/app/docker/rest/container.js +++ b/app/docker/rest/container.js @@ -65,6 +65,9 @@ function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { }, inspect: { method: 'GET', params: { id: '@id', action: 'json' } + }, + update: { + method: 'POST', params: { id: '@id', action: 'update'} } }); }]); diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index a0783d2d9..ce81cd8c5 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -63,6 +63,14 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe return Container.rename({id: id, name: newContainerName }, {}).$promise; }; + service.updateRestartPolicy = updateRestartPolicy; + + function updateRestartPolicy(id, restartPolicy, maximumRetryCounts) { + return Container.update({ id: id }, + { RestartPolicy: { Name: restartPolicy, MaximumRetryCount: maximumRetryCounts } } + ).$promise; + } + service.createContainer = function(configuration) { var deferred = $q.defer(); Container.create(configuration).$promise diff --git a/app/docker/services/volumeService.js b/app/docker/services/volumeService.js index d52b60528..449ec661c 100644 --- a/app/docker/services/volumeService.js +++ b/app/docker/services/volumeService.js @@ -1,5 +1,5 @@ angular.module('portainer.docker') -.factory('VolumeService', ['$q', 'Volume', 'VolumeHelper', 'ResourceControlService', 'UserService', 'TeamService', function VolumeServiceFactory($q, Volume, VolumeHelper, ResourceControlService, UserService, TeamService) { +.factory('VolumeService', ['$q', 'Volume', 'VolumeHelper', 'ResourceControlService', function VolumeServiceFactory($q, Volume, VolumeHelper, ResourceControlService) { 'use strict'; var service = {}; diff --git a/app/docker/views/configs/create/createConfigController.js b/app/docker/views/configs/create/createConfigController.js index 07868dd3c..c2f70685c 100644 --- a/app/docker/views/configs/create/createConfigController.js +++ b/app/docker/views/configs/create/createConfigController.js @@ -1,7 +1,6 @@ angular.module('portainer.docker') -.controller('CreateConfigController', ['$scope', '$state', 'Notifications', 'ConfigService', 'Authentication', 'FormValidator', 'ResourceControlService', -function ($scope, $state, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) { - +.controller('CreateConfigController', ['$scope', '$state', '$transition$', 'Notifications', 'ConfigService', 'Authentication', 'FormValidator', 'ResourceControlService', +function ($scope, $state, $transition$, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) { $scope.formValues = { Name: '', Labels: [], @@ -90,4 +89,30 @@ function ($scope, $state, Notifications, ConfigService, Authentication, FormVali $scope.editorUpdate = function(cm) { $scope.formValues.ConfigContent = cm.getValue(); }; + + function initView() { + if (!$transition$.params().id) { + $scope.formValues.displayCodeEditor = true; + return; + } + + ConfigService.config($transition$.params().id) + .then(function success(data) { + $scope.formValues.Name = data.Name + '_copy'; + $scope.formValues.Data = data.Data; + var labels = _.keys(data.Labels); + for (var i = 0; i < labels.length; i++) { + var labelName = labels[i]; + var labelValue = data.Labels[labelName]; + $scope.formValues.Labels.push({ name: labelName, value: labelValue}); + } + $scope.formValues.displayCodeEditor = true; + }) + .catch(function error(err) { + $scope.formValues.displayCodeEditor = true; + Notifications.error('Failure', err, 'Unable to clone config'); + }); + } + + initView(); }]); diff --git a/app/docker/views/configs/create/createconfig.html b/app/docker/views/configs/create/createconfig.html index 18cfc51ac..158cc4c2f 100644 --- a/app/docker/views/configs/create/createconfig.html +++ b/app/docker/views/configs/create/createconfig.html @@ -20,12 +20,13 @@
-
+
diff --git a/app/docker/views/configs/edit/config.html b/app/docker/views/configs/edit/config.html index 376bd2014..5fb4dda01 100644 --- a/app/docker/views/configs/edit/config.html +++ b/app/docker/views/configs/edit/config.html @@ -25,6 +25,7 @@
diff --git a/app/docker/views/configs/edit/configController.js b/app/docker/views/configs/edit/configController.js index 3e0ad0178..2457c9609 100644 --- a/app/docker/views/configs/edit/configController.js +++ b/app/docker/views/configs/edit/configController.js @@ -4,7 +4,7 @@ function ($scope, $transition$, $state, ConfigService, Notifications) { $scope.removeConfig = function removeConfig(configId) { ConfigService.remove(configId) - .then(function success(data) { + .then(function success() { Notifications.success('Config successfully removed'); $state.go('docker.configs', {}); }) diff --git a/app/docker/views/containers/console/containerConsoleController.js b/app/docker/views/containers/console/containerConsoleController.js index f877c5836..812e5b567 100644 --- a/app/docker/views/containers/console/containerConsoleController.js +++ b/app/docker/views/containers/console/containerConsoleController.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') -.controller('ContainerConsoleController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ExecService', 'HttpRequestHelper', 'LocalStorage', -function ($scope, $transition$, ContainerService, ImageService, EndpointProvider, Notifications, ContainerHelper, ExecService, HttpRequestHelper, LocalStorage) { +.controller('ContainerConsoleController', ['$scope', '$transition$', 'ContainerService', 'ImageService', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ExecService', 'HttpRequestHelper', 'LocalStorage', 'CONSOLE_COMMANDS_LABEL_PREFIX', +function ($scope, $transition$, ContainerService, ImageService, EndpointProvider, Notifications, ContainerHelper, ExecService, HttpRequestHelper, LocalStorage, CONSOLE_COMMANDS_LABEL_PREFIX) { var socket, term; $scope.state = { @@ -9,9 +9,10 @@ function ($scope, $transition$, ContainerService, ImageService, EndpointProvider }; $scope.formValues = {}; + $scope.containerCommands = []; // Ensure the socket is closed before leaving the view - $scope.$on('$stateChangeStart', function (event, next, current) { + $scope.$on('$stateChangeStart', function () { if (socket && socket !== null) { socket.close(); } @@ -68,7 +69,7 @@ function ($scope, $transition$, ContainerService, ImageService, EndpointProvider socket = new WebSocket(url); $scope.state.connected = true; - socket.onopen = function(evt) { + socket.onopen = function() { term = new Terminal(); term.on('data', function (data) { @@ -87,10 +88,10 @@ function ($scope, $transition$, ContainerService, ImageService, EndpointProvider socket.onmessage = function (e) { term.write(e.data); }; - socket.onerror = function (error) { + socket.onerror = function () { $scope.state.connected = false; }; - socket.onclose = function(evt) { + socket.onclose = function() { $scope.state.connected = false; }; }; @@ -106,8 +107,16 @@ function ($scope, $transition$, ContainerService, ImageService, EndpointProvider }) .then(function success(data) { var image = data; + var containerLabels = $scope.container.Config.Labels; $scope.imageOS = image.Os; $scope.formValues.command = image.Os === 'windows' ? 'powershell' : 'bash'; + $scope.containerCommands = Object.keys(containerLabels) + .filter(function(label) { + return label.indexOf(CONSOLE_COMMANDS_LABEL_PREFIX) === 0; + }) + .map(function(label) { + return {title: label.replace(CONSOLE_COMMANDS_LABEL_PREFIX, ''), command: containerLabels[label]}; + }); $scope.state.loaded = true; }) .catch(function error(err) { diff --git a/app/docker/views/containers/console/containerconsole.html b/app/docker/views/containers/console/containerconsole.html index f8b4e7e54..e938c49c0 100644 --- a/app/docker/views/containers/console/containerconsole.html +++ b/app/docker/views/containers/console/containerconsole.html @@ -27,6 +27,7 @@ + diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index d45b989be..83b153e44 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -2,6 +2,8 @@ angular.module('portainer.docker') .controller('CreateContainerController', ['$q', '$scope', '$state', '$timeout', '$transition$', '$filter', 'Container', 'ContainerHelper', 'Image', 'ImageHelper', 'Volume', 'NetworkService', 'ResourceControlService', 'Authentication', 'Notifications', 'ContainerService', 'ImageService', 'FormValidator', 'ModalService', 'RegistryService', 'SystemService', 'SettingsService', 'HttpRequestHelper', function ($q, $scope, $state, $timeout, $transition$, $filter, Container, ContainerHelper, Image, ImageHelper, Volume, NetworkService, ResourceControlService, Authentication, Notifications, ContainerService, ImageService, FormValidator, ModalService, RegistryService, SystemService, SettingsService, HttpRequestHelper) { + $scope.create = create; + $scope.formValues = { alwaysPull: true, Console: 'none', @@ -16,7 +18,8 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai CpuLimit: 0, MemoryLimit: 0, MemoryReservation: 0, - NodeName: null + NodeName: null, + capabilities: [] }; $scope.extraNetworks = {}; @@ -47,8 +50,11 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai Binds: [], NetworkMode: 'bridge', Privileged: false, + Runtime: '', ExtraHosts: [], - Devices:[] + Devices: [], + CapAdd: [], + CapDrop: [] }, NetworkingConfig: { EndpointsConfig: {} @@ -251,6 +257,15 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai } } + function prepareCapabilities(config) { + var allowed = $scope.formValues.capabilities.filter(function(item) {return item.allowed === true;}); + var notAllowed = $scope.formValues.capabilities.filter(function(item) {return item.allowed === false;}); + + var getCapName = function(item) {return item.capability;}; + config.HostConfig.CapAdd = allowed.map(getCapName); + config.HostConfig.CapDrop = notAllowed.map(getCapName); + } + function prepareConfiguration() { var config = angular.copy($scope.config); config.Cmd = ContainerHelper.commandStringToArray(config.Cmd); @@ -263,51 +278,12 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai prepareLabels(config); prepareDevices(config); prepareResources(config); + prepareCapabilities(config); return config; } - function confirmCreateContainer() { - var deferred = $q.defer(); - Container.query({ all: 1, filters: {name: ['^/' + $scope.config.name + '$'] }}).$promise - .then(function success(data) { - var existingContainer = data[0]; - if (existingContainer) { - ModalService.confirm({ - title: 'Are you sure ?', - message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?', - buttons: { - confirm: { - label: 'Replace', - className: 'btn-danger' - } - }, - callback: function onConfirm(confirmed) { - if(!confirmed) { deferred.resolve(false); } - else { - // Remove old container - ContainerService.remove(existingContainer, true) - .then(function success(data) { - Notifications.success('Container Removed', existingContainer.Id); - deferred.resolve(true); - }) - .catch(function error(err) { - deferred.reject({ msg: 'Unable to remove container', err: err }); - }); - } - } - }); - } else { - deferred.resolve(true); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve containers'); - return undefined; - }); - return deferred.promise; - } - - function loadFromContainerCmd(d) { + + function loadFromContainerCmd() { if ($scope.config.Cmd) { $scope.config.Cmd = ContainerHelper.commandArrayToString($scope.config.Cmd); } else { @@ -315,7 +291,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai } } - function loadFromContainerPortBindings(d) { + function loadFromContainerPortBindings() { var bindings = []; for (var p in $scope.config.HostConfig.PortBindings) { if ({}.hasOwnProperty.call($scope.config.HostConfig.PortBindings, p)) { @@ -398,7 +374,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai } else { $scope.formValues.MacAddress = ''; } - + // ExtraHosts if ($scope.config.HostConfig.ExtraHosts) { var extraHosts = $scope.config.HostConfig.ExtraHosts; @@ -410,7 +386,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai } } - function loadFromContainerEnvironmentVariables(d) { + function loadFromContainerEnvironmentVariables() { var envArr = []; for (var e in $scope.config.Env) { if ({}.hasOwnProperty.call($scope.config.Env, e)) { @@ -421,7 +397,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai $scope.config.Env = envArr; } - function loadFromContainerLabels(d) { + function loadFromContainerLabels() { for (var l in $scope.config.Labels) { if ({}.hasOwnProperty.call($scope.config.Labels, l)) { $scope.formValues.Labels.push({ name: l, value: $scope.config.Labels[l]}); @@ -429,7 +405,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai } } - function loadFromContainerConsole(d) { + function loadFromContainerConsole() { if ($scope.config.OpenStdin && $scope.config.Tty) { $scope.formValues.Console = 'both'; } else if (!$scope.config.OpenStdin && $scope.config.Tty) { @@ -441,7 +417,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai } } - function loadFromContainerDevices(d) { + function loadFromContainerDevices() { var path = []; for (var dev in $scope.config.HostConfig.Devices) { if ({}.hasOwnProperty.call($scope.config.HostConfig.Devices, dev)) { @@ -452,7 +428,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai $scope.config.HostConfig.Devices = path; } - function loadFromContainerImageConfig(d) { + function loadFromContainerImageConfig() { var imageInfo = ImageHelper.extractImageAndRegistryFromRepository($scope.config.Image); RegistryService.retrieveRegistryFromRepository($scope.config.Image) .then(function success(data) { @@ -478,12 +454,28 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai } } + function loadFromContainerCapabilities(d) { + if (d.HostConfig.CapAdd) { + d.HostConfig.CapAdd.forEach(function(cap) { + $scope.formValues.capabilities.push(new ContainerCapability(cap, true)); + }); + } + if (d.HostConfig.CapDrop) { + d.HostConfig.CapDrop.forEach(function(cap) { + $scope.formValues.capabilities.push(new ContainerCapability(cap, false)); + }); + } + $scope.formValues.capabilities.sort(function(a, b) { + return a.capability < b.capability ? -1 : 1; + }); + } + function loadFromContainerSpec() { // Get container Container.get({ id: $transition$.params().from }).$promise .then(function success(d) { var fromContainer = new ContainerDetailsViewModel(d); - if (!fromContainer.ResourceControl) { + if (fromContainer.ResourceControl && fromContainer.ResourceControl.Public) { $scope.formValues.AccessControlData.AccessControlEnabled = false; } $scope.fromContainer = fromContainer; @@ -498,6 +490,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai loadFromContainerDevices(d); loadFromContainerImageConfig(d); loadFromContainerResources(d); + loadFromContainerCapabilities(d); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve container'); @@ -543,6 +536,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai } else { $scope.fromContainer = {}; $scope.formValues.Registry = {}; + $scope.formValues.capabilities = new ContainerCapabilities(); } }, function(e) { Notifications.error('Failure', e, 'Unable to retrieve running containers'); @@ -550,6 +544,14 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai SystemService.info() .then(function success(data) { + var runtimes = data.Runtimes; + $scope.availableRuntimes = runtimes; + if ('runc' in runtimes) { + $scope.config.HostConfig.Runtime = 'runc'; + } + else if (Object.keys(runtimes).length !== 0) { + $scope.config.HostConfig.Runtime = Object.keys(runtimes)[0]; + } $scope.state.sliderMaxCpu = 32; if (data.NCPU) { $scope.state.sliderMaxCpu = data.NCPU; @@ -588,62 +590,179 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai return true; } - $scope.create = function () { - confirmCreateContainer() - .then(function success(confirm) { - if (!confirm) { - return false; + + function create() { + var oldContainer = null; + + + HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName); + return findCurrentContainer() + .then(confirmCreateContainer) + .then(startCreationProcess) + .catch(notifyOnError) + .finally(final); + + function final() { + $scope.state.actionInProgress = false; + } + + function findCurrentContainer() { + return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } }) + .$promise + .then(function onQuerySuccess(containers) { + if (!containers.length) { + return; + } + oldContainer = containers[0]; + return oldContainer; + }) + .catch(notifyOnError); + + function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to retrieve containers'); + } + } + + function startCreationProcess(confirmed) { + if (!confirmed) { + return $q.when(); + } + if (!validateAccessControl()) { + return $q.when(); + } + $scope.state.actionInProgress = true; + return pullImageIfNeeded() + .then(stopAndRenameContainer(oldContainer)) + .then(createNewContainer) + .then(applyResourceControl) + .then(connectToExtraNetworks) + .then(removeOldContainer) + .then(onSuccess); + } + + function confirmCreateContainer(container) { + if (!container) { + return $q.when(true); } + return showConfirmationModal(); + + function showConfirmationModal() { + var deferred = $q.defer(); + + ModalService.confirm({ + title: 'Are you sure ?', + message: 'A container with the same name already exists. Portainer can automatically remove it and re-create one. Do you want to replace it?', + buttons: { + confirm: { + label: 'Replace', + className: 'btn-danger' + } + }, + callback: function onConfirm(confirmed) { + deferred.resolve(confirmed); + } + }); + + return deferred.promise; + } + } + + function stopAndRenameContainer(oldContainer) { + if (!oldContainer) { + return $q.when(); + } + return stopContainerIfNeeded(oldContainer) + .then(renameContainer); + } + + function stopContainerIfNeeded(oldContainer) { + if (oldContainer.State !== 'running') { + return $q.when(); + } + return ContainerService.stopContainer(oldContainer.Id); + } + + function renameContainer() { + return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1) + '-old'); + } + + function pullImageIfNeeded() { + return $q.when($scope.formValues.alwaysPull && + ImageService.pullImage($scope.config.Image, $scope.formValues.Registry, true)); + } + + function createNewContainer() { + var config = prepareConfiguration(); + return ContainerService.createAndStartContainer(config); + } + + function applyResourceControl(newContainer) { + var containerIdentifier = newContainer.Id; + var userId = Authentication.getUserDetails().ID; + + return $q.when(ResourceControlService.applyResourceControl( + 'container', + containerIdentifier, + userId, + $scope.formValues.AccessControlData, [] + )).then(function onApplyResourceControlSuccess() { + return containerIdentifier; + }); + } + + function connectToExtraNetworks(newContainerId) { + if (!$scope.extraNetworks) { + return $q.when(); + } + + var connectionPromises = Object.keys($scope.extraNetworks).map(function (networkName) { + return NetworkService.connectContainer(networkName, newContainerId); + }); + + return $q.all(connectionPromises); + } + + function removeOldContainer() { + var deferred = $q.defer(); + + if (!oldContainer) { + deferred.resolve(); + return; + } + + ContainerService.remove(oldContainer, true) + .then(notifyOnRemoval) + .catch(notifyOnRemoveError); + + return deferred.promise; + + function notifyOnRemoval() { + Notifications.success('Container Removed', oldContainer.Id); + deferred.resolve(); + } + + function notifyOnRemoveError(err) { + deferred.reject({ msg: 'Unable to remove container', err: err }); + } + } + + function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to create container'); + } + + function validateAccessControl() { var accessControlData = $scope.formValues.AccessControlData; var userDetails = Authentication.getUserDetails(); var isAdmin = userDetails.role === 1; - if (!validateForm(accessControlData, isAdmin)) { - return; - } + return validateForm(accessControlData, isAdmin); + } - $scope.state.actionInProgress = true; - var config = prepareConfiguration(); - var nodeName = $scope.formValues.NodeName; - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); - createContainer(config, accessControlData); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create container'); - }); - }; - - function createContainer(config, accessControlData) { - var containerIdentifier; - $q.when(!$scope.formValues.alwaysPull || ImageService.pullImage($scope.config.Image, $scope.formValues.Registry, true)) - .finally(function final() { - ContainerService.createAndStartContainer(config) - .then(function success(data) { - containerIdentifier = data.Id; - var userId = Authentication.getUserDetails().ID; - return ResourceControlService.applyResourceControl('container', containerIdentifier, userId, accessControlData, []); - }) - .then(function success() { - if($scope.extraNetworks) { - return $q.all( - Object.keys($scope.extraNetworks).map(function(networkName) { - return NetworkService.connectContainer(networkName, containerIdentifier); - }) - ); - } - }) - .then(function success() { - Notifications.success('Container successfully created'); - $state.go('docker.containers', {}, {reload: true}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to create container'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }); + function onSuccess() { + Notifications.success('Container successfully created'); + $state.go('docker.containers', {}, { reload: true }); + } } initView(); diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 4c6f957c1..e946afa17 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -130,6 +130,7 @@ Deploy the containerDeployment in progress... + {{ state.formValidationError }} @@ -152,6 +153,7 @@
  • Labels
  • Restart policy
  • Runtime & Resources
  • +
  • Capabilities
  • @@ -499,6 +501,17 @@
    + +
    + +
    + +
    +
    + @@ -585,6 +598,11 @@ + +
    + +
    + diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index 95c790140..a251ae2d2 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -187,7 +187,7 @@ - + @@ -223,21 +223,13 @@
    - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'public' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
    {{ config.Id }} +
    Image{{ container.Image }}{{ container.Config.Image}}@{{container.Image}}
    Port configuration
    - + Restart policies - - - - - - - - - -
    Name{{ container.HostConfig.RestartPolicy.Name }}
    MaximumRetryCount - {{ container.HostConfig.RestartPolicy.MaximumRetryCount }} -
    + diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index ad0f261a8..f687b116c 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -15,6 +15,8 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co leaveNetworkInProgress: false }; + $scope.updateRestartPolicy = updateRestartPolicy; + var update = function () { var nodeName = $transition$.params().nodeName; HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); @@ -54,7 +56,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co function executeContainerAction(id, action, successMessage, errorMessage) { action(id) - .then(function success(data) { + .then(function success() { Notifications.success(successMessage, id); update(); }) @@ -102,7 +104,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co $scope.renameContainer = function () { var container = $scope.container; ContainerService.renameContainer($transition$.params().id, container.newContainerName) - .then(function success(data) { + .then(function success() { container.Name = container.newContainerName; Notifications.success('Container successfully renamed', container.Name); }) @@ -118,7 +120,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co $scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) { $scope.state.leaveNetworkInProgress = true; NetworkService.disconnectContainer(networkId, container.Id, false) - .then(function success(data) { + .then(function success() { Notifications.success('Container left network', container.Id); $state.reload(); }) @@ -133,7 +135,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co $scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) { $scope.state.joinNetworkInProgress = true; NetworkService.connectContainer(networkId, container.Id) - .then(function success(data) { + .then(function success() { Notifications.success('Container joined network', container.Id); $state.reload(); }) @@ -149,7 +151,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co var image = $scope.config.Image; var registry = $scope.config.Registry; var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL); - Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) { + Commit.commitContainer({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function () { update(); Notifications.success('Container commited', $transition$.params().id); }, function (e) { @@ -192,40 +194,109 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co var container = $scope.container; var config = ContainerHelper.configFromContainer(container.Model); $scope.state.recreateContainerInProgress = true; - ContainerService.remove(container, true) - .then(function success() { - return RegistryService.retrieveRegistryFromRepository(container.Config.Image); - }) - .then(function success(data) { - return $q.when(!pullImage || ImageService.pullImage(container.Config.Image, data, true)); - }) - .then(function success() { - return ContainerService.createAndStartContainer(config); - }) - .then(function success(data) { - if (!container.ResourceControl) { - return true; - } else { - var containerIdentifier = data.Id; - var resourceControl = container.ResourceControl; - var users = resourceControl.UserAccesses.map(function(u) { - return u.UserId; - }); - var teams = resourceControl.TeamAccesses.map(function(t) { - return t.TeamId; - }); - return ResourceControlService.createResourceControl(resourceControl.AdministratorsOnly, - users, teams, containerIdentifier, 'container', []); + var isRunning = container.State.Running; + + return pullImageIfNeeded() + .then(stopContainerIfNeeded) + .then(renameContainer) + .then(setMainNetworkAndCreateContainer) + .then(connectContainerToOtherNetworks) + .then(startContainerIfNeeded) + .then(createResourceControlIfNeeded) + .then(deleteOldContainer) + .then(notifyAndChangeView) + .catch(notifyOnError); + + function stopContainerIfNeeded() { + if (!isRunning) { + return $q.when(); } - }) - .then(function success(data) { + return ContainerService.stopContainer(container.Id); + } + + function renameContainer() { + return ContainerService.renameContainer(container.Id, container.Name + '-old'); + } + + function pullImageIfNeeded() { + if (!pullImage) { + return $q.when(); + } + return getRegistry().then(function pullImage(containerRegistery) { + return ImageService.pullImage(container.Config.Image, containerRegistery, true); + }); + } + + function getRegistry() { + return RegistryService.retrieveRegistryFromRepository(container.Config.Image); + } + + function setMainNetworkAndCreateContainer() { + var networks = config.NetworkingConfig.EndpointsConfig; + var networksNames = Object.keys(networks); + if (networksNames.length > 1) { + config.NetworkingConfig.EndpointsConfig = {}; + config.NetworkingConfig.EndpointsConfig[networksNames[0]] = networks[0]; + } + return $q.all([ContainerService.createContainer(config), networks]); + } + + function connectContainerToOtherNetworks(createContainerData) { + var newContainer = createContainerData[0]; + var networks = createContainerData[1]; + var networksNames = Object.keys(networks); + var connectionPromises = networksNames.map(function connectToNetwork(name) { + NetworkService.connectContainer(name, newContainer.Id); + }); + return $q.all(connectionPromises) + .then(function onConnectToNetworkSuccess() { + return newContainer; + }); + } + + function deleteOldContainer(newContainer) { + return ContainerService.remove(container, true).then( + function onRemoveSuccess() { + return newContainer; + } + ); + } + + function startContainerIfNeeded(newContainer) { + if (!isRunning) { + return $q.when(newContainer); + } + return ContainerService.startContainer(newContainer.Id).then( + function onStartSuccess() { + return newContainer; + } + ); + } + + function createResourceControlIfNeeded(newContainer) { + if (!container.ResourceControl) { + return $q.when(); + } + var containerIdentifier = newContainer.Id; + var resourceControl = container.ResourceControl; + var users = resourceControl.UserAccesses.map(function(u) { + return u.UserId; + }); + var teams = resourceControl.TeamAccesses.map(function(t) { + return t.TeamId; + }); + return ResourceControlService.createResourceControl(resourceControl.Public, users, teams, containerIdentifier, 'container', []); + } + + function notifyAndChangeView() { Notifications.success('Container successfully re-created'); - $state.go('docker.containers', {}, {reload: true}); - }) - .catch(function error(err) { + $state.go('docker.containers', {}, { reload: true }); + } + + function notifyOnError(err) { Notifications.error('Failure', err, 'Unable to re-create container'); $scope.state.recreateContainerInProgress = false; - }); + } } $scope.recreate = function() { @@ -239,6 +310,28 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co }); }; + function updateRestartPolicy(restartPolicy, maximumRetryCount) { + maximumRetryCount = restartPolicy === 'on-failure' ? maximumRetryCount : undefined; + + return ContainerService + .updateRestartPolicy($scope.container.Id, restartPolicy, maximumRetryCount) + .then(onUpdateSuccess) + .catch(notifyOnError); + + function onUpdateSuccess() { + $scope.container.HostConfig.RestartPolicy = { + Name: restartPolicy, + MaximumRetryCount: maximumRetryCount + }; + Notifications.success('Restart policy updated'); + } + + function notifyOnError(err) { + Notifications.error('Failure', err, 'Unable to update restart policy'); + return $q.reject(err); + } + } + var provider = $scope.applicationState.endpoint.mode.provider; var apiVersion = $scope.applicationState.endpoint.apiVersion; NetworkService.networks( diff --git a/app/docker/views/containers/logs/containerLogsController.js b/app/docker/views/containers/logs/containerLogsController.js index 2141a1f3b..1d23adeb8 100644 --- a/app/docker/views/containers/logs/containerLogsController.js +++ b/app/docker/views/containers/logs/containerLogsController.js @@ -27,10 +27,6 @@ function ($scope, $transition$, $interval, ContainerService, Notifications, Http } } - function update(logs) { - $scope.logs = logs; - } - function setUpdateRepeater(skipHeaders) { var refreshRate = $scope.state.refreshRate; $scope.repeater = $interval(function() { diff --git a/app/docker/views/dashboard/dashboard.html b/app/docker/views/dashboard/dashboard.html index 86950c43e..5b1a2bac3 100644 --- a/app/docker/views/dashboard/dashboard.html +++ b/app/docker/views/dashboard/dashboard.html @@ -9,30 +9,22 @@
    -
    -
    - - -
    - Information -
    -
    - -

    - - Portainer is connected to a node that is part of a Swarm cluster. Some resources located on other nodes in the cluster might not be available for management, have a look - at our agent setup for more details. -

    -

    - - Portainer is connected to a worker node. Swarm management features will not be available. -

    -
    -
    -
    -
    -
    -
    + + +

    + + Portainer is connected to a node that is part of a Swarm cluster. Some resources located on other nodes in the cluster might not be available for management, have a look + at our agent setup for more details. +

    +

    + + Portainer is connected to a worker node. Swarm management features will not be available. +

    +
    +
    @@ -45,7 +37,10 @@ Endpoint {{ endpoint.Name }} - {{ info.NCPU }} {{ info.MemTotal | humansize }} + + {{ endpoint.Snapshots[0].TotalCPU }} + {{ endpoint.Snapshots[0].TotalMemory | humansize }} + - {{ info.Swarm && info.Swarm.NodeID !== '' ? 'Swarm' : 'Standalone' }} {{ info.ServerVersion }} + Agent @@ -115,8 +110,8 @@
    -
    {{ containers | containerswithstatus:'running' }} running
    -
    {{ containers | containerswithstatus:'stopped' }} stopped
    +
    {{ containers | runningcontainers }} running
    +
    {{ containers | stoppedcontainers }} stopped
    {{ containers.length }}
    Containers
    diff --git a/app/docker/views/dashboard/dashboardController.js b/app/docker/views/dashboard/dashboardController.js index d9bf1b2c4..114c745d0 100644 --- a/app/docker/views/dashboard/dashboardController.js +++ b/app/docker/views/dashboard/dashboardController.js @@ -1,6 +1,10 @@ angular.module('portainer.docker') -.controller('DashboardController', ['$scope', '$q', 'ContainerService', 'ImageService', 'NetworkService', 'VolumeService', 'SystemService', 'ServiceService', 'StackService', 'EndpointService', 'Notifications', 'EndpointProvider', -function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeService, SystemService, ServiceService, StackService, EndpointService, Notifications, EndpointProvider) { +.controller('DashboardController', ['$scope', '$q', 'ContainerService', 'ImageService', 'NetworkService', 'VolumeService', 'SystemService', 'ServiceService', 'StackService', 'EndpointService', 'Notifications', 'EndpointProvider', 'StateManager', +function ($scope, $q, ContainerService, ImageService, NetworkService, VolumeService, SystemService, ServiceService, StackService, EndpointService, Notifications, EndpointProvider, StateManager) { + + $scope.dismissInformationPanel = function(id) { + StateManager.dismissInformationPanel(id); + }; function initView() { var endpointMode = $scope.applicationState.endpoint.mode; diff --git a/app/docker/views/images/edit/imageController.js b/app/docker/views/images/edit/imageController.js index d89cd4dc3..29d91d8da 100644 --- a/app/docker/views/images/edit/imageController.js +++ b/app/docker/views/images/edit/imageController.js @@ -29,7 +29,7 @@ function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryServ var registry = $scope.formValues.Registry; ImageService.tagImage($transition$.params().id, image, registry.URL) - .then(function success(data) { + .then(function success() { Notifications.success('Image successfully tagged'); $state.go('docker.images.image', {id: $transition$.params().id}, {reload: true}); }) @@ -45,7 +45,7 @@ function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryServ var registry = data; return ImageService.pushImage(repository, registry); }) - .then(function success(data) { + .then(function success() { Notifications.success('Image successfully pushed', repository); }) .catch(function error(err) { @@ -63,7 +63,7 @@ function ($q, $scope, $transition$, $state, $timeout, ImageService, RegistryServ var registry = data; return ImageService.pullImage(repository, registry, false); }) - .then(function success(data) { + .then(function success() { Notifications.success('Image successfully pulled', repository); }) .catch(function error(err) { diff --git a/app/docker/views/images/imagesController.js b/app/docker/views/images/imagesController.js index cb364edad..f495f4185 100644 --- a/app/docker/views/images/imagesController.js +++ b/app/docker/views/images/imagesController.js @@ -21,7 +21,7 @@ function ($scope, $state, ImageService, Notifications, ModalService, HttpRequest $scope.state.actionInProgress = true; ImageService.pullImage(image, registry, false) - .then(function success(data) { + .then(function success() { Notifications.success('Image successfully pulled', image); $state.reload(); }) diff --git a/app/docker/views/images/import/importimage.html b/app/docker/views/images/import/importimage.html index 7615080f6..6a6b20f31 100644 --- a/app/docker/views/images/import/importimage.html +++ b/app/docker/views/images/import/importimage.html @@ -21,7 +21,7 @@
    - + {{ formValues.UploadFile.name }} diff --git a/app/docker/views/networks/create/createNetworkController.js b/app/docker/views/networks/create/createNetworkController.js index 0f6d3f31c..9f5dd6493 100644 --- a/app/docker/views/networks/create/createNetworkController.js +++ b/app/docker/views/networks/create/createNetworkController.js @@ -1,138 +1,207 @@ angular.module('portainer.docker') -.controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator', 'HttpRequestHelper', -function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator, HttpRequestHelper) { + .controller('CreateNetworkController', ['$q', '$scope', '$state', 'PluginService', 'Notifications', 'NetworkService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator', 'HttpRequestHelper', + function ($q, $scope, $state, PluginService, Notifications, NetworkService, LabelHelper, Authentication, ResourceControlService, FormValidator, HttpRequestHelper) { - $scope.formValues = { - DriverOptions: [], - Subnet: '', - Gateway: '', - Labels: [], - AccessControlData: new AccessControlFormData(), - NodeName: null - }; + $scope.formValues = { + DriverOptions: [], + Subnet: '', + Gateway: '', + IPRange: '', + AuxAddress: '', + Labels: [], + AccessControlData: new AccessControlFormData(), + NodeName: null, + Macvlan: new MacvlanFormData() + }; - $scope.state = { - formValidationError: '', - actionInProgress: false - }; + $scope.state = { + formValidationError: '', + actionInProgress: false + }; - $scope.availableNetworkDrivers = []; + $scope.availableNetworkDrivers = []; - $scope.config = { - Driver: 'bridge', - CheckDuplicate: true, - Internal: false, - // Force IPAM Driver to 'default', should not be required. - // See: https://github.com/docker/docker/issues/25735 - IPAM: { - Driver: 'default', - Config: [] - }, - Labels: {} - }; + $scope.config = { + Driver: 'bridge', + CheckDuplicate: true, + Internal: false, + Attachable: false, + // Force IPAM Driver to 'default', should not be required. + // See: https://github.com/docker/docker/issues/25735 + IPAM: { + Driver: 'default', + Config: [] + }, + Labels: {} + }; - $scope.addDriverOption = function() { - $scope.formValues.DriverOptions.push({ name: '', value: '' }); - }; + $scope.addDriverOption = function () { + $scope.formValues.DriverOptions.push({ + name: '', + value: '' + }); + }; - $scope.removeDriverOption = function(index) { - $scope.formValues.DriverOptions.splice(index, 1); - }; + $scope.removeDriverOption = function (index) { + $scope.formValues.DriverOptions.splice(index, 1); + }; - $scope.addLabel = function() { - $scope.formValues.Labels.push({ key: '', value: ''}); - }; + $scope.addLabel = function () { + $scope.formValues.Labels.push({ + key: '', + value: '' + }); + }; - $scope.removeLabel = function(index) { - $scope.formValues.Labels.splice(index, 1); - }; + $scope.removeLabel = function (index) { + $scope.formValues.Labels.splice(index, 1); + }; - function prepareIPAMConfiguration(config) { - if ($scope.formValues.Subnet) { - var ipamConfig = {}; - ipamConfig.Subnet = $scope.formValues.Subnet; - if ($scope.formValues.Gateway) { - ipamConfig.Gateway = $scope.formValues.Gateway ; + function prepareIPAMConfiguration(config) { + if ($scope.formValues.Subnet) { + var ipamConfig = {}; + ipamConfig.Subnet = $scope.formValues.Subnet; + if ($scope.formValues.Gateway) { + ipamConfig.Gateway = $scope.formValues.Gateway; + } + if ($scope.formValues.IPRange) { + ipamConfig.IPRange = $scope.formValues.IPRange; + } + if ($scope.formValues.AuxAddress) { + ipamConfig.AuxAddress = $scope.formValues.AuxAddress; + } + config.IPAM.Config.push(ipamConfig); + } } - config.IPAM.Config.push(ipamConfig); + + function prepareDriverOptions(config) { + var options = {}; + $scope.formValues.DriverOptions.forEach(function (option) { + options[option.name] = option.value; + }); + config.Options = options; + } + + function prepareLabelsConfig(config) { + config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); + } + + function prepareConfiguration() { + var config = angular.copy($scope.config); + prepareIPAMConfiguration(config); + prepareDriverOptions(config); + prepareLabelsConfig(config); + return config; + } + + function modifyNetworkConfigurationForMacvlanConfigOnly(config) { + config.Internal = null; + config.Attachable = null; + config.ConfigOnly = true; + config.Options.parent = $scope.formValues.Macvlan.ParentNetworkCard; + } + + function modifyNetworkConfigurationForMacvlanConfigFrom(config, selectedNetworkConfig) { + config.ConfigFrom = { + Network: selectedNetworkConfig.Name + }; + config.Scope = 'swarm'; + } + + function validateForm(accessControlData, isAdmin) { + $scope.state.formValidationError = ''; + var error = ''; + error = FormValidator.validateAccessControl(accessControlData, isAdmin); + + if (error) { + $scope.state.formValidationError = error; + return false; + } + return true; + } + + function createNetwork(context) { + HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName); + + $scope.state.actionInProgress = true; + NetworkService.create(context.networkConfiguration) + .then(function success(data) { + var networkIdentifier = data.Id; + var userId = context.userDetails.ID; + return ResourceControlService.applyResourceControl('network', networkIdentifier, userId, context.accessControlData, []); + }) + .then(function success() { + Notifications.success('Network successfully created'); + if (context.reload) { + $state.go('docker.networks', {}, { + reload: true + }); + } + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'An error occured during network creation'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + } + + $scope.create = function () { + var networkConfiguration = prepareConfiguration(); + var accessControlData = $scope.formValues.AccessControlData; + var userDetails = Authentication.getUserDetails(); + var isAdmin = userDetails.role === 1; + + if (!validateForm(accessControlData, isAdmin)) { + return; + } + + var creationContext = { + nodeName: $scope.formValues.NodeName, + networkConfiguration: networkConfiguration, + userDetails: userDetails, + accessControlData: accessControlData, + reload: true + }; + + if ($scope.config.Driver === 'macvlan') { + if ($scope.formValues.Macvlan.Scope === 'local') { + modifyNetworkConfigurationForMacvlanConfigOnly(networkConfiguration); + } else if ($scope.formValues.Macvlan.Scope === 'swarm') { + var selectedNetworkConfig = $scope.formValues.Macvlan.SelectedNetworkConfig; + modifyNetworkConfigurationForMacvlanConfigFrom(networkConfiguration, selectedNetworkConfig); + creationContext.nodeName = selectedNetworkConfig.NodeName; + } + } + + if ($scope.config.Driver === 'macvlan' && $scope.formValues.Macvlan.Scope === 'local' && + $scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE') { + var selectedNodes = $scope.formValues.Macvlan.DatatableState.selectedItems; + selectedNodes.forEach(function (node, idx) { + creationContext.nodeName = node.Hostname; + creationContext.reload = idx === selectedNodes.length - 1 ? true : false; + createNetwork(creationContext); + }); + } else { + createNetwork(creationContext); + } + }; + + function initView() { + var apiVersion = $scope.applicationState.endpoint.apiVersion; + + PluginService.networkPlugins(apiVersion < 1.25) + .then(function success(data) { + if ($scope.applicationState.endpoint.mode.provider !== 'DOCKER_SWARM_MODE') { + data.splice(data.indexOf('macvlan'), 1); + } + $scope.availableNetworkDrivers = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve network drivers'); + }); + } + + initView(); } - } - - function prepareDriverOptions(config) { - var options = {}; - $scope.formValues.DriverOptions.forEach(function (option) { - options[option.name] = option.value; - }); - config.Options = options; - } - - function prepareLabelsConfig(config) { - config.Labels = LabelHelper.fromKeyValueToLabelHash($scope.formValues.Labels); - } - - function prepareConfiguration() { - var config = angular.copy($scope.config); - prepareIPAMConfiguration(config); - prepareDriverOptions(config); - prepareLabelsConfig(config); - return config; - } - - function validateForm(accessControlData, isAdmin) { - $scope.state.formValidationError = ''; - var error = ''; - error = FormValidator.validateAccessControl(accessControlData, isAdmin); - - if (error) { - $scope.state.formValidationError = error; - return false; - } - return true; - } - - $scope.create = function () { - var networkConfiguration = prepareConfiguration(); - var accessControlData = $scope.formValues.AccessControlData; - var userDetails = Authentication.getUserDetails(); - var isAdmin = userDetails.role === 1; - - if (!validateForm(accessControlData, isAdmin)) { - return; - } - - var nodeName = $scope.formValues.NodeName; - HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); - - $scope.state.actionInProgress = true; - NetworkService.create(networkConfiguration) - .then(function success(data) { - var networkIdentifier = data.Id; - var userId = userDetails.ID; - return ResourceControlService.applyResourceControl('network', networkIdentifier, userId, accessControlData, []); - }) - .then(function success() { - Notifications.success('Network successfully created'); - $state.go('docker.networks', {}, {reload: true}); - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'An error occured during network creation'); - }) - .finally(function final() { - $scope.state.actionInProgress = false; - }); - }; - - function initView() { - var apiVersion = $scope.applicationState.endpoint.apiVersion; - - PluginService.networkPlugins(apiVersion < 1.25) - .then(function success(data){ - $scope.availableNetworkDrivers = data; - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve network drivers'); - }); - } - - initView(); -}]); + ]); \ No newline at end of file diff --git a/app/docker/views/networks/create/createnetwork.html b/app/docker/views/networks/create/createnetwork.html index 827f725d3..03711c36c 100644 --- a/app/docker/views/networks/create/createnetwork.html +++ b/app/docker/views/networks/create/createnetwork.html @@ -9,37 +9,22 @@
    -
    +
    - -
    + +
    -
    - Network configuration -
    - -
    - -
    - -
    - -
    - -
    -
    -
    Driver configuration
    -
    +
    @@ -77,6 +62,38 @@
    + + + +
    +
    + Network configuration +
    + +
    + +
    + +
    + +
    + +
    +
    + + +
    + +
    + +
    + +
    + +
    +
    + +
    Advanced configuration
    @@ -108,24 +125,37 @@
    -
    +
    -
    -
    + +
    +
    + + +
    +
    + +
    Deployment
    - +
    @@ -138,7 +168,8 @@
    - @@ -151,4 +182,4 @@
    -
    +
    \ No newline at end of file diff --git a/app/docker/views/networks/edit/network.html b/app/docker/views/networks/edit/network.html index 08db62c3f..f22562163 100644 --- a/app/docker/views/networks/edit/network.html +++ b/app/docker/views/networks/edit/network.html @@ -31,6 +31,14 @@ Scope {{ network.Scope }} + + Attachable + {{ network.Attachable }} + + + Internal + {{ network.Internal }} + Subnet {{ network.IPAM.Config[0].Subnet }} diff --git a/app/docker/views/networks/edit/networkController.js b/app/docker/views/networks/edit/networkController.js index 0ba905c2a..9ca04092e 100644 --- a/app/docker/views/networks/edit/networkController.js +++ b/app/docker/views/networks/edit/networkController.js @@ -1,10 +1,10 @@ angular.module('portainer.docker') -.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'ContainerHelper', 'Notifications', 'HttpRequestHelper', -function ($scope, $state, $transition$, $filter, NetworkService, Container, ContainerHelper, Notifications, HttpRequestHelper) { +.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', +function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper) { - $scope.removeNetwork = function removeNetwork(networkId) { + $scope.removeNetwork = function removeNetwork() { NetworkService.remove($transition$.params().id, $transition$.params().id) - .then(function success(data) { + .then(function success() { Notifications.success('Network removed', $transition$.params().id); $state.go('docker.networks', {}); }) @@ -16,7 +16,7 @@ function ($scope, $state, $transition$, $filter, NetworkService, Container, Cont $scope.containerLeaveNetwork = function containerLeaveNetwork(network, container) { HttpRequestHelper.setPortainerAgentTargetHeader(container.NodeName); NetworkService.disconnectContainer($transition$.params().id, container.Id, false) - .then(function success(data) { + .then(function success() { Notifications.success('Container left network', $transition$.params().id); $state.go('docker.networks.network', { id: network.Id }, { reload: true }); }) diff --git a/app/docker/views/nodes/edit/nodeController.js b/app/docker/views/nodes/edit/nodeController.js index bdb59984e..30f210b2a 100644 --- a/app/docker/views/nodes/edit/nodeController.js +++ b/app/docker/views/nodes/edit/nodeController.js @@ -52,7 +52,7 @@ function ($scope, $state, $transition$, LabelHelper, Node, NodeHelper, Task, Not config.Role = node.Role; config.Labels = LabelHelper.fromKeyValueToLabelHash(node.Labels); - Node.update({ id: node.Id, version: node.Version }, config, function (data) { + Node.update({ id: node.Id, version: node.Version }, config, function () { Notifications.success('Node successfully updated', 'Node updated'); $state.go('docker.nodes.node', {id: node.Id}, {reload: true}); }, function (e) { diff --git a/app/docker/views/secrets/edit/secretController.js b/app/docker/views/secrets/edit/secretController.js index 3be0614f9..15aecfc1f 100644 --- a/app/docker/views/secrets/edit/secretController.js +++ b/app/docker/views/secrets/edit/secretController.js @@ -4,7 +4,7 @@ function ($scope, $transition$, $state, SecretService, Notifications) { $scope.removeSecret = function removeSecret(secretId) { SecretService.remove(secretId) - .then(function success(data) { + .then(function success() { Notifications.success('Secret successfully removed'); $state.go('docker.secrets', {}); }) diff --git a/app/docker/views/services/create/createServiceController.js b/app/docker/views/services/create/createServiceController.js index 8a2fd3e4e..9f69ad0b5 100644 --- a/app/docker/views/services/create/createServiceController.js +++ b/app/docker/views/services/create/createServiceController.js @@ -1,6 +1,6 @@ -angular.module('portainer.docker') -.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'PluginService', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService', -function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, PluginService, RegistryService, HttpRequestHelper, NodeService, SettingsService) { + angular.module('portainer.docker') +.controller('CreateServiceController', ['$q', '$scope', '$state', '$timeout', 'Service', 'ServiceHelper', 'ConfigService', 'ConfigHelper', 'SecretHelper', 'SecretService', 'VolumeService', 'NetworkService', 'ImageHelper', 'LabelHelper', 'Authentication', 'ResourceControlService', 'Notifications', 'FormValidator', 'PluginService', 'RegistryService', 'HttpRequestHelper', 'NodeService', 'SettingsService', 'WebhookService','EndpointProvider', +function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, ConfigHelper, SecretHelper, SecretService, VolumeService, NetworkService, ImageHelper, LabelHelper, Authentication, ResourceControlService, Notifications, FormValidator, PluginService, RegistryService, HttpRequestHelper, NodeService, SettingsService, WebhookService,EndpointProvider) { $scope.formValues = { Name: '', @@ -40,7 +40,8 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C RestartMaxAttempts: 0, RestartWindow: '0s', LogDriverName: '', - LogDriverOpts: [] + LogDriverOpts: [], + Webhook: false }; $scope.state = { @@ -142,7 +143,7 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C $scope.formValues.ContainerLabels.splice(index, 1); }; - $scope.addLogDriverOpt = function(value) { + $scope.addLogDriverOpt = function() { $scope.formValues.LogDriverOpts.push({ name: '', value: ''}); }; @@ -422,9 +423,14 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C var registry = $scope.formValues.Registry; var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : ''; HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); + + var serviceIdentifier; Service.create(config).$promise .then(function success(data) { - var serviceIdentifier = data.ID; + serviceIdentifier = data.ID; + return $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceIdentifier, EndpointProvider.endpointID())); + }) + .then(function success() { var userId = Authentication.getUserDetails().ID; return ResourceControlService.applyResourceControl('service', serviceIdentifier, userId, accessControlData, []); }) @@ -492,7 +498,6 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C function initView() { var apiVersion = $scope.applicationState.endpoint.apiVersion; - var provider = $scope.applicationState.endpoint.mode.provider; $q.all({ volumes: VolumeService.volumes(), diff --git a/app/docker/views/services/create/createservice.html b/app/docker/views/services/create/createservice.html index d9481adb1..40d830f1a 100644 --- a/app/docker/views/services/create/createservice.html +++ b/app/docker/views/services/create/createservice.html @@ -101,6 +101,22 @@
    + +
    + Webhooks +
    +
    +
    + + +
    +
    + diff --git a/app/docker/views/services/edit/service.html b/app/docker/views/services/edit/service.html index e0b2b49de..0b3e2e854 100644 --- a/app/docker/views/services/edit/service.html +++ b/app/docker/views/services/edit/service.html @@ -71,6 +71,24 @@ ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" ng-disabled="isUpdating"> + + + Service webhook + + + + + {{ webhookURL | truncatelr }} + + + + + + Service logs @@ -93,7 +111,7 @@