Merge branch 'release/1.24.0'

pull/2698/merge 1.24.0
Anthony Lapenna 2020-05-29 14:34:39 +12:00
commit 13f712d06d
850 changed files with 38478 additions and 29706 deletions

View File

@ -10,6 +10,10 @@ globals:
extends: extends:
- 'eslint:recommended' - 'eslint:recommended'
- prettier
plugins:
- import
parserOptions: parserOptions:
ecmaVersion: 2018 ecmaVersion: 2018
@ -17,276 +21,9 @@ parserOptions:
ecmaFeatures: ecmaFeatures:
modules: true modules: true
# # http://eslint.org/docs/rules/
rules: rules:
# # Possible Errors
# no-await-in-loop: off
# no-cond-assign: error
# no-console: off
# no-constant-condition: error
no-control-regex: off no-control-regex: off
# no-debugger: error
# no-dupe-args: error
# no-dupe-keys: error
# no-duplicate-case: error
# no-empty-character-class: error
no-empty: warn no-empty: warn
# no-ex-assign: error
# no-extra-boolean-cast: error
# no-extra-parens: off
# no-extra-semi: error
# no-func-assign: error
# no-inner-declarations:
# - error
# - functions
# no-invalid-regexp: error
# no-irregular-whitespace: error
# no-negated-in-lhs: error
# no-obj-calls: error
# no-prototype-builtins: off
# no-regex-spaces: error
# no-sparse-arrays: error
# no-template-curly-in-string: off
# no-unexpected-multiline: error
# no-unreachable: error
# no-unsafe-finally: off
# no-unsafe-negation: off
# use-isnan: error
# valid-jsdoc: off
# valid-typeof: error
# # Best Practices
# accessor-pairs: error
# array-callback-return: off
# block-scoped-var: off
# class-methods-use-this: off
# complexity:
# - error
# - 6
# consistent-return: off
# curly: off
# default-case: off
# dot-location: off
# dot-notation: off
# eqeqeq: error
# guard-for-in: error
# no-alert: error
# no-caller: error
# no-case-declarations: error
# no-div-regex: error
# no-else-return: off
no-empty-function: warn no-empty-function: warn
# no-empty-pattern: error
# no-eq-null: error
# no-eval: error
# no-extend-native: error
# no-extra-bind: error
# no-extra-label: off
# no-fallthrough: error
# no-floating-decimal: off
# no-global-assign: off
# no-implicit-coercion: off
# no-implied-eval: error
# no-invalid-this: off
# no-iterator: error
# no-labels:
# - error
# - allowLoop: true
# allowSwitch: true
# no-lone-blocks: error
# no-loop-func: error
# no-magic-number: off
# no-multi-spaces: off
# no-multi-str: off
# no-native-reassign: error
# no-new-func: error
# no-new-wrappers: error
# no-new: error
# no-octal-escape: error
# no-octal: error
# no-param-reassign: off
# no-proto: error
# no-redeclare: error
# no-restricted-properties: off
# no-return-assign: error
# no-return-await: off
# no-script-url: error
# no-self-assign: off
# no-self-compare: error
# no-sequences: off
# no-throw-literal: off
# no-unmodified-loop-condition: off
# no-unused-expressions: error
# no-unused-labels: off
# no-useless-call: error
# no-useless-concat: error
no-useless-escape: off no-useless-escape: off
# no-useless-return: off import/order: error
# no-void: error
# no-warning-comments: off
# no-with: error
# prefer-promise-reject-errors: off
# radix: error
# require-await: off
# vars-on-top: off
# wrap-iife: error
# yoda: off
# # Strict
# strict: off
# # Variables
# init-declarations: off
# no-catch-shadow: error
# no-delete-var: error
# no-label-var: error
# no-restricted-globals: off
# no-shadow-restricted-names: error
# no-shadow: off
# no-undef-init: error
# no-undef: off
# no-undefined: off
# no-unused-vars:
# - warn
# -
# vars: local
# no-use-before-define: off
# # Node.js and CommonJS
# callback-return: error
# global-require: error
# handle-callback-err: error
# no-mixed-requires: off
# no-new-require: off
# no-path-concat: error
# no-process-env: off
# no-process-exit: error
# no-restricted-modules: off
# no-sync: off
# # Stylistic Issues
# array-bracket-spacing: off
# block-spacing: off
# brace-style: off
# camelcase: off
# capitalized-comments: off
# comma-dangle:
# - error
# - never
# comma-spacing: off
# comma-style: off
# computed-property-spacing: off
# consistent-this: off
# eol-last: off
# func-call-spacing: off
# func-name-matching: off
# func-names: off
# func-style: off
# id-length: off
# id-match: off
# indent: off
# jsx-quotes: off
# key-spacing: off
# keyword-spacing: off
# line-comment-position: off
# linebreak-style:
# - error
# - unix
# lines-around-comment: off
# lines-around-directive: off
# max-depth: off
# max-len: off
# max-nested-callbacks: off
# max-params: off
# max-statements-per-line: off
# max-statements:
# - error
# - 30
# multiline-ternary: off
# new-cap: off
# new-parens: off
# newline-after-var: off
# newline-before-return: off
# newline-per-chained-call: off
# no-array-constructor: off
# no-bitwise: off
# no-continue: off
# no-inline-comments: off
# no-lonely-if: off
# no-mixed-operators: off
# no-mixed-spaces-and-tabs: off
# no-multi-assign: off
# no-multiple-empty-lines: off
# no-negated-condition: off
# no-nested-ternary: off
# no-new-object: off
# no-plusplus: off
# no-restricted-syntax: off
# no-spaced-func: off
# no-tabs: off
# no-ternary: off
# no-trailing-spaces: off
# no-underscore-dangle: off
# no-unneeded-ternary: off
# object-curly-newline: off
# object-curly-spacing: off
# object-property-newline: off
# one-var-declaration-per-line: off
# one-var: off
# operator-assignment: off
# operator-linebreak: off
# padded-blocks: off
# quote-props: off
# quotes:
# - error
# - single
# require-jsdoc: off
# semi-spacing: off
# semi:
# - error
# - always
# sort-keys: off
# sort-vars: off
# space-before-blocks: off
# space-before-function-paren: off
# space-in-parens: off
# space-infix-ops: off
# space-unary-ops: off
# spaced-comment: off
# template-tag-spacing: off
# unicode-bom: off
# wrap-regex: off
# # ECMAScript 6
# arrow-body-style: off
# arrow-parens: off
# arrow-spacing: off
# constructor-super: off
# generator-star-spacing: off
# no-class-assign: off
# no-confusing-arrow: off
# no-const-assign: off
# no-dupe-class-members: off
# no-duplicate-imports: off
# no-new-symbol: off
# no-restricted-imports: off
# no-this-before-super: off
# no-useless-computed-key: off
# no-useless-constructor: off
# no-useless-rename: off
# no-var: off
# object-shorthand: off
# prefer-arrow-callback: off
# prefer-const: off
# prefer-destructuring: off
# prefer-numeric-literals: off
# prefer-rest-params: off
# prefer-reflect: off
# prefer-spread: off
# prefer-template: off
# require-yield: off
# rest-spread-spacing: off
# sort-imports: off
# symbol-description: off
# template-curly-spacing: off
# yield-star-spacing: off

3
.gitignore vendored
View File

@ -4,4 +4,5 @@ dist
portainer-checksum.txt portainer-checksum.txt
api/cmd/portainer/portainer* api/cmd/portainer/portainer*
.tmp .tmp
.vscode .vscode
.eslintcache

13
.prettierrc Normal file
View File

@ -0,0 +1,13 @@
{
"printWidth": 180,
"singleQuote": true,
"htmlWhitespaceSensitivity": "strict",
"overrides": [
{
"files": ["*.html"],
"options": {
"parser": "angular"
}
}
]
}

View File

@ -15,21 +15,7 @@ For example, if you work on a bugfix for the issue #361, you could name the bran
## Issues open to contribution ## Issues open to contribution
Want to contribute but don't know where to start? Want to contribute but don't know where to start? Have a look at the issues labeled with the `good first issue` label: https://github.com/portainer/portainer/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22
Some of the open issues are labeled with prefix `exp/`, this is used to mark them as available for contributors to work on. All of these have an attributed difficulty level:
* **beginner**: a task that should be accessible with users not familiar with the codebase
* **intermediate**: a task that require some understanding of the project codebase or some experience in
either AngularJS or Golang
* **advanced**: a task that require a deep understanding of the project codebase
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

View File

@ -3,15 +3,14 @@
</p> </p>
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/) [![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") [![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)
[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop) [![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer) [![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters). **_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too). **_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*. **_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
## Demo ## Demo
@ -29,36 +28,31 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
## Getting started ## Getting started
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html) - [Deploy Portainer](https://www.portainer.io/installation/)
* [Documentation](https://portainer.readthedocs.io) - [Documentation](https://www.portainer.io/documentation/)
## Getting help ## Getting help
**NOTE**: You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/ For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
* Issues: https://github.com/portainer/portainer/issues For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/ - Issues: https://github.com/portainer/portainer/issues
- FAQ: https://www.portainer.io/documentation/faqs/
- Slack (chat): https://portainer.io/slack/
## Reporting bugs and contributing ## Reporting bugs and contributing
* Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new). - Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
* Want to help us build **_portainer_**? Follow our [contribution guidelines](https://portainer.readthedocs.io/en/latest/contribute.html) to build it locally and make a pull request. We need all the help we can get! - Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get!
## Security ## Security
* Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>. - Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## Limitations ## Limitations
**_Portainer_** has full support for the following Docker versions: Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported.
* Docker 1.10 to the latest version
* Standalone Docker Swarm >= 1.2.3 _(**NOTE:** Use of Standalone Docker Swarm is being discouraged since the introduction of built-in Swarm Mode in Docker. While older versions of Portainer had support for Standalone Docker Swarm, Portainer 1.17.0 and newer **do not** support it. However, the built-in Swarm Mode of Docker is fully supported.)_
Partial support for the following Docker versions (some features may not be available):
* Docker 1.9
## Licensing ## Licensing
@ -68,4 +62,4 @@ Portainer also contains the following code, which is licensed under the [MIT lic
UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io) UI For Docker: Copyright (c) 2013-2016 Michael Crosby (crosbymichael.com), Kevan Ahlquist (kevanahlquist.com), Anthony Lapenna (portainer.io)
rdash-angular: Copyright (c) [2014] [Elliot Hesp] rdash-angular: Copyright (c) [2014][elliot hesp]

View File

@ -5,6 +5,9 @@ import (
"path" "path"
"time" "time"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/tunnelserver" "github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
@ -36,28 +39,31 @@ const (
// Store defines the implementation of portainer.DataStore using // Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system. // BoltDB as the storage system.
type Store struct { type Store struct {
path string path string
db *bolt.DB db *bolt.DB
checkForDataMigration bool checkForDataMigration bool
fileService portainer.FileService fileService portainer.FileService
RoleService *role.Service RoleService *role.Service
DockerHubService *dockerhub.Service DockerHubService *dockerhub.Service
EndpointGroupService *endpointgroup.Service EdgeGroupService *edgegroup.Service
EndpointService *endpoint.Service EdgeStackService *edgestack.Service
ExtensionService *extension.Service EndpointGroupService *endpointgroup.Service
RegistryService *registry.Service EndpointService *endpoint.Service
ResourceControlService *resourcecontrol.Service EndpointRelationService *endpointrelation.Service
SettingsService *settings.Service ExtensionService *extension.Service
StackService *stack.Service RegistryService *registry.Service
TagService *tag.Service ResourceControlService *resourcecontrol.Service
TeamMembershipService *teammembership.Service SettingsService *settings.Service
TeamService *team.Service StackService *stack.Service
TemplateService *template.Service TagService *tag.Service
TunnelServerService *tunnelserver.Service TeamMembershipService *teammembership.Service
UserService *user.Service TeamService *team.Service
VersionService *version.Service TemplateService *template.Service
WebhookService *webhook.Service TunnelServerService *tunnelserver.Service
ScheduleService *schedule.Service UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
ScheduleService *schedule.Service
} }
// NewStore initializes a new Store and the associated services // NewStore initializes a new Store and the associated services
@ -117,22 +123,24 @@ func (store *Store) MigrateData() error {
if version < portainer.DBVersion { if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{ migratorParams := &migrator.Parameters{
DB: store.db, DB: store.db,
DatabaseVersion: version, DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService, EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService, EndpointService: store.EndpointService,
ExtensionService: store.ExtensionService, EndpointRelationService: store.EndpointRelationService,
RegistryService: store.RegistryService, ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService, RegistryService: store.RegistryService,
RoleService: store.RoleService, ResourceControlService: store.ResourceControlService,
ScheduleService: store.ScheduleService, RoleService: store.RoleService,
SettingsService: store.SettingsService, ScheduleService: store.ScheduleService,
StackService: store.StackService, SettingsService: store.SettingsService,
TeamMembershipService: store.TeamMembershipService, StackService: store.StackService,
TemplateService: store.TemplateService, TagService: store.TagService,
UserService: store.UserService, TeamMembershipService: store.TeamMembershipService,
VersionService: store.VersionService, TemplateService: store.TemplateService,
FileService: store.fileService, UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
} }
migrator := migrator.NewMigrator(migratorParams) migrator := migrator.NewMigrator(migratorParams)
@ -160,6 +168,18 @@ func (store *Store) initServices() error {
} }
store.DockerHubService = dockerhubService store.DockerHubService = dockerhubService
edgeStackService, err := edgestack.NewService(store.db)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
edgeGroupService, err := edgegroup.NewService(store.db)
if err != nil {
return err
}
store.EdgeGroupService = edgeGroupService
endpointgroupService, err := endpointgroup.NewService(store.db) endpointgroupService, err := endpointgroup.NewService(store.db)
if err != nil { if err != nil {
return err return err
@ -172,6 +192,12 @@ func (store *Store) initServices() error {
} }
store.EndpointService = endpointService store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.db)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.db) extensionService, err := extension.NewService(store.db)
if err != nil { if err != nil {
return err return err

View File

@ -0,0 +1,94 @@
package edgegroup
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgegroups"
)
// Service represents a service for managing Edge group 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
}
// EdgeGroups return an array containing all the Edge groups.
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 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 group portainer.EdgeGroup
err := internal.UnmarshalObjectWithJsoniter(v, &group)
if err != nil {
return err
}
groups = append(groups, group)
}
return nil
})
return groups, err
}
// EdgeGroup returns an Edge group by ID.
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
var group portainer.EdgeGroup
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &group)
if err != nil {
return nil, err
}
return &group, nil
}
// UpdateEdgeGroup updates an Edge group.
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, group)
}
// DeleteEdgeGroup deletes an Edge group.
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
func (service *Service) CreateEdgeGroup(group *portainer.EdgeGroup) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
group.ID = portainer.EdgeGroupID(id)
data, err := internal.MarshalObject(group)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(group.ID)), data)
})
}

View File

@ -0,0 +1,101 @@
package edgestack
import (
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edge_stack"
)
// Service represents a service for managing Edge stack 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
}
// EdgeStacks returns an array containing all edge stacks
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
var stacks = make([]portainer.EdgeStack, 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 stack portainer.EdgeStack
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err
}
stacks = append(stacks, stack)
}
return nil
})
return stacks, err
}
// EdgeStack returns an Edge stack by ID.
func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
var stack portainer.EdgeStack
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &stack)
if err != nil {
return nil, err
}
return &stack, nil
}
// CreateEdgeStack assign an ID to a new Edge stack and saves it.
func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
if edgeStack.ID == 0 {
id, _ := bucket.NextSequence()
edgeStack.ID = portainer.EdgeStackID(id)
}
data, err := internal.MarshalObject(edgeStack)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(edgeStack.ID)), data)
})
}
// UpdateEdgeStack updates an Edge stack.
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, edgeStack)
}
// DeleteEdgeStack deletes an Edge stack.
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
identifier := internal.Itob(int(ID))
return internal.DeleteObject(service.db, BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an endpoint.
func (service *Service) GetNextIdentifier() int {
return internal.GetNextIdentifier(service.db, BucketName)
}

View File

@ -0,0 +1,68 @@
package endpointrelation
import (
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/internal"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_relations"
)
// Service represents a service for managing endpoint relation 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
}
// EndpointRelation returns a Endpoint relation object by EndpointID
func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
var endpointRelation portainer.EndpointRelation
identifier := internal.Itob(int(endpointID))
err := internal.GetObject(service.db, BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
return &endpointRelation, nil
}
// CreateEndpointRelation saves endpointRelation
func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error {
return service.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
data, err := internal.MarshalObject(endpointRelation)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(endpointRelation.EndpointID)), data)
})
}
// UpdateEndpointRelation updates an Endpoint relation object
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
identifier := internal.Itob(int(EndpointID))
return internal.UpdateObject(service.db, BucketName, identifier, endpointRelation)
}
// DeleteEndpointRelation deletes an Endpoint relation object
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
identifier := internal.Itob(int(EndpointID))
return internal.DeleteObject(service.db, BucketName, identifier)
}

View File

@ -16,7 +16,7 @@ func (store *Store) Init() error {
Labels: []portainer.Pair{}, Labels: []portainer.Pair{},
UserAccessPolicies: portainer.UserAccessPolicies{}, UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{},
Tags: []string{}, TagIDs: []portainer.TagID{},
} }
err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup) err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)

View File

@ -0,0 +1,92 @@
package migrator
import "github.com/portainer/portainer/api"
func (m *Migrator) updateTagsToDBVersion23() error {
tags, err := m.tagService.Tags()
if err != nil {
return err
}
for _, tag := range tags {
tag.EndpointGroups = make(map[portainer.EndpointGroupID]bool)
tag.Endpoints = make(map[portainer.EndpointID]bool)
err = m.tagService.UpdateTag(tag.ID, &tag)
if err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
tags, err := m.tagService.Tags()
if err != nil {
return err
}
tagsNameMap := make(map[string]portainer.Tag)
for _, tag := range tags {
tagsNameMap[tag.Name] = tag
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
endpointTags := make([]portainer.TagID, 0)
for _, tagName := range endpoint.Tags {
tag, ok := tagsNameMap[tagName]
if ok {
endpointTags = append(endpointTags, tag.ID)
tag.Endpoints[endpoint.ID] = true
}
}
endpoint.TagIDs = endpointTags
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
relation := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
err = m.endpointRelationService.CreateEndpointRelation(relation)
if err != nil {
return err
}
}
endpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
}
for _, endpointGroup := range endpointGroups {
endpointGroupTags := make([]portainer.TagID, 0)
for _, tagName := range endpointGroup.Tags {
tag, ok := tagsNameMap[tagName]
if ok {
endpointGroupTags = append(endpointGroupTags, tag.ID)
tag.EndpointGroups[endpointGroup.ID] = true
}
}
endpointGroup.TagIDs = endpointGroupTags
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
if err != nil {
return err
}
}
for _, tag := range tagsNameMap {
err = m.tagService.UpdateTag(tag.ID, &tag)
if err != nil {
return err
}
}
return nil
}

View File

@ -5,6 +5,7 @@ import (
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/endpoint" "github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup" "github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/extension" "github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/registry" "github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol" "github.com/portainer/portainer/api/bolt/resourcecontrol"
@ -12,6 +13,7 @@ import (
"github.com/portainer/portainer/api/bolt/schedule" "github.com/portainer/portainer/api/bolt/schedule"
"github.com/portainer/portainer/api/bolt/settings" "github.com/portainer/portainer/api/bolt/settings"
"github.com/portainer/portainer/api/bolt/stack" "github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/teammembership" "github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/template" "github.com/portainer/portainer/api/bolt/template"
"github.com/portainer/portainer/api/bolt/user" "github.com/portainer/portainer/api/bolt/user"
@ -21,64 +23,70 @@ import (
type ( type (
// Migrator defines a service to migrate data after a Portainer version update. // Migrator defines a service to migrate data after a Portainer version update.
Migrator struct { Migrator struct {
currentDBVersion int currentDBVersion int
db *bolt.DB db *bolt.DB
endpointGroupService *endpointgroup.Service endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service endpointService *endpoint.Service
extensionService *extension.Service endpointRelationService *endpointrelation.Service
registryService *registry.Service extensionService *extension.Service
resourceControlService *resourcecontrol.Service registryService *registry.Service
roleService *role.Service resourceControlService *resourcecontrol.Service
scheduleService *schedule.Service roleService *role.Service
settingsService *settings.Service scheduleService *schedule.Service
stackService *stack.Service settingsService *settings.Service
teamMembershipService *teammembership.Service stackService *stack.Service
templateService *template.Service tagService *tag.Service
userService *user.Service teamMembershipService *teammembership.Service
versionService *version.Service templateService *template.Service
fileService portainer.FileService userService *user.Service
versionService *version.Service
fileService portainer.FileService
} }
// Parameters represents the required parameters to create a new Migrator instance. // Parameters represents the required parameters to create a new Migrator instance.
Parameters struct { Parameters struct {
DB *bolt.DB DB *bolt.DB
DatabaseVersion int DatabaseVersion int
EndpointGroupService *endpointgroup.Service EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service EndpointService *endpoint.Service
ExtensionService *extension.Service EndpointRelationService *endpointrelation.Service
RegistryService *registry.Service ExtensionService *extension.Service
ResourceControlService *resourcecontrol.Service RegistryService *registry.Service
RoleService *role.Service ResourceControlService *resourcecontrol.Service
ScheduleService *schedule.Service RoleService *role.Service
SettingsService *settings.Service ScheduleService *schedule.Service
StackService *stack.Service SettingsService *settings.Service
TeamMembershipService *teammembership.Service StackService *stack.Service
TemplateService *template.Service TagService *tag.Service
UserService *user.Service TeamMembershipService *teammembership.Service
VersionService *version.Service TemplateService *template.Service
FileService portainer.FileService UserService *user.Service
VersionService *version.Service
FileService portainer.FileService
} }
) )
// NewMigrator creates a new Migrator. // NewMigrator creates a new Migrator.
func NewMigrator(parameters *Parameters) *Migrator { func NewMigrator(parameters *Parameters) *Migrator {
return &Migrator{ return &Migrator{
db: parameters.DB, db: parameters.DB,
currentDBVersion: parameters.DatabaseVersion, currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService, endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService, endpointService: parameters.EndpointService,
extensionService: parameters.ExtensionService, endpointRelationService: parameters.EndpointRelationService,
registryService: parameters.RegistryService, extensionService: parameters.ExtensionService,
resourceControlService: parameters.ResourceControlService, registryService: parameters.RegistryService,
roleService: parameters.RoleService, resourceControlService: parameters.ResourceControlService,
scheduleService: parameters.ScheduleService, roleService: parameters.RoleService,
settingsService: parameters.SettingsService, scheduleService: parameters.ScheduleService,
teamMembershipService: parameters.TeamMembershipService, settingsService: parameters.SettingsService,
templateService: parameters.TemplateService, tagService: parameters.TagService,
stackService: parameters.StackService, teamMembershipService: parameters.TeamMembershipService,
userService: parameters.UserService, templateService: parameters.TemplateService,
versionService: parameters.VersionService, stackService: parameters.StackService,
fileService: parameters.FileService, userService: parameters.UserService,
versionService: parameters.VersionService,
fileService: parameters.FileService,
} }
} }
@ -301,5 +309,18 @@ func (m *Migrator) Migrate() error {
} }
} }
// Portainer 1.24.0
if m.currentDBVersion < 23 {
err := m.updateTagsToDBVersion23()
if err != nil {
return err
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion) return m.versionService.StoreDBVersion(portainer.DBVersion)
} }

View File

@ -52,6 +52,19 @@ func (service *Service) Tags() ([]portainer.Tag, error) {
return tags, err return tags, err
} }
// Tag returns a tag by ID.
func (service *Service) Tag(ID portainer.TagID) (*portainer.Tag, error) {
var tag portainer.Tag
identifier := internal.Itob(int(ID))
err := internal.GetObject(service.db, BucketName, identifier, &tag)
if err != nil {
return nil, err
}
return &tag, nil
}
// CreateTag creates a new tag. // CreateTag creates a new tag.
func (service *Service) CreateTag(tag *portainer.Tag) error { func (service *Service) CreateTag(tag *portainer.Tag) error {
return service.db.Update(func(tx *bolt.Tx) error { return service.db.Update(func(tx *bolt.Tx) error {
@ -69,6 +82,12 @@ func (service *Service) CreateTag(tag *portainer.Tag) error {
}) })
} }
// UpdateTag updates a tag.
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.db, BucketName, identifier, tag)
}
// DeleteTag deletes a tag. // DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error { func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID)) identifier := internal.Itob(int(ID))

View File

@ -179,7 +179,7 @@ func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tun
} }
endpointURL := endpoint.URL endpointURL := endpoint.URL
endpoint.URL = fmt.Sprintf("tcp://localhost:%d", tunnelPort) endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
snapshot, err := service.snapshotter.CreateSnapshot(endpoint) snapshot, err := service.snapshotter.CreateSnapshot(endpoint)
if err != nil { if err != nil {
return err return err

View File

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"log"
"time" "time"
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
@ -38,8 +39,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(), ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints (deprecated)").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(), NoAuth: kingpin.Flag("no-auth", "Disable authentication (deprecated)").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(), NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(), TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
@ -49,15 +50,15 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(), SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(), SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(), SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(), SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source (deprecated)").Default(defaultSyncInterval).String(),
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots").Default(defaultSnapshot).Bool(), Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots (deprecated)").Default(defaultSnapshot).Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(), SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(), AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(), AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')), Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(), Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(), Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
TemplateFile: kingpin.Flag("template-file", "Path to the templates (app) definitions on the filesystem").Default(defaultTemplateFile).String(), TemplateFile: kingpin.Flag("template-file", "Path to the App templates definitions on the filesystem (deprecated)").Default(defaultTemplateFile).String(),
} }
kingpin.Parse() kingpin.Parse()
@ -76,6 +77,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
// ValidateFlags validates the values of the flags. // ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error { func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" { if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal return errEndpointExcludeExternal
} }
@ -116,6 +119,28 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
return nil return nil
} }
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.ExternalEndpoints != "" {
log.Println("Warning: the --external-endpoint flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.SyncInterval != defaultSyncInterval {
log.Println("Warning: the --sync-interval flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.NoAuth {
log.Println("Warning: the --no-auth flag is deprecated and will likely be removed in a future version of Portainer.")
}
if !*flags.Snapshot {
log.Println("Warning: the --no-snapshot flag is deprecated and will likely be removed in a future version of Portainer.")
}
if *flags.TemplateFile != "" {
log.Println("Warning: the --template-file flag is deprecated and will likely be removed in a future version of Portainer.")
}
}
func validateEndpointURL(endpointURL string) error { func validateEndpointURL(endpointURL string) error {
if endpointURL != "" { if endpointURL != "" {
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") { if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {

View File

@ -259,7 +259,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
LogoURL: *flags.Logo, LogoURL: *flags.Logo,
AuthenticationMethod: portainer.AuthenticationInternal, AuthenticationMethod: portainer.AuthenticationInternal,
LDAPSettings: portainer.LDAPSettings{ LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true, AnonymousMode: true,
AutoCreateUsers: true, AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{}, TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{ SearchSettings: []portainer.LDAPSearchSettings{
@ -397,7 +397,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
UserAccessPolicies: portainer.UserAccessPolicies{}, UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{}, Extensions: []portainer.EndpointExtension{},
Tags: []string{}, TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp, Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{}, Snapshots: []portainer.Snapshot{},
} }
@ -440,7 +440,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
UserAccessPolicies: portainer.UserAccessPolicies{}, UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{}, Extensions: []portainer.EndpointExtension{},
Tags: []string{}, TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp, Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{}, Snapshots: []portainer.Snapshot{},
} }
@ -651,44 +651,47 @@ func main() {
} }
var server portainer.Server = &http.Server{ var server portainer.Server = &http.Server{
ReverseTunnelService: reverseTunnelService, ReverseTunnelService: reverseTunnelService,
Status: applicationStatus, Status: applicationStatus,
BindAddress: *flags.Addr, BindAddress: *flags.Addr,
AssetsPath: *flags.Assets, AssetsPath: *flags.Assets,
AuthDisabled: *flags.NoAuth, AuthDisabled: *flags.NoAuth,
EndpointManagement: endpointManagement, EndpointManagement: endpointManagement,
RoleService: store.RoleService, RoleService: store.RoleService,
UserService: store.UserService, UserService: store.UserService,
TeamService: store.TeamService, TeamService: store.TeamService,
TeamMembershipService: store.TeamMembershipService, TeamMembershipService: store.TeamMembershipService,
EndpointService: store.EndpointService, EdgeGroupService: store.EdgeGroupService,
EndpointGroupService: store.EndpointGroupService, EdgeStackService: store.EdgeStackService,
ExtensionService: store.ExtensionService, EndpointService: store.EndpointService,
ResourceControlService: store.ResourceControlService, EndpointGroupService: store.EndpointGroupService,
SettingsService: store.SettingsService, EndpointRelationService: store.EndpointRelationService,
RegistryService: store.RegistryService, ExtensionService: store.ExtensionService,
DockerHubService: store.DockerHubService, ResourceControlService: store.ResourceControlService,
StackService: store.StackService, SettingsService: store.SettingsService,
ScheduleService: store.ScheduleService, RegistryService: store.RegistryService,
TagService: store.TagService, DockerHubService: store.DockerHubService,
TemplateService: store.TemplateService, StackService: store.StackService,
WebhookService: store.WebhookService, ScheduleService: store.ScheduleService,
SwarmStackManager: swarmStackManager, TagService: store.TagService,
ComposeStackManager: composeStackManager, TemplateService: store.TemplateService,
ExtensionManager: extensionManager, WebhookService: store.WebhookService,
CryptoService: cryptoService, SwarmStackManager: swarmStackManager,
JWTService: jwtService, ComposeStackManager: composeStackManager,
FileService: fileService, ExtensionManager: extensionManager,
LDAPService: ldapService, CryptoService: cryptoService,
GitService: gitService, JWTService: jwtService,
SignatureService: digitalSignatureService, FileService: fileService,
JobScheduler: jobScheduler, LDAPService: ldapService,
Snapshotter: snapshotter, GitService: gitService,
SSL: *flags.SSL, SignatureService: digitalSignatureService,
SSLCert: *flags.SSLCert, JobScheduler: jobScheduler,
SSLKey: *flags.SSLKey, Snapshotter: snapshotter,
DockerClientFactory: clientFactory, SSL: *flags.SSL,
JobService: jobService, SSLCert: *flags.SSLCert,
SSLKey: *flags.SSLKey,
DockerClientFactory: clientFactory,
JobService: jobService,
} }
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)

View File

@ -81,7 +81,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portain
} }
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID) tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpointURL := fmt.Sprintf("http://localhost:%d", tunnel.Port) endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
return client.NewClientWithOpts( return client.NewClientWithOpts(
client.WithHost(endpointURL), client.WithHost(endpointURL),

54
api/edgegroup.go Normal file
View File

@ -0,0 +1,54 @@
package portainer
// EdgeGroupRelatedEndpoints returns a list of endpoints related to this Edge group
func EdgeGroupRelatedEndpoints(edgeGroup *EdgeGroup, endpoints []Endpoint, endpointGroups []EndpointGroup) []EndpointID {
if !edgeGroup.Dynamic {
return edgeGroup.Endpoints
}
endpointIDs := []EndpointID{}
for _, endpoint := range endpoints {
if endpoint.Type != EdgeAgentEnvironment {
continue
}
var endpointGroup EndpointGroup
for _, group := range endpointGroups {
if endpoint.GroupID == group.ID {
endpointGroup = group
break
}
}
if edgeGroupRelatedToEndpoint(edgeGroup, &endpoint, &endpointGroup) {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
return endpointIDs
}
// edgeGroupRelatedToEndpoint returns true is edgeGroup is associated with endpoint
func edgeGroupRelatedToEndpoint(edgeGroup *EdgeGroup, endpoint *Endpoint, endpointGroup *EndpointGroup) bool {
if !edgeGroup.Dynamic {
for _, endpointID := range edgeGroup.Endpoints {
if endpoint.ID == endpointID {
return true
}
}
return false
}
endpointTags := TagSet(endpoint.TagIDs)
if endpointGroup.TagIDs != nil {
endpointTags = TagUnion(endpointTags, TagSet(endpointGroup.TagIDs))
}
edgeGroupTags := TagSet(edgeGroup.TagIDs)
if edgeGroup.PartialMatch {
intersection := TagIntersection(endpointTags, edgeGroupTags)
return len(intersection) != 0
}
return TagContains(edgeGroupTags, endpointTags)
}

27
api/edgestack.go Normal file
View File

@ -0,0 +1,27 @@
package portainer
import "errors"
// EdgeStackRelatedEndpoints returns a list of endpoints related to this Edge stack
func EdgeStackRelatedEndpoints(edgeGroupIDs []EdgeGroupID, endpoints []Endpoint, endpointGroups []EndpointGroup, edgeGroups []EdgeGroup) ([]EndpointID, error) {
edgeStackEndpoints := []EndpointID{}
for _, edgeGroupID := range edgeGroupIDs {
var edgeGroup *EdgeGroup
for _, group := range edgeGroups {
if group.ID == edgeGroupID {
edgeGroup = &group
break
}
}
if edgeGroup == nil {
return nil, errors.New("Edge group was not found")
}
edgeStackEndpoints = append(edgeStackEndpoints, EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)...)
}
return edgeStackEndpoints, nil
}

25
api/endpoint.go Normal file
View File

@ -0,0 +1,25 @@
package portainer
// EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Endpoint
func EndpointRelatedEdgeStacks(endpoint *Endpoint, endpointGroup *EndpointGroup, edgeGroups []EdgeGroup, edgeStacks []EdgeStack) []EdgeStackID {
relatedEdgeGroupsSet := map[EdgeGroupID]bool{}
for _, edgeGroup := range edgeGroups {
if edgeGroupRelatedToEndpoint(&edgeGroup, endpoint, endpointGroup) {
relatedEdgeGroupsSet[edgeGroup.ID] = true
}
}
relatedEdgeStacks := []EdgeStackID{}
for _, edgeStack := range edgeStacks {
for _, edgeGroupID := range edgeStack.EdgeGroups {
if relatedEdgeGroupsSet[edgeGroupID] {
relatedEdgeStacks = append(relatedEdgeStacks, edgeStack.ID)
break
}
}
}
return relatedEdgeStacks
}

View File

@ -123,7 +123,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
endpointURL := endpoint.URL endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentEnvironment { if endpoint.Type == portainer.EdgeAgentEnvironment {
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port) endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
} }
args = append(args, "-H", endpointURL) args = append(args, "-H", endpointURL)

View File

@ -29,6 +29,8 @@ const (
ComposeStorePath = "compose" ComposeStorePath = "compose"
// ComposeFileDefaultName represents the default name of a compose file. // ComposeFileDefaultName represents the default name of a compose file.
ComposeFileDefaultName = "docker-compose.yml" ComposeFileDefaultName = "docker-compose.yml"
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
EdgeStackStorePath = "edge_stacks"
// PrivateKeyFile represents the name on disk of the file containing the private key. // PrivateKeyFile represents the name on disk of the file containing the private key.
PrivateKeyFile = "portainer.key" PrivateKeyFile = "portainer.key"
// PublicKeyFile represents the name on disk of the file containing the public key. // PublicKeyFile represents the name on disk of the file containing the public key.
@ -121,6 +123,32 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
return path.Join(service.fileStorePath, stackStorePath), nil return path.Join(service.fileStorePath, stackStorePath), nil
} }
// GetEdgeStackProjectPath returns the absolute path on the FS for a edge stack based
// on its identifier.
func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string {
return path.Join(service.fileStorePath, EdgeStackStorePath, edgeStackIdentifier)
}
// StoreEdgeStackFileFromBytes creates a subfolder in the EdgeStackStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) {
stackStorePath := path.Join(EdgeStackStorePath, edgeStackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := path.Join(stackStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, stackStorePath), nil
}
// StoreRegistryManagementFileFromBytes creates a subfolder in the // StoreRegistryManagementFileFromBytes creates a subfolder in the
// ExtensionRegistryManagementStorePath and stores a new file from bytes. // ExtensionRegistryManagementStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored. // It returns the path to the folder where the file is stored.

View File

@ -0,0 +1,104 @@
package edgegroups
import (
"github.com/portainer/portainer/api"
)
type endpointSetType map[portainer.EndpointID]bool
func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatch bool) ([]portainer.EndpointID, error) {
if len(tagIDs) == 0 {
return []portainer.EndpointID{}, nil
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return nil, err
}
groupEndpoints := mapEndpointGroupToEndpoints(endpoints)
tags := []portainer.Tag{}
for _, tagID := range tagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return nil, err
}
tags = append(tags, *tag)
}
setsOfEndpoints := mapTagsToEndpoints(tags, groupEndpoints)
var endpointSet endpointSetType
if partialMatch {
endpointSet = setsUnion(setsOfEndpoints)
} else {
endpointSet = setsIntersection(setsOfEndpoints)
}
results := []portainer.EndpointID{}
for _, endpoint := range endpoints {
if _, ok := endpointSet[endpoint.ID]; ok && endpoint.Type == portainer.EdgeAgentEnvironment {
results = append(results, endpoint.ID)
}
}
return results, nil
}
func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType {
groupEndpoints := map[portainer.EndpointGroupID]endpointSetType{}
for _, endpoint := range endpoints {
groupID := endpoint.GroupID
if groupEndpoints[groupID] == nil {
groupEndpoints[groupID] = endpointSetType{}
}
groupEndpoints[groupID][endpoint.ID] = true
}
return groupEndpoints
}
func mapTagsToEndpoints(tags []portainer.Tag, groupEndpoints map[portainer.EndpointGroupID]endpointSetType) []endpointSetType {
sets := []endpointSetType{}
for _, tag := range tags {
set := tag.Endpoints
for groupID := range tag.EndpointGroups {
for endpointID := range groupEndpoints[groupID] {
set[endpointID] = true
}
}
sets = append(sets, set)
}
return sets
}
func setsIntersection(sets []endpointSetType) endpointSetType {
if len(sets) == 0 {
return endpointSetType{}
}
intersectionSet := sets[0]
for _, set := range sets {
for endpointID := range intersectionSet {
if !set[endpointID] {
delete(intersectionSet, endpointID)
}
}
}
return intersectionSet
}
func setsUnion(sets []endpointSetType) endpointSetType {
unionSet := endpointSetType{}
for _, set := range sets {
for endpointID := range set {
unionSet[endpointID] = true
}
}
return unionSet
}

View File

@ -0,0 +1,83 @@
package edgegroups
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type edgeGroupCreatePayload struct {
Name string
Dynamic bool
TagIDs []portainer.TagID
Endpoints []portainer.EndpointID
PartialMatch bool
}
func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid Edge group name")
}
if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) {
return portainer.Error("TagIDs is mandatory for a dynamic Edge group")
}
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
return portainer.Error("Endpoints is mandatory for a static Edge group")
}
return nil
}
func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload edgeGroupCreatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
for _, edgeGroup := range edgeGroups {
if edgeGroup.Name == payload.Name {
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")}
}
}
edgeGroup := &portainer.EdgeGroup{
Name: payload.Name,
Dynamic: payload.Dynamic,
TagIDs: []portainer.TagID{},
Endpoints: []portainer.EndpointID{},
PartialMatch: payload.PartialMatch,
}
if edgeGroup.Dynamic {
edgeGroup.TagIDs = payload.TagIDs
} else {
endpointIDs := []portainer.EndpointID{}
for _, endpointID := range payload.Endpoints {
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
}
if endpoint.Type == portainer.EdgeAgentEnvironment {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
edgeGroup.Endpoints = endpointIDs
}
err = handler.EdgeGroupService.CreateEdgeGroup(edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Edge group inside the database", err}
}
return response.JSON(w, edgeGroup)
}

View File

@ -0,0 +1,45 @@
package edgegroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
}
_, err = handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err}
}
for _, edgeStack := range edgeStacks {
for _, groupID := range edgeStack.EdgeGroups {
if groupID == portainer.EdgeGroupID(edgeGroupID) {
return &httperror.HandlerError{http.StatusForbidden, "Edge group is used by an Edge stack", portainer.Error("Edge group is used by an Edge stack")}
}
}
}
err = handler.EdgeGroupService.DeleteEdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge group from the database", err}
}
return response.Empty(w)
}

View File

@ -0,0 +1,35 @@
package edgegroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
}
edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
}
if edgeGroup.Dynamic {
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err}
}
edgeGroup.Endpoints = endpoints
}
return response.JSON(w, edgeGroup)
}

View File

@ -0,0 +1,55 @@
package edgegroups
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type decoratedEdgeGroup struct {
portainer.EdgeGroup
HasEdgeStack bool `json:"HasEdgeStack"`
}
func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err}
}
usedEdgeGroups := make(map[portainer.EdgeGroupID]bool)
for _, stack := range edgeStacks {
for _, groupID := range stack.EdgeGroups {
usedEdgeGroups[groupID] = true
}
}
decoratedEdgeGroups := []decoratedEdgeGroup{}
for _, orgEdgeGroup := range edgeGroups {
edgeGroup := decoratedEdgeGroup{
EdgeGroup: orgEdgeGroup,
}
if edgeGroup.Dynamic {
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err}
}
edgeGroup.Endpoints = endpoints
}
edgeGroup.HasEdgeStack = usedEdgeGroups[edgeGroup.ID]
decoratedEdgeGroups = append(decoratedEdgeGroups, edgeGroup)
}
return response.JSON(w, decoratedEdgeGroups)
}

View File

@ -0,0 +1,154 @@
package edgegroups
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type edgeGroupUpdatePayload struct {
Name string
Dynamic bool
TagIDs []portainer.TagID
Endpoints []portainer.EndpointID
PartialMatch *bool
}
func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid Edge group name")
}
if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) {
return portainer.Error("TagIDs is mandatory for a dynamic Edge group")
}
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
return portainer.Error("Endpoints is mandatory for a static Edge group")
}
return nil
}
func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
}
var payload edgeGroupUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
}
if payload.Name != "" {
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
}
for _, edgeGroup := range edgeGroups {
if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) {
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")}
}
}
edgeGroup.Name = payload.Name
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
oldRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
edgeGroup.Dynamic = payload.Dynamic
if edgeGroup.Dynamic {
edgeGroup.TagIDs = payload.TagIDs
} else {
endpointIDs := []portainer.EndpointID{}
for _, endpointID := range payload.Endpoints {
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
}
if endpoint.Type == portainer.EdgeAgentEnvironment {
endpointIDs = append(endpointIDs, endpoint.ID)
}
}
edgeGroup.Endpoints = endpointIDs
}
if payload.PartialMatch != nil {
edgeGroup.PartialMatch = *payload.PartialMatch
}
err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge group changes inside the database", err}
}
newRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...)
for _, endpointID := range endpointsToUpdate {
err = handler.updateEndpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Endpoint relation changes inside the database", err}
}
}
return response.JSON(w, edgeGroup)
}
func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return err
}
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
return err
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return err
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return err
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return err
}
edgeStackSet := map[portainer.EdgeStackID]bool{}
endpointEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
for _, edgeStackID := range endpointEdgeStacks {
edgeStackSet[edgeStackID] = true
}
relation.EdgeStacks = edgeStackSet
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, relation)
}

View File

@ -0,0 +1,39 @@
package edgegroups
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
TagService portainer.TagService
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/edge_groups",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupCreate))).Methods(http.MethodPost)
h.Handle("/edge_groups",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupList))).Methods(http.MethodGet)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupInspect))).Methods(http.MethodGet)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupUpdate))).Methods(http.MethodPut)
h.Handle("/edge_groups/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupDelete))).Methods(http.MethodDelete)
return h
}

View File

@ -0,0 +1,289 @@
package edgestacks
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
// POST request on /api/endpoint_groups
func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
method, err := request.RetrieveQueryParameter(r, "method", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
}
edgeStack, err := handler.createSwarmStack(method, r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Edge stack", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
for _, endpointID := range relatedEndpoints {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
relation.EdgeStacks[edgeStack.ID] = true
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
}
return response.JSON(w, edgeStack)
}
func (handler *Handler) createSwarmStack(method string, r *http.Request) (*portainer.EdgeStack, error) {
switch method {
case "string":
return handler.createSwarmStackFromFileContent(r)
case "repository":
return handler.createSwarmStackFromGitRepository(r)
case "file":
return handler.createSwarmStackFromFileUpload(r)
}
return nil, errors.New("Invalid value for query parameter: method. Value must be one of: string, repository or file")
}
type swarmStackFromFileContentPayload struct {
Name string
StackFileContent string
EdgeGroups []portainer.EdgeGroupID
}
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid stack name")
}
if govalidator.IsNull(payload.StackFileContent) {
return portainer.Error("Invalid stack file content")
}
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
}
return nil
}
func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*portainer.EdgeStack, error) {
var payload swarmStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return nil, err
}
err = handler.validateUniqueName(payload.Name)
if err != nil {
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
EntryPoint: filesystem.ComposeFileDefaultName,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return nil, err
}
stack.ProjectPath = projectPath
err = handler.EdgeStackService.CreateEdgeStack(stack)
if err != nil {
return nil, err
}
return stack, nil
}
type swarmStackFromGitRepositoryPayload struct {
Name string
RepositoryURL string
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
ComposeFilePathInRepository string
EdgeGroups []portainer.EdgeGroupID
}
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid stack name")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
}
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
}
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
}
return nil
}
func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*portainer.EdgeStack, error) {
var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return nil, err
}
err = handler.validateUniqueName(payload.Name)
if err != nil {
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
EntryPoint: payload.ComposeFilePathInRepository,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1,
}
projectPath := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(stack.ID)))
stack.ProjectPath = projectPath
gitCloneParams := &cloneRepositoryParameters{
url: payload.RepositoryURL,
referenceName: payload.RepositoryReferenceName,
path: projectPath,
authentication: payload.RepositoryAuthentication,
username: payload.RepositoryUsername,
password: payload.RepositoryPassword,
}
err = handler.cloneGitRepository(gitCloneParams)
if err != nil {
return nil, err
}
err = handler.EdgeStackService.CreateEdgeStack(stack)
if err != nil {
return nil, err
}
return stack, nil
}
type swarmStackFromFileUploadPayload struct {
Name string
StackFileContent []byte
EdgeGroups []portainer.EdgeGroupID
}
func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error {
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
if err != nil {
return portainer.Error("Invalid stack name")
}
payload.Name = name
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
if err != nil {
return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
}
payload.StackFileContent = composeFileContent
var edgeGroups []portainer.EdgeGroupID
err = request.RetrieveMultiPartFormJSONValue(r, "EdgeGroups", &edgeGroups, false)
if err != nil || len(edgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
}
payload.EdgeGroups = edgeGroups
return nil
}
func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portainer.EdgeStack, error) {
payload := &swarmStackFromFileUploadPayload{}
err := payload.Validate(r)
if err != nil {
return nil, err
}
err = handler.validateUniqueName(payload.Name)
if err != nil {
return nil, err
}
stackID := handler.EdgeStackService.GetNextIdentifier()
stack := &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: payload.Name,
EntryPoint: filesystem.ComposeFileDefaultName,
CreationDate: time.Now().Unix(),
EdgeGroups: payload.EdgeGroups,
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
Version: 1,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return nil, err
}
stack.ProjectPath = projectPath
err = handler.EdgeStackService.CreateEdgeStack(stack)
if err != nil {
return nil, err
}
return stack, nil
}
func (handler *Handler) validateUniqueName(name string) error {
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return err
}
for _, stack := range edgeStacks {
if strings.EqualFold(stack.Name, name) {
return portainer.Error("Edge stack name must be unique")
}
}
return nil
}

View File

@ -0,0 +1,62 @@
package edgestacks
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
err = handler.EdgeStackService.DeleteEdgeStack(portainer.EdgeStackID(edgeStackID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the edge stack from the database", err}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
for _, endpointID := range relatedEndpoints {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
delete(relation.EdgeStacks, edgeStack.ID)
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
}
return response.Empty(w)
}

View File

@ -0,0 +1,37 @@
package edgestacks
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type stackFileResponse struct {
StackFileContent string `json:"StackFileContent"`
}
// GET request on /api/edge_stacks/:id/file
func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}
return response.JSON(w, &stackFileResponse{StackFileContent: string(stackFileContent)})
}

View File

@ -0,0 +1,26 @@
package edgestacks
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
return response.JSON(w, edgeStack)
}

View File

@ -0,0 +1,17 @@
package edgestacks
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
)
func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
return response.JSON(w, edgeStacks)
}

View File

@ -0,0 +1,76 @@
package edgestacks
import (
"net/http"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
)
type updateStatusPayload struct {
Error string
Status *portainer.EdgeStackStatusType
EndpointID *portainer.EndpointID
}
func (payload *updateStatusPayload) Validate(r *http.Request) error {
if payload.Status == nil {
return portainer.Error("Invalid status")
}
if payload.EndpointID == nil {
return portainer.Error("Invalid EndpointID")
}
if *payload.Status == portainer.StatusError && govalidator.IsNull(payload.Error) {
return portainer.Error("Error message is mandatory when status is error")
}
return nil
}
func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
var payload updateStatusPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(*payload.EndpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
stack.Status[*payload.EndpointID] = portainer.EdgeStackStatus{
Type: *payload.Status,
Error: payload.Error,
EndpointID: *payload.EndpointID,
}
err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
}

View File

@ -0,0 +1,156 @@
package edgestacks
import (
"net/http"
"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/api"
)
type updateEdgeStackPayload struct {
StackFileContent string
Version *int
Prune *bool
EdgeGroups []portainer.EdgeGroupID
}
func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.StackFileContent) {
return portainer.Error("Invalid stack file content")
}
if payload.EdgeGroups != nil && len(payload.EdgeGroups) == 0 {
return portainer.Error("Edge Groups are mandatory for an Edge stack")
}
return nil
}
func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
}
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
var payload updateEdgeStackPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
if payload.EdgeGroups != nil {
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
}
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
}
oldRelated, err := portainer.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
}
newRelated, err := portainer.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
}
oldRelatedSet := EndpointSet(oldRelated)
newRelatedSet := EndpointSet(newRelated)
endpointsToRemove := map[portainer.EndpointID]bool{}
for endpointID := range oldRelatedSet {
if !newRelatedSet[endpointID] {
endpointsToRemove[endpointID] = true
}
}
for endpointID := range endpointsToRemove {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
delete(relation.EdgeStacks, stack.ID)
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
}
endpointsToAdd := map[portainer.EndpointID]bool{}
for endpointID := range newRelatedSet {
if !oldRelatedSet[endpointID] {
endpointsToAdd[endpointID] = true
}
}
for endpointID := range endpointsToAdd {
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
}
relation.EdgeStacks[stack.ID] = true
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
}
}
stack.EdgeGroups = payload.EdgeGroups
}
if payload.Prune != nil {
stack.Prune = *payload.Prune
}
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
}
if payload.Version != nil && *payload.Version != stack.Version {
stack.Version = *payload.Version
stack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
}
err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
}
return response.JSON(w, stack)
}
func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bool {
set := map[portainer.EndpointID]bool{}
for _, endpointID := range endpointIDs {
set[endpointID] = true
}
return set
}

View File

@ -0,0 +1,17 @@
package edgestacks
type cloneRepositoryParameters struct {
url string
referenceName string
path string
authentication bool
username string
password string
}
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
if parameters.authentication {
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
}
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
}

View File

@ -0,0 +1,46 @@
package edgestacks
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
FileService portainer.FileService
GitService portainer.GitService
}
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.Handle("/edge_stacks",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackCreate))).Methods(http.MethodPost)
h.Handle("/edge_stacks",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackList))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackInspect))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackUpdate))).Methods(http.MethodPut)
h.Handle("/edge_stacks/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackDelete))).Methods(http.MethodDelete)
h.Handle("/edge_stacks/{id}/file",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackFile))).Methods(http.MethodGet)
h.Handle("/edge_stacks/{id}/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
return h
}

View File

@ -0,0 +1,49 @@
package edgetemplates
import (
"encoding/json"
"log"
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
)
// GET request on /api/edgetemplates
func (handler *Handler) edgeTemplateList(w 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}
}
url := portainer.EdgeTemplatesURL
if settings.TemplatesURL != "" {
url = settings.TemplatesURL
}
var templateData []byte
templateData, err = client.Get(url, 0)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
}
var templates []portainer.Template
err = json.Unmarshal(templateData, &templates)
if err != nil {
log.Printf("[DEBUG] [http,edge,templates] [failed parsing edge templates] [body: %s]", templateData)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external templates", err}
}
filteredTemplates := []portainer.Template{}
for _, template := range templates {
if template.Type == portainer.EdgeStackTemplate {
filteredTemplates = append(filteredTemplates, template)
}
}
return response.JSON(w, filteredTemplates)
}

View File

@ -0,0 +1,31 @@
package edgetemplates
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/gorilla/mux"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle edge endpoint operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
SettingsService portainer.SettingsService
}
// NewHandler creates a handler to manage endpoint operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.Handle("/edge_templates",
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeTemplateList))).Methods(http.MethodGet)
return h
}

View File

@ -0,0 +1,60 @@
package endpointedge
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
)
type configResponse struct {
Prune bool
StackFileContent string
Name string
}
// GET request on api/endpoints/:id/edge/stacks/:stackId
func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "stackId")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
}
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
}
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}
return response.JSON(w, configResponse{
Prune: edgeStack.Prune,
StackFileContent: string(stackFileContent),
Name: edgeStack.Name,
})
}

View File

@ -0,0 +1,33 @@
package endpointedge
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/gorilla/mux"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle edge endpoint operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
EndpointService portainer.EndpointService
EdgeStackService portainer.EdgeStackService
FileService portainer.FileService
}
// NewHandler creates a handler to manage endpoint operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.Handle("/{id}/edge/stacks/{stackId}",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
return h
}

View File

@ -14,15 +14,15 @@ type endpointGroupCreatePayload struct {
Name string Name string
Description string Description string
AssociatedEndpoints []portainer.EndpointID AssociatedEndpoints []portainer.EndpointID
Tags []string TagIDs []portainer.TagID
} }
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error { func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) { if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid endpoint group name") return portainer.Error("Invalid endpoint group name")
} }
if payload.Tags == nil { if payload.TagIDs == nil {
payload.Tags = []string{} payload.TagIDs = []portainer.TagID{}
} }
return nil return nil
} }
@ -40,7 +40,7 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
Description: payload.Description, Description: payload.Description,
UserAccessPolicies: portainer.UserAccessPolicies{}, UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{},
Tags: payload.Tags, TagIDs: payload.TagIDs,
} }
err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup) err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup)
@ -63,10 +63,29 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
} }
err = handler.updateEndpointRelations(&endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
break break
} }
} }
} }
for _, tagID := range endpointGroup.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tag from the database", err}
}
tag.EndpointGroups[endpointGroup.ID] = true
err = handler.TagService.UpdateTag(tagID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
return response.JSON(w, endpointGroup) return response.JSON(w, endpointGroup)
} }

View File

@ -20,7 +20,7 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", portainer.ErrCannotRemoveDefaultGroup} return &httperror.HandlerError{http.StatusForbidden, "Unable to remove the default 'Unassigned' group", portainer.ErrCannotRemoveDefaultGroup}
} }
_, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrObjectNotFound { if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
} else if err != nil { } else if err != nil {
@ -46,6 +46,11 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
} }
err = handler.updateEndpointRelations(&endpoint, nil)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
} }
} }
@ -56,5 +61,19 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
} }
} }
for _, tagID := range endpointGroup.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tag from the database", err}
}
delete(tag.EndpointGroups, endpointGroup.ID)
err = handler.TagService.UpdateTag(tagID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
return response.Empty(w) return response.Empty(w)
} }

View File

@ -42,5 +42,10 @@ func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
} }
err = handler.updateEndpointRelations(endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
return response.Empty(w) return response.Empty(w)
} }

View File

@ -42,5 +42,10 @@ func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
} }
err = handler.updateEndpointRelations(endpoint, nil)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
return response.Empty(w) return response.Empty(w)
} }

View File

@ -13,7 +13,7 @@ import (
type endpointGroupUpdatePayload struct { type endpointGroupUpdatePayload struct {
Name string Name string
Description string Description string
Tags []string TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies
} }
@ -50,8 +50,44 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
endpointGroup.Description = payload.Description endpointGroup.Description = payload.Description
} }
if payload.Tags != nil { tagsChanged := false
endpointGroup.Tags = payload.Tags if payload.TagIDs != nil {
payloadTagSet := portainer.TagSet(payload.TagIDs)
endpointGroupTagSet := portainer.TagSet((endpointGroup.TagIDs))
union := portainer.TagUnion(payloadTagSet, endpointGroupTagSet)
intersection := portainer.TagIntersection(payloadTagSet, endpointGroupTagSet)
tagsChanged = len(union) > len(intersection)
if tagsChanged {
removeTags := portainer.TagDifference(endpointGroupTagSet, payloadTagSet)
for tagID := range removeTags {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err}
}
delete(tag.EndpointGroups, endpointGroup.ID)
err = handler.TagService.UpdateTag(tag.ID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
endpointGroup.TagIDs = payload.TagIDs
for _, tagID := range payload.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err}
}
tag.EndpointGroups[endpointGroup.ID] = true
err = handler.TagService.UpdateTag(tag.ID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
}
} }
updateAuthorizations := false updateAuthorizations := false
@ -77,5 +113,22 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
} }
} }
if tagsChanged {
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
if endpoint.GroupID == endpointGroup.ID {
err = handler.updateEndpointRelations(&endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
}
}
}
return response.JSON(w, endpointGroup) return response.JSON(w, endpointGroup)
} }

View File

@ -0,0 +1,42 @@
package endpointgroups
import portainer "github.com/portainer/portainer/api"
func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) error {
if endpoint.Type != portainer.EdgeAgentEnvironment {
return nil
}
if endpointGroup == nil {
unassignedGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(1))
if err != nil {
return err
}
endpointGroup = unassignedGroup
}
endpointRelation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
if err != nil {
return err
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return err
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return err
}
endpointStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
stacksSet := map[portainer.EdgeStackID]bool{}
for _, edgeStackID := range endpointStacks {
stacksSet[edgeStackID] = true
}
endpointRelation.EdgeStacks = stacksSet
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, endpointRelation)
}

View File

@ -12,9 +12,13 @@ import (
// Handler is the HTTP handler used to handle endpoint group operations. // Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct { type Handler struct {
*mux.Router *mux.Router
EndpointService portainer.EndpointService AuthorizationService *portainer.AuthorizationService
EndpointGroupService portainer.EndpointGroupService EdgeGroupService portainer.EdgeGroupService
AuthorizationService *portainer.AuthorizationService EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
TagService portainer.TagService
} }
// NewHandler creates a handler to manage endpoint group operations. // NewHandler creates a handler to manage endpoint group operations.

View File

@ -32,7 +32,7 @@ type endpointCreatePayload struct {
AzureApplicationID string AzureApplicationID string
AzureTenantID string AzureTenantID string
AzureAuthenticationKey string AzureAuthenticationKey string
Tags []string TagIDs []portainer.TagID
} }
func (payload *endpointCreatePayload) Validate(r *http.Request) error { func (payload *endpointCreatePayload) Validate(r *http.Request) error {
@ -54,14 +54,14 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
} }
payload.GroupID = groupID payload.GroupID = groupID
var tags []string var tagIDs []portainer.TagID
err = request.RetrieveMultiPartFormJSONValue(r, "Tags", &tags, true) err = request.RetrieveMultiPartFormJSONValue(r, "TagIds", &tagIDs, true)
if err != nil { if err != nil {
return portainer.Error("Invalid Tags parameter") return portainer.Error("Invalid TagIds parameter")
} }
payload.Tags = tags payload.TagIDs = tagIDs
if payload.Tags == nil { if payload.TagIDs == nil {
payload.Tags = make([]string, 0) payload.TagIDs = make([]portainer.TagID, 0)
} }
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true) useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
@ -146,6 +146,38 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
return endpointCreationError return endpointCreationError
} }
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group inside the database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
relationObject := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
if endpoint.Type == portainer.EdgeAgentEnvironment {
relatedEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
for _, stackID := range relatedEdgeStacks {
relationObject.EdgeStacks[stackID] = true
}
}
err = handler.EndpointRelationService.CreateEndpointRelation(relationObject)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the relation object inside the database", err}
}
return response.JSON(w, endpoint) return response.JSON(w, endpoint)
} }
@ -187,7 +219,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
TeamAccessPolicies: portainer.TeamAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{}, Extensions: []portainer.EndpointExtension{},
AzureCredentials: credentials, AzureCredentials: credentials,
Tags: payload.Tags, TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp, Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{}, Snapshots: []portainer.Snapshot{},
} }
@ -232,7 +264,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
AuthorizedUsers: []portainer.UserID{}, AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{}, AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{}, Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags, TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp, Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{}, Snapshots: []portainer.Snapshot{},
EdgeKey: edgeKey, EdgeKey: edgeKey,
@ -278,7 +310,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
UserAccessPolicies: portainer.UserAccessPolicies{}, UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{}, Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags, TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp, Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{}, Snapshots: []portainer.Snapshot{},
} }
@ -322,7 +354,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload)
UserAccessPolicies: portainer.UserAccessPolicies{}, UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{}, TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{}, Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags, TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp, Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{}, Snapshots: []portainer.Snapshot{},
} }
@ -377,6 +409,20 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
return handler.AuthorizationService.UpdateUsersAuthorizations() return handler.AuthorizationService.UpdateUsersAuthorizations()
} }
for _, tagID := range endpoint.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return err
}
tag.Endpoints[endpoint.ID] = true
err = handler.TagService.UpdateTag(tagID, tag)
if err != nil {
return err
}
}
return nil return nil
} }

View File

@ -50,5 +50,75 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
} }
} }
err = handler.EndpointRelationService.DeleteEndpointRelation(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint relation from the database", err}
}
for _, tagID := range endpoint.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find tag inside the database", err}
}
delete(tag.Endpoints, endpoint.ID)
err = handler.TagService.UpdateTag(tagID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag relation inside the database", err}
}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err}
}
for idx := range edgeGroups {
edgeGroup := &edgeGroups[idx]
endpointIdx := findEndpointIndex(edgeGroup.Endpoints, endpoint.ID)
if endpointIdx != -1 {
edgeGroup.Endpoints = removeElement(edgeGroup.Endpoints, endpointIdx)
err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge group", err}
}
}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
for idx := range edgeStacks {
edgeStack := &edgeStacks[idx]
if _, ok := edgeStack.Status[endpoint.ID]; ok {
delete(edgeStack.Status, endpoint.ID)
err = handler.EdgeStackService.UpdateEdgeStack(edgeStack.ID, edgeStack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update edge stack", err}
}
}
}
return response.Empty(w) return response.Empty(w)
} }
func findEndpointIndex(tags []portainer.EndpointID, searchEndpointID portainer.EndpointID) int {
for idx, tagID := range tags {
if searchEndpointID == tagID {
return idx
}
}
return -1
}
func removeElement(arr []portainer.EndpointID, index int) []portainer.EndpointID {
if index < 0 {
return arr
}
lastTagIdx := len(arr) - 1
arr[index] = arr[lastTagIdx]
return arr[:lastTagIdx]
}

View File

@ -5,7 +5,7 @@ import (
"strconv" "strconv"
"strings" "strings"
portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
@ -28,6 +28,15 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true) groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true) limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
endpointType, _ := request.RetrieveNumericQueryParameter(r, "type", true)
var tagIDs []portainer.TagID
request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true)
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
var endpointIDs []portainer.EndpointID
request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true)
endpointGroups, err := handler.EndpointGroupService.EndpointGroups() endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
if err != nil { if err != nil {
@ -46,12 +55,32 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext) filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
if endpointIDs != nil {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
}
if groupID != 0 { if groupID != 0 {
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID)) filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
} }
if search != "" { if search != "" {
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, search) tags, err := handler.TagService.Tags()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err}
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
}
if endpointType != 0 {
filteredEndpoints = filterEndpointsByType(filteredEndpoints, portainer.EndpointType(endpointType))
}
if tagIDs != nil {
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups, tagsPartialMatch)
} }
filteredEndpointCount := len(filteredEndpoints) filteredEndpointCount := len(filteredEndpoints)
@ -97,17 +126,17 @@ func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID po
return filteredEndpoints return filteredEndpoints
} }
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) []portainer.Endpoint { func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0) filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints { for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, searchCriteria) { if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint) filteredEndpoints = append(filteredEndpoints, endpoint)
continue continue
} }
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, searchCriteria) { if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint) filteredEndpoints = append(filteredEndpoints, endpoint)
} }
} }
@ -115,7 +144,7 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro
return filteredEndpoints return filteredEndpoints
} }
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria string) bool { func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) { if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true return true
} }
@ -129,8 +158,7 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" { } else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true return true
} }
for _, tag := range tags {
for _, tag := range endpoint.Tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) { if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true return true
} }
@ -139,14 +167,14 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st
return false return false
} }
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) bool { func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups { for _, group := range endpointGroups {
if group.ID == endpoint.GroupID { if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) { if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true return true
} }
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range group.Tags { for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) { if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true return true
} }
@ -156,3 +184,106 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou
return false return false
} }
func filterEndpointsByType(endpoints []portainer.Endpoint, endpointType portainer.EndpointType) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpoint.Type == endpointType {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
endpointMatched := false
if partialMatch {
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
} else {
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
}
if endpointMatched {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.EndpointGroup) portainer.EndpointGroup {
var endpointGroup portainer.EndpointGroup
for _, group := range groups {
if group.ID == groupID {
endpointGroup = group
break
}
}
return endpointGroup
}
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
tagSet := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
tagSet[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if tagSet[tagID] {
return true
}
}
for _, tagID := range endpointGroup.TagIDs {
if tagSet[tagID] {
return true
}
}
return false
}
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
missingTags := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
missingTags[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
for _, tagID := range endpointGroup.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
idsSet := make(map[portainer.EndpointID]bool)
for _, id := range ids {
idsSet[id] = true
}
for _, endpoint := range endpoints {
if idsSet[endpoint.ID] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}

View File

@ -1,7 +1,6 @@
package endpoints package endpoints
import ( import (
"errors"
"net/http" "net/http"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
@ -10,12 +9,18 @@ import (
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
) )
type stackStatusResponse struct {
ID portainer.EdgeStackID
Version int
}
type endpointStatusInspectResponse struct { type endpointStatusInspectResponse struct {
Status string `json:"status"` Status string `json:"status"`
Port int `json:"port"` Port int `json:"port"`
Schedules []portainer.EdgeSchedule `json:"schedules"` Schedules []portainer.EdgeSchedule `json:"schedules"`
CheckinInterval int `json:"checkin"` CheckinInterval int `json:"checkin"`
Credentials string `json:"credentials"` Credentials string `json:"credentials"`
Stacks []stackStatusResponse `json:"stacks"`
} }
// GET request on /api/endpoints/:id/status // GET request on /api/endpoints/:id/status
@ -32,20 +37,14 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
} }
if endpoint.Type != portainer.EdgeAgentEnvironment { err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
return &httperror.HandlerError{http.StatusInternalServerError, "Status unavailable for non Edge agent endpoints", errors.New("Status unavailable")} if err != nil {
} return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
if edgeIdentifier == "" {
return &httperror.HandlerError{http.StatusForbidden, "Missing Edge identifier", errors.New("missing Edge identifier")}
}
if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier {
return &httperror.HandlerError{http.StatusForbidden, "Invalid Edge identifier", errors.New("invalid Edge identifier")}
} }
if endpoint.EdgeID == "" { if endpoint.EdgeID == "" {
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
endpoint.EdgeID = edgeIdentifier endpoint.EdgeID = edgeIdentifier
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) err := handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
@ -73,5 +72,27 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID) handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID)
} }
relation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve relation object from the database", err}
}
edgeStacksStatus := []stackStatusResponse{}
for stackID := range relation.EdgeStacks {
stack, err := handler.EdgeStackService.EdgeStack(stackID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack from the database", err}
}
stackStatus := stackStatusResponse{
ID: stack.ID,
Version: stack.Version,
}
edgeStacksStatus = append(edgeStacksStatus, stackStatus)
}
statusResponse.Stacks = edgeStacksStatus
return response.JSON(w, statusResponse) return response.JSON(w, statusResponse)
} }

View File

@ -24,7 +24,7 @@ type endpointUpdatePayload struct {
AzureApplicationID *string AzureApplicationID *string
AzureTenantID *string AzureTenantID *string
AzureAuthenticationKey *string AzureAuthenticationKey *string
Tags []string TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies TeamAccessPolicies portainer.TeamAccessPolicies
} }
@ -69,12 +69,52 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.PublicURL = *payload.PublicURL endpoint.PublicURL = *payload.PublicURL
} }
groupIDChanged := false
if payload.GroupID != nil { if payload.GroupID != nil {
endpoint.GroupID = portainer.EndpointGroupID(*payload.GroupID) groupID := portainer.EndpointGroupID(*payload.GroupID)
groupIDChanged = groupID != endpoint.GroupID
endpoint.GroupID = groupID
} }
if payload.Tags != nil { tagsChanged := false
endpoint.Tags = payload.Tags if payload.TagIDs != nil {
payloadTagSet := portainer.TagSet(payload.TagIDs)
endpointTagSet := portainer.TagSet((endpoint.TagIDs))
union := portainer.TagUnion(payloadTagSet, endpointTagSet)
intersection := portainer.TagIntersection(payloadTagSet, endpointTagSet)
tagsChanged = len(union) > len(intersection)
if tagsChanged {
removeTags := portainer.TagDifference(endpointTagSet, payloadTagSet)
for tagID := range removeTags {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err}
}
delete(tag.Endpoints, endpoint.ID)
err = handler.TagService.UpdateTag(tag.ID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
endpoint.TagIDs = payload.TagIDs
for _, tagID := range payload.TagIDs {
tag, err := handler.TagService.Tag(tagID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag inside the database", err}
}
tag.Endpoints[endpoint.ID] = true
err = handler.TagService.UpdateTag(tag.ID, tag)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist tag changes inside the database", err}
}
}
}
} }
updateAuthorizations := false updateAuthorizations := false
@ -184,5 +224,41 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
} }
} }
if endpoint.Type == portainer.EdgeAgentEnvironment && (groupIDChanged || tagsChanged) {
relation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation inside the database", err}
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint group inside the database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
edgeStackSet := map[portainer.EdgeStackID]bool{}
endpointEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
for _, edgeStackID := range endpointEdgeStacks {
edgeStackSet[edgeStackID] = true
}
relation.EdgeStacks = edgeStackSet
err = handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, relation)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation changes inside the database", err}
}
}
return response.JSON(w, endpoint) return response.JSON(w, endpoint)
} }

View File

@ -29,15 +29,19 @@ type Handler struct {
*mux.Router *mux.Router
authorizeEndpointManagement bool authorizeEndpointManagement bool
requestBouncer *security.RequestBouncer requestBouncer *security.RequestBouncer
AuthorizationService *portainer.AuthorizationService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
FileService portainer.FileService FileService portainer.FileService
ProxyManager *proxy.Manager
Snapshotter portainer.Snapshotter
JobService portainer.JobService JobService portainer.JobService
ProxyManager *proxy.Manager
ReverseTunnelService portainer.ReverseTunnelService ReverseTunnelService portainer.ReverseTunnelService
SettingsService portainer.SettingsService SettingsService portainer.SettingsService
AuthorizationService *portainer.AuthorizationService Snapshotter portainer.Snapshotter
TagService portainer.TagService
} }
// NewHandler creates a handler to manage endpoint operations. // NewHandler creates a handler to manage endpoint operations.
@ -70,6 +74,5 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/status", h.Handle("/endpoints/{id}/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet) bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
return h return h
} }

View File

@ -4,6 +4,10 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgestacks"
"github.com/portainer/portainer/api/http/handler/edgetemplates"
"github.com/portainer/portainer/api/http/handler/endpointedge"
"github.com/portainer/portainer/api/http/handler/support" "github.com/portainer/portainer/api/http/handler/support"
"github.com/portainer/portainer/api/http/handler/schedules" "github.com/portainer/portainer/api/http/handler/schedules"
@ -37,6 +41,10 @@ import (
type Handler struct { type Handler struct {
AuthHandler *auth.Handler AuthHandler *auth.Handler
DockerHubHandler *dockerhub.Handler DockerHubHandler *dockerhub.Handler
EdgeGroupsHandler *edgegroups.Handler
EdgeStacksHandler *edgestacks.Handler
EdgeTemplatesHandler *edgetemplates.Handler
EndpointEdgeHandler *endpointedge.Handler
EndpointGroupHandler *endpointgroups.Handler EndpointGroupHandler *endpointgroups.Handler
EndpointHandler *endpoints.Handler EndpointHandler *endpoints.Handler
EndpointProxyHandler *endpointproxy.Handler EndpointProxyHandler *endpointproxy.Handler
@ -68,6 +76,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"): case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/edge_stacks"):
http.StripPrefix("/api", h.EdgeStacksHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/edge_groups"):
http.StripPrefix("/api", h.EdgeGroupsHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/edge_templates"):
http.StripPrefix("/api", h.EdgeTemplatesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"): case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"):
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/endpoints"): case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
@ -78,6 +92,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/azure/"): case strings.Contains(r.URL.Path, "/azure/"):
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/edge/"):
http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r)
default: default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
} }

View File

@ -16,6 +16,7 @@ type publicSettingsResponse struct {
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
ExternalTemplates bool `json:"ExternalTemplates"` ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"` OAuthLoginURI string `json:"OAuthLoginURI"`
} }
@ -34,6 +35,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
ExternalTemplates: false, ExternalTemplates: false,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI, settings.OAuthSettings.AuthorizationURI,

View File

@ -24,6 +24,7 @@ type settingsUpdatePayload struct {
SnapshotInterval *string SnapshotInterval *string
TemplatesURL *string TemplatesURL *string
EdgeAgentCheckinInterval *int EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
} }
func (payload *settingsUpdatePayload) Validate(r *http.Request) error { func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@ -109,6 +110,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
} }
if payload.EnableEdgeComputeFeatures != nil {
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
}
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
if err != nil { if err != nil {

View File

@ -9,7 +9,7 @@ import (
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
) )
// DELETE request on /api/stacks/:id?external=<external>&endpointId=<endpointId> // DELETE request on /api/stacks/:id?external=<external>&endpointId=<endpointId>
@ -21,9 +21,14 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err} return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
} }
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true) externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
if externalStack { if externalStack {
return handler.deleteExternalStack(r, w, stackID) return handler.deleteExternalStack(r, w, stackID, securityContext)
} }
id, err := strconv.Atoi(stackID) id, err := strconv.Atoi(stackID)
@ -68,11 +73,6 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
} }
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl) access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
@ -106,7 +106,38 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return response.Empty(w) return response.Empty(w)
} }
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string) *httperror.HandlerError { func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string, securityContext *security.RestrictedRequestContext) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
user, err := handler.UserService.User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
}
rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify if RBAC extension is loaded", err}
}
endpointResourceAccess := false
_, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
if ok {
endpointResourceAccess = true
}
if rbacExtension != nil {
if !securityContext.IsAdmin && !endpointResourceAccess {
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", portainer.ErrUnauthorized}
}
} else {
if !securityContext.IsAdmin {
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", portainer.ErrUnauthorized}
}
}
stack, err := handler.StackService.StackByName(stackName) stack, err := handler.StackService.StackByName(stackName)
if err != nil && err != portainer.ErrObjectNotFound { if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err}
@ -115,11 +146,6 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", portainer.ErrStackNotExternal} return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", portainer.ErrStackNotExternal}
} }
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound { if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}

View File

@ -12,7 +12,12 @@ import (
// Handler is the HTTP handler used to handle tag operations. // Handler is the HTTP handler used to handle tag operations.
type Handler struct { type Handler struct {
*mux.Router *mux.Router
TagService portainer.TagService TagService portainer.TagService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
} }
// NewHandler creates a handler to manage tag operations. // NewHandler creates a handler to manage tag operations.
@ -23,7 +28,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/tags", h.Handle("/tags",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost) bouncer.AdminAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
h.Handle("/tags", h.Handle("/tags",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet)
h.Handle("/tags/{id}", h.Handle("/tags/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete) bouncer.AdminAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)

View File

@ -41,7 +41,9 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe
} }
tag := &portainer.Tag{ tag := &portainer.Tag{
Name: payload.Name, Name: payload.Name,
EndpointGroups: map[portainer.EndpointGroupID]bool{},
Endpoints: map[portainer.EndpointID]bool{},
} }
err = handler.TagService.CreateTag(tag) err = handler.TagService.CreateTag(tag)

View File

@ -15,11 +15,126 @@ func (handler *Handler) tagDelete(w http.ResponseWriter, r *http.Request) *httpe
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid tag identifier route variable", err} return &httperror.HandlerError{http.StatusBadRequest, "Invalid tag identifier route variable", err}
} }
tagID := portainer.TagID(id)
err = handler.TagService.DeleteTag(portainer.TagID(id)) tag, err := handler.TagService.Tag(tagID)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a tag with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a tag with the specified identifier inside the database", err}
}
for endpointID := range tag.Endpoints {
endpoint, err := handler.EndpointService.Endpoint(endpointID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
}
tagIdx := findTagIndex(endpoint.TagIDs, tagID)
if tagIdx != -1 {
endpoint.TagIDs = removeElement(endpoint.TagIDs, tagIdx)
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
}
}
}
for endpointGroupID := range tag.EndpointGroups {
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpointGroupID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint group from the database", err}
}
tagIdx := findTagIndex(endpointGroup.TagIDs, tagID)
if tagIdx != -1 {
endpointGroup.TagIDs = removeElement(endpointGroup.TagIDs, tagIdx)
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err}
}
}
}
endpoints, err := handler.EndpointService.Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from the database", err}
}
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
}
for _, endpoint := range endpoints {
if (tag.Endpoints[endpoint.ID] || tag.EndpointGroups[endpoint.GroupID]) && endpoint.Type == portainer.EdgeAgentEnvironment {
err = handler.updateEndpointRelations(endpoint, edgeGroups, edgeStacks)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint relations in the database", err}
}
}
}
for idx := range edgeGroups {
edgeGroup := &edgeGroups[idx]
tagIdx := findTagIndex(edgeGroup.TagIDs, tagID)
if tagIdx != -1 {
edgeGroup.TagIDs = removeElement(edgeGroup.TagIDs, tagIdx)
err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint group", err}
}
}
}
err = handler.TagService.DeleteTag(tagID)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the tag from the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the tag from the database", err}
} }
return response.Empty(w) return response.Empty(w)
} }
func (handler *Handler) updateEndpointRelations(endpoint portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
endpointRelation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
if err != nil {
return err
}
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
if err != nil {
return err
}
endpointStacks := portainer.EndpointRelatedEdgeStacks(&endpoint, endpointGroup, edgeGroups, edgeStacks)
stacksSet := map[portainer.EdgeStackID]bool{}
for _, edgeStackID := range endpointStacks {
stacksSet[edgeStackID] = true
}
endpointRelation.EdgeStacks = stacksSet
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, endpointRelation)
}
func findTagIndex(tags []portainer.TagID, searchTagID portainer.TagID) int {
for idx, tagID := range tags {
if searchTagID == tagID {
return idx
}
}
return -1
}
func removeElement(arr []portainer.TagID, index int) []portainer.TagID {
if index < 0 {
return arr
}
lastTagIdx := len(arr) - 1
arr[index] = arr[lastTagIdx]
return arr[:lastTagIdx]
}

View File

@ -14,7 +14,7 @@ import (
func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
tunnel := handler.ReverseTunnelService.GetTunnelDetails(params.endpoint.ID) tunnel := handler.ReverseTunnelService.GetTunnelDetails(params.endpoint.ID)
endpointURL, err := url.Parse(fmt.Sprintf("http://localhost:%d", tunnel.Port)) endpointURL, err := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port))
if err != nil { if err != nil {
return err return err
} }

View File

@ -34,7 +34,7 @@ func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (
func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) { func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
if endpoint.Type == portainer.EdgeAgentEnvironment { if endpoint.Type == portainer.EdgeAgentEnvironment {
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpoint.URL = fmt.Sprintf("http://localhost:%d", tunnel.Port) endpoint.URL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
} }
endpointURL, err := url.Parse(endpoint.URL) endpointURL, err := url.Parse(endpoint.URL)

View File

@ -158,7 +158,7 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
return responseutils.RewriteResponse(response, responseObject, http.StatusOK) return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
} }
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || portainer.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) { if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || (resourceControl != nil && portainer.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
responseObject = decorateObject(responseObject, resourceControl) responseObject = decorateObject(responseObject, resourceControl)
return responseutils.RewriteResponse(response, responseObject, http.StatusOK) return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
} }

View File

@ -171,11 +171,13 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
switch { switch {
case strings.HasPrefix(requestPath, "/browse"): case strings.HasPrefix(requestPath, "/browse"):
// host file browser request
volumeIDParameter, found := r.URL.Query()["volumeID"] volumeIDParameter, found := r.URL.Query()["volumeID"]
if !found || len(volumeIDParameter) < 1 { if !found || len(volumeIDParameter) < 1 {
return transport.administratorOperation(r) return transport.administratorOperation(r)
} }
// volume browser request
return transport.restrictedResourceOperation(r, volumeIDParameter[0], portainer.VolumeResourceControl, true) return transport.restrictedResourceOperation(r, volumeIDParameter[0], portainer.VolumeResourceControl, true)
} }
@ -273,7 +275,7 @@ func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Re
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) { func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath { switch requestPath := request.URL.Path; requestPath {
case "/volumes/create": case "/volumes/create":
return transport.decorateGenericResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl) return transport.decorateVolumeResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
case "/volumes/prune": case "/volumes/prune":
return transport.administratorOperation(request) return transport.administratorOperation(request)
@ -443,10 +445,16 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
return nil, err return nil, err
} }
// Return access denied for all roles except endpoint-administrator if !settings.AllowVolumeBrowserForRegularUsers {
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList] if rbacExtension == nil {
if rbacExtension != nil && !settings.AllowVolumeBrowserForRegularUsers && !userCanBrowse { return responseutils.WriteAccessDeniedResponse()
return responseutils.WriteAccessDeniedResponse() }
// Return access denied for all roles except endpoint-administrator
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList]
if !userCanBrowse {
return responseutils.WriteAccessDeniedResponse()
}
} }
} }

View File

@ -2,12 +2,14 @@ package docker
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
) )
const ( const (
@ -87,3 +89,40 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec
func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} { func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} {
return responseutils.GetJSONObject(responseObject, "Labels") return responseutils.GetJSONObject(responseObject, "Labels")
} }
func (transport *Transport) decorateVolumeResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
volumeID := request.Header.Get("X-Portainer-VolumeName")
if volumeID != "" {
cli := transport.dockerClient
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
if agentTargetHeader != "" {
dockerClient, err := transport.dockerClientFactory.CreateClient(transport.endpoint, agentTargetHeader)
if err != nil {
return nil, err
}
defer dockerClient.Close()
cli = dockerClient
}
_, err = cli.VolumeInspect(context.Background(), volumeID)
if err == nil {
return nil, errors.New("a volume with the same name already exists")
}
}
response, err := transport.executeDockerRequest(request)
if err != nil {
return response, err
}
if response.StatusCode == http.StatusCreated {
err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID)
}
return response, err
}

View File

@ -1,6 +1,8 @@
package security package security
import ( import (
"errors"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
@ -143,6 +145,24 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp
return nil return nil
} }
// AuthorizedEdgeEndpointOperation verifies that the request was received from a valid Edge endpoint
func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
if endpoint.Type != portainer.EdgeAgentEnvironment {
return errors.New("Invalid endpoint type")
}
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
if edgeIdentifier == "" {
return errors.New("missing Edge identifier")
}
if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier {
return errors.New("invalid Edge identifier")
}
return nil
}
func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Request, endpoint *portainer.Endpoint) error { func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Request, endpoint *portainer.Endpoint) error {
tokenData, err := RetrieveTokenData(r) tokenData, err := RetrieveTokenData(r)
if err != nil { if err != nil {

View File

@ -3,11 +3,15 @@ package http
import ( import (
"time" "time"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgestacks"
"github.com/portainer/portainer/api/http/handler/edgetemplates"
"github.com/portainer/portainer/api/http/handler/endpointedge"
"github.com/portainer/portainer/api/http/handler/support" "github.com/portainer/portainer/api/http/handler/support"
"github.com/portainer/portainer/api/http/handler/roles" "github.com/portainer/portainer/api/http/handler/roles"
"github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/handler" "github.com/portainer/portainer/api/http/handler"
"github.com/portainer/portainer/api/http/handler/auth" "github.com/portainer/portainer/api/http/handler/auth"
@ -41,45 +45,48 @@ import (
// Server implements the portainer.Server interface // Server implements the portainer.Server interface
type Server struct { type Server struct {
BindAddress string BindAddress string
AssetsPath string AssetsPath string
AuthDisabled bool AuthDisabled bool
EndpointManagement bool EndpointManagement bool
Status *portainer.Status Status *portainer.Status
ReverseTunnelService portainer.ReverseTunnelService ReverseTunnelService portainer.ReverseTunnelService
ExtensionManager portainer.ExtensionManager ExtensionManager portainer.ExtensionManager
ComposeStackManager portainer.ComposeStackManager ComposeStackManager portainer.ComposeStackManager
CryptoService portainer.CryptoService CryptoService portainer.CryptoService
SignatureService portainer.DigitalSignatureService SignatureService portainer.DigitalSignatureService
JobScheduler portainer.JobScheduler JobScheduler portainer.JobScheduler
Snapshotter portainer.Snapshotter Snapshotter portainer.Snapshotter
RoleService portainer.RoleService RoleService portainer.RoleService
DockerHubService portainer.DockerHubService DockerHubService portainer.DockerHubService
EndpointService portainer.EndpointService EdgeGroupService portainer.EdgeGroupService
EndpointGroupService portainer.EndpointGroupService EdgeStackService portainer.EdgeStackService
FileService portainer.FileService EndpointService portainer.EndpointService
GitService portainer.GitService EndpointGroupService portainer.EndpointGroupService
JWTService portainer.JWTService EndpointRelationService portainer.EndpointRelationService
LDAPService portainer.LDAPService FileService portainer.FileService
ExtensionService portainer.ExtensionService GitService portainer.GitService
RegistryService portainer.RegistryService JWTService portainer.JWTService
ResourceControlService portainer.ResourceControlService LDAPService portainer.LDAPService
ScheduleService portainer.ScheduleService ExtensionService portainer.ExtensionService
SettingsService portainer.SettingsService RegistryService portainer.RegistryService
StackService portainer.StackService ResourceControlService portainer.ResourceControlService
SwarmStackManager portainer.SwarmStackManager ScheduleService portainer.ScheduleService
TagService portainer.TagService SettingsService portainer.SettingsService
TeamService portainer.TeamService StackService portainer.StackService
TeamMembershipService portainer.TeamMembershipService SwarmStackManager portainer.SwarmStackManager
TemplateService portainer.TemplateService TagService portainer.TagService
UserService portainer.UserService TeamService portainer.TeamService
WebhookService portainer.WebhookService TeamMembershipService portainer.TeamMembershipService
Handler *handler.Handler TemplateService portainer.TemplateService
SSL bool UserService portainer.UserService
SSLCert string WebhookService portainer.WebhookService
SSLKey string Handler *handler.Handler
DockerClientFactory *docker.ClientFactory SSL bool
JobService portainer.JobService SSLCert string
SSLKey string
DockerClientFactory *docker.ClientFactory
JobService portainer.JobService
} }
// Start starts the HTTP server // Start starts the HTTP server
@ -144,21 +151,54 @@ func (server *Server) Start() error {
var dockerHubHandler = dockerhub.NewHandler(requestBouncer) var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
dockerHubHandler.DockerHubService = server.DockerHubService dockerHubHandler.DockerHubService = server.DockerHubService
var edgeGroupsHandler = edgegroups.NewHandler(requestBouncer)
edgeGroupsHandler.EdgeGroupService = server.EdgeGroupService
edgeGroupsHandler.EdgeStackService = server.EdgeStackService
edgeGroupsHandler.EndpointService = server.EndpointService
edgeGroupsHandler.EndpointGroupService = server.EndpointGroupService
edgeGroupsHandler.EndpointRelationService = server.EndpointRelationService
edgeGroupsHandler.TagService = server.TagService
var edgeStacksHandler = edgestacks.NewHandler(requestBouncer)
edgeStacksHandler.EdgeGroupService = server.EdgeGroupService
edgeStacksHandler.EdgeStackService = server.EdgeStackService
edgeStacksHandler.EndpointService = server.EndpointService
edgeStacksHandler.EndpointGroupService = server.EndpointGroupService
edgeStacksHandler.EndpointRelationService = server.EndpointRelationService
edgeStacksHandler.FileService = server.FileService
edgeStacksHandler.GitService = server.GitService
var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer)
edgeTemplatesHandler.SettingsService = server.SettingsService
var endpointHandler = endpoints.NewHandler(requestBouncer, server.EndpointManagement) var endpointHandler = endpoints.NewHandler(requestBouncer, server.EndpointManagement)
endpointHandler.AuthorizationService = authorizationService
endpointHandler.EdgeGroupService = server.EdgeGroupService
endpointHandler.EdgeStackService = server.EdgeStackService
endpointHandler.EndpointService = server.EndpointService endpointHandler.EndpointService = server.EndpointService
endpointHandler.EndpointGroupService = server.EndpointGroupService endpointHandler.EndpointGroupService = server.EndpointGroupService
endpointHandler.EndpointRelationService = server.EndpointRelationService
endpointHandler.FileService = server.FileService endpointHandler.FileService = server.FileService
endpointHandler.ProxyManager = proxyManager
endpointHandler.Snapshotter = server.Snapshotter
endpointHandler.JobService = server.JobService endpointHandler.JobService = server.JobService
endpointHandler.ProxyManager = proxyManager
endpointHandler.ReverseTunnelService = server.ReverseTunnelService endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.SettingsService = server.SettingsService endpointHandler.SettingsService = server.SettingsService
endpointHandler.AuthorizationService = authorizationService endpointHandler.Snapshotter = server.Snapshotter
endpointHandler.TagService = server.TagService
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
endpointEdgeHandler.EdgeStackService = server.EdgeStackService
endpointEdgeHandler.EndpointService = server.EndpointService
endpointEdgeHandler.FileService = server.FileService
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer) var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
endpointGroupHandler.EndpointService = server.EndpointService
endpointGroupHandler.AuthorizationService = authorizationService endpointGroupHandler.AuthorizationService = authorizationService
endpointGroupHandler.EdgeGroupService = server.EdgeGroupService
endpointGroupHandler.EdgeStackService = server.EdgeStackService
endpointGroupHandler.EndpointService = server.EndpointService
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
endpointGroupHandler.EndpointRelationService = server.EndpointRelationService
endpointGroupHandler.TagService = server.TagService
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer) var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
endpointProxyHandler.EndpointService = server.EndpointService endpointProxyHandler.EndpointService = server.EndpointService
@ -221,6 +261,11 @@ func (server *Server) Start() error {
stackHandler.ExtensionService = server.ExtensionService stackHandler.ExtensionService = server.ExtensionService
var tagHandler = tags.NewHandler(requestBouncer) var tagHandler = tags.NewHandler(requestBouncer)
tagHandler.EdgeGroupService = server.EdgeGroupService
tagHandler.EdgeStackService = server.EdgeStackService
tagHandler.EndpointService = server.EndpointService
tagHandler.EndpointGroupService = server.EndpointGroupService
tagHandler.EndpointRelationService = server.EndpointRelationService
tagHandler.TagService = server.TagService tagHandler.TagService = server.TagService
var teamHandler = teams.NewHandler(requestBouncer) var teamHandler = teams.NewHandler(requestBouncer)
@ -266,8 +311,12 @@ func (server *Server) Start() error {
RoleHandler: roleHandler, RoleHandler: roleHandler,
AuthHandler: authHandler, AuthHandler: authHandler,
DockerHubHandler: dockerHubHandler, DockerHubHandler: dockerHubHandler,
EdgeGroupsHandler: edgeGroupsHandler,
EdgeStacksHandler: edgeStacksHandler,
EdgeTemplatesHandler: edgeTemplatesHandler,
EndpointGroupHandler: endpointGroupHandler, EndpointGroupHandler: endpointGroupHandler,
EndpointHandler: endpointHandler, EndpointHandler: endpointHandler,
EndpointEdgeHandler: endpointEdgeHandler,
EndpointProxyHandler: endpointProxyHandler, EndpointProxyHandler: endpointProxyHandler,
FileHandler: fileHandler, FileHandler: fileHandler,
MOTDHandler: motdHandler, MOTDHandler: motdHandler,

View File

@ -39,7 +39,7 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
endpointURL := endpoint.URL endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentEnvironment { if endpoint.Type == portainer.EdgeAgentEnvironment {
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port) endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
} }
clientOpts := client.Options{ clientOpts := client.Options{

File diff suppressed because it is too large Load Diff

View File

@ -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). **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.23.2" version: "1.24.0"
title: "Portainer API" title: "Portainer API"
contact: contact:
email: "info@portainer.io" email: "info@portainer.io"
@ -3174,7 +3174,7 @@ definitions:
description: "Is analytics enabled" description: "Is analytics enabled"
Version: Version:
type: "string" type: "string"
example: "1.23.2" example: "1.24.0"
description: "Portainer API version" description: "Portainer API version"
PublicSettingsInspectResponse: PublicSettingsInspectResponse:
type: "object" type: "object"

View File

@ -1,5 +1,5 @@
{ {
"packageName": "portainer", "packageName": "portainer",
"packageVersion": "1.23.2", "packageVersion": "1.24.0",
"projectName": "portainer" "projectName": "portainer"
} }

71
api/tag.go Normal file
View File

@ -0,0 +1,71 @@
package portainer
type tagSet map[TagID]bool
// TagSet converts an array of ids to a set
func TagSet(tagIDs []TagID) tagSet {
set := map[TagID]bool{}
for _, tagID := range tagIDs {
set[tagID] = true
}
return set
}
// TagIntersection returns a set intersection of the provided sets
func TagIntersection(sets ...tagSet) tagSet {
intersection := tagSet{}
if len(sets) == 0 {
return intersection
}
setA := sets[0]
for tag := range setA {
inAll := true
for _, setB := range sets {
if !setB[tag] {
inAll = false
break
}
}
if inAll {
intersection[tag] = true
}
}
return intersection
}
// TagUnion returns a set union of provided sets
func TagUnion(sets ...tagSet) tagSet {
union := tagSet{}
for _, set := range sets {
for tag := range set {
union[tag] = true
}
}
return union
}
// TagContains return true if setA contains setB
func TagContains(setA tagSet, setB tagSet) bool {
containedTags := 0
for tag := range setB {
if setA[tag] {
containedTags++
}
}
return containedTags == len(setA)
}
// TagDifference returns the set difference tagsA - tagsB
func TagDifference(setA tagSet, setB tagSet) tagSet {
set := tagSet{}
for tag := range setA {
if !setB[tag] {
set[tag] = true
}
}
return set
}

View File

@ -4,6 +4,7 @@ import angular from 'angular';
import './agent/_module'; import './agent/_module';
import './azure/_module'; import './azure/_module';
import './docker/__module'; import './docker/__module';
import './edge/__module';
import './portainer/__module'; import './portainer/__module';
angular.module('portainer', [ angular.module('portainer', [
@ -29,15 +30,16 @@ angular.module('portainer', [
'portainer.agent', 'portainer.agent',
'portainer.azure', 'portainer.azure',
'portainer.docker', 'portainer.docker',
'portainer.edge',
'portainer.extensions', 'portainer.extensions',
'portainer.integrations', 'portainer.integrations',
'rzModule', 'rzModule',
'moment-picker' 'moment-picker',
]); ]);
if (require) { if (require) {
var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im); var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im);
req.keys().forEach(function(key) { req.keys().forEach(function (key) {
req(key); req(key);
}); });
} }

View File

@ -4,7 +4,7 @@ angular.module('portainer.agent').controller('FileUploaderController', [
var ctrl = this; var ctrl = this;
ctrl.state = { ctrl.state = {
uploadInProgress: false uploadInProgress: false,
}; };
ctrl.onFileSelected = onFileSelected; ctrl.onFileSelected = onFileSelected;
@ -19,5 +19,5 @@ angular.module('portainer.agent').controller('FileUploaderController', [
ctrl.state.uploadInProgress = false; ctrl.state.uploadInProgress = false;
}); });
} }
} },
]); ]);

View File

@ -1,6 +1,3 @@
<button <button ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress">
ngf-select="$ctrl.onFileSelected($file)" <i style="margin: 0;" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
class="btn ng-scope"
button-spinner="$ctrl.state.uploadInProgress">
<i style="margin:0" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
</button> </button>

View File

@ -2,6 +2,6 @@ angular.module('portainer.agent').component('fileUploader', {
templateUrl: './file-uploader.html', templateUrl: './file-uploader.html',
controller: 'FileUploaderController', controller: 'FileUploaderController',
bindings: { bindings: {
uploadFile: '<onFileSelected' uploadFile: '<onFileSelected',
} },
}); });

View File

@ -1,14 +1,20 @@
<div class="datatable"> <div class="datatable">
<rd-widget> <rd-widget>
<rd-widget-header icon="{{$ctrl.titleIcon}}" title-text="{{ $ctrl.titleText }}"> <rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="{{ $ctrl.titleText }}">
<file-uploader authorization="DockerAgentBrowsePut" ng-if="$ctrl.isUploadAllowed" on-file-selected="$ctrl.onFileSelectedForUpload"> <file-uploader authorization="DockerAgentBrowsePut" ng-if="$ctrl.isUploadAllowed" on-file-selected="($ctrl.onFileSelectedForUpload)"> </file-uploader>
</file-uploader>
</rd-widget-header> </rd-widget-header>
<rd-widget-body classes="no-padding"> <rd-widget-body classes="no-padding">
<div class="searchBar"> <div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i> <i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-model-options="{ debounce: 300 }" <input
ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus> type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
/>
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table">
@ -43,36 +49,33 @@
<tbody> <tbody>
<tr ng-if="!$ctrl.isRoot"> <tr ng-if="!$ctrl.isRoot">
<td colspan="4"> <td colspan="4">
<a ng-click="$ctrl.goToParent()"><i class="fa fa-level-up-alt space-right"></i>Go <a ng-click="$ctrl.goToParent()"><i class="fa fa-level-up-alt space-right"></i>Go to parent</a>
to parent</a>
</td> </td>
</tr> </tr>
<tr ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))"> <tr ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))">
<td> <td>
<span ng-if="item.edit"> <span ng-if="item.edit">
<input class="input-sm" type="text" ng-model="item.newName" <input
on-enter-key="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;" class="input-sm"
auto-focus /> type="text"
ng-model="item.newName"
on-enter-key="$ctrl.rename({ name: item.Name, newName: item.newName }); item.edit = false"
auto-focus
/>
<a class="interactive" ng-click="item.edit = false;"><i class="fa fa-times"></i></a> <a class="interactive" ng-click="item.edit = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><i <a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><i class="fa fa-check-square"></i></a>
class="fa fa-check-square"></i></a>
</span> </span>
<span ng-if="!item.edit && item.Dir"> <span ng-if="!item.edit && item.Dir">
<a ng-click="$ctrl.browse({name: item.Name})"><i class="fa fa-folder space-right" <a ng-click="$ctrl.browse({name: item.Name})"><i class="fa fa-folder space-right" aria-hidden="true"></i>{{ item.Name }}</a>
aria-hidden="true"></i>{{ item.Name }}</a>
</span>
<span ng-if="!item.edit && !item.Dir">
<i class="fa fa-file space-right" aria-hidden="true"></i>{{
item.Name }}
</span> </span>
<span ng-if="!item.edit && !item.Dir"> <i class="fa fa-file space-right" aria-hidden="true"></i>{{ item.Name }} </span>
</td> </td>
<td>{{ item.Size | humansize }}</td> <td>{{ item.Size | humansize }}</td>
<td> <td>
{{ item.ModTime | getisodatefromtimestamp }} {{ item.ModTime | getisodatefromtimestamp }}
</td> </td>
<td> <td>
<btn authorization="DockerAgentBrowseGet" class="btn btn-xs btn-primary space-right" ng-click="$ctrl.download({ name: item.Name })" <btn authorization="DockerAgentBrowseGet" class="btn btn-xs btn-primary space-right" ng-click="$ctrl.download({ name: item.Name })" ng-if="!item.Dir">
ng-if="!item.Dir">
<i class="fa fa-download" aria-hidden="true"></i> Download <i class="fa fa-download" aria-hidden="true"></i> Download
</btn> </btn>
<btn authorization="DockerAgentBrowseRename" class="btn btn-xs btn-primary space-right" ng-click="item.newName = item.Name; item.edit = true"> <btn authorization="DockerAgentBrowseRename" class="btn btn-xs btn-primary space-right" ng-click="item.newName = item.Name; item.edit = true">
@ -94,4 +97,4 @@
</div> </div>
</rd-widget-body> </rd-widget-body>
</rd-widget> </rd-widget>
</div> </div>

View File

@ -15,8 +15,8 @@ angular.module('portainer.agent').component('filesDatatable', {
rename: '&', rename: '&',
download: '&', download: '&',
delete: '&', delete: '&',
isUploadAllowed: '<', isUploadAllowed: '<',
onFileSelectedForUpload: '<' onFileSelectedForUpload: '<',
} },
}); });

View File

@ -1,12 +1,15 @@
import _ from 'lodash-es'; import _ from 'lodash-es';
angular.module('portainer.agent').controller('HostBrowserController', [ angular.module('portainer.agent').controller('HostBrowserController', [
'HostBrowserService', 'Notifications', 'FileSaver', 'ModalService', 'HostBrowserService',
'Notifications',
'FileSaver',
'ModalService',
function HostBrowserController(HostBrowserService, Notifications, FileSaver, ModalService) { function HostBrowserController(HostBrowserService, Notifications, FileSaver, ModalService) {
var ctrl = this; var ctrl = this;
var ROOT_PATH = '/host'; var ROOT_PATH = '/host';
ctrl.state = { ctrl.state = {
path: ROOT_PATH path: ROOT_PATH,
}; };
ctrl.goToParent = goToParent; ctrl.goToParent = goToParent;
@ -21,7 +24,7 @@ angular.module('portainer.agent').controller('HostBrowserController', [
function getRelativePath(path) { function getRelativePath(path) {
path = path || ctrl.state.path; path = path || ctrl.state.path;
var rootPathRegex = new RegExp('^' + ROOT_PATH + '\/?'); var rootPathRegex = new RegExp('^' + ROOT_PATH + '/?');
var relativePath = path.replace(rootPathRegex, '/'); var relativePath = path.replace(rootPathRegex, '/');
return relativePath; return relativePath;
} }
@ -71,7 +74,7 @@ angular.module('portainer.agent').controller('HostBrowserController', [
HostBrowserService.get(filePath) HostBrowserService.get(filePath)
.then(function onFileReceived(data) { .then(function onFileReceived(data) {
var downloadData = new Blob([data.file], { var downloadData = new Blob([data.file], {
type: 'text/plain;charset=utf-8' type: 'text/plain;charset=utf-8',
}); });
FileSaver.saveAs(downloadData, file); FileSaver.saveAs(downloadData, file);
}) })
@ -83,15 +86,12 @@ angular.module('portainer.agent').controller('HostBrowserController', [
function confirmDeleteFile(name) { function confirmDeleteFile(name) {
var filePath = buildPath(ctrl.state.path, name); var filePath = buildPath(ctrl.state.path, name);
ModalService.confirmDeletion( ModalService.confirmDeletion('Are you sure that you want to delete ' + getRelativePath(filePath) + ' ?', function onConfirm(confirmed) {
'Are you sure that you want to delete ' + getRelativePath(filePath) + ' ?', if (!confirmed) {
function onConfirm(confirmed) { return;
if (!confirmed) {
return;
}
return deleteFile(filePath);
} }
); return deleteFile(filePath);
});
} }
function deleteFile(path) { function deleteFile(path) {
@ -145,5 +145,5 @@ angular.module('portainer.agent').controller('HostBrowserController', [
function refreshList() { function refreshList() {
getFilesForPath(ctrl.state.path); getFilesForPath(ctrl.state.path);
} }
} },
]); ]);

View File

@ -1,6 +1,8 @@
<files-datatable <files-datatable
title-text="Host browser - {{$ctrl.getRelativePath()}}" title-icon="fa-file" title-text="Host browser - {{ $ctrl.getRelativePath() }}"
dataset="$ctrl.files" table-key="host_browser" title-icon="fa-file"
dataset="$ctrl.files"
table-key="host_browser"
order-by="Dir" order-by="Dir"
is-root="$ctrl.isRoot()" is-root="$ctrl.isRoot()"
go-to-parent="$ctrl.goToParent()" go-to-parent="$ctrl.goToParent()"
@ -8,9 +10,7 @@
rename="$ctrl.renameFile(name, newName)" rename="$ctrl.renameFile(name, newName)"
download="$ctrl.downloadFile(name)" download="$ctrl.downloadFile(name)"
delete="$ctrl.deleteFile(name)" delete="$ctrl.deleteFile(name)"
is-upload-allowed="true" is-upload-allowed="true"
on-file-selected-for-upload="$ctrl.onFileSelectedForUpload" on-file-selected-for-upload="($ctrl.onFileSelectedForUpload)"
> >
</files-datatable> </files-datatable>

View File

@ -1,5 +1,5 @@
angular.module('portainer.agent').component('hostBrowser', { angular.module('portainer.agent').component('hostBrowser', {
controller: 'HostBrowserController', controller: 'HostBrowserController',
templateUrl: './host-browser.html', templateUrl: './host-browser.html',
bindings: {} bindings: {},
}); });

View File

@ -2,6 +2,6 @@ angular.module('portainer.agent').component('nodeSelector', {
templateUrl: './nodeSelector.html', templateUrl: './nodeSelector.html',
controller: 'NodeSelectorController', controller: 'NodeSelectorController',
bindings: { bindings: {
model: '=' model: '=',
} },
}); });

View File

@ -1,8 +1,6 @@
<div class="form-group"> <div class="form-group">
<label for="target_node" class="col-sm-1 control-label text-left">Node</label> <label for="target_node" class="col-sm-1 control-label text-left">Node</label>
<div class="col-sm-11"> <div class="col-sm-11">
<select class="form-control" <select class="form-control" ng-model="$ctrl.model" ng-options="agent.NodeName as agent.NodeName for agent in $ctrl.agents"></select>
ng-model="$ctrl.model" ng-options="agent.NodeName as agent.NodeName for agent in $ctrl.agents"
></select>
</div> </div>
</div> </div>

View File

@ -1,18 +1,20 @@
angular.module('portainer.agent') angular.module('portainer.agent').controller('NodeSelectorController', [
.controller('NodeSelectorController', ['AgentService', 'Notifications', function (AgentService, Notifications) { 'AgentService',
var ctrl = this; 'Notifications',
function (AgentService, Notifications) {
var ctrl = this;
this.$onInit = function() { this.$onInit = function () {
AgentService.agents() AgentService.agents()
.then(function success(data) { .then(function success(data) {
ctrl.agents = data; ctrl.agents = data;
if (!ctrl.model) { if (!ctrl.model) {
ctrl.model = data[0].NodeName; ctrl.model = data[0].NodeName;
} }
}) })
.catch(function error(err) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to load agents'); Notifications.error('Failure', err, 'Unable to load agents');
}); });
}; };
},
}]); ]);

View File

@ -4,6 +4,6 @@ angular.module('portainer.agent').component('volumeBrowser', {
bindings: { bindings: {
volumeId: '<', volumeId: '<',
nodeName: '<', nodeName: '<',
isUploadEnabled: '<' isUploadEnabled: '<',
} },
}); });

View File

@ -1,6 +1,8 @@
<files-datatable <files-datatable
title-text="Volume browser" title-icon="fa-file" title-text="Volume browser"
dataset="$ctrl.files" table-key="volume_browser" title-icon="fa-file"
dataset="$ctrl.files"
table-key="volume_browser"
order-by="Dir" order-by="Dir"
is-root="$ctrl.state.path === '/'" is-root="$ctrl.state.path === '/'"
go-to-parent="$ctrl.up()" go-to-parent="$ctrl.up()"
@ -8,7 +10,6 @@
rename="$ctrl.rename(name, newName)" rename="$ctrl.rename(name, newName)"
download="$ctrl.download(name)" download="$ctrl.download(name)"
delete="$ctrl.delete(name)" delete="$ctrl.delete(name)"
is-upload-allowed="$ctrl.isUploadEnabled" is-upload-allowed="$ctrl.isUploadEnabled"
on-file-selected-for-upload="$ctrl.onFileSelectedForUpload" on-file-selected-for-upload="($ctrl.onFileSelectedForUpload)"
></files-datatable> ></files-datatable>

View File

@ -1,137 +1,137 @@
import _ from 'lodash-es'; import _ from 'lodash-es';
angular.module('portainer.agent') angular.module('portainer.agent').controller('VolumeBrowserController', [
.controller('VolumeBrowserController', ['HttpRequestHelper', 'VolumeBrowserService', 'FileSaver', 'Blob', 'ModalService', 'Notifications', 'HttpRequestHelper',
function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) { 'VolumeBrowserService',
var ctrl = this; 'FileSaver',
'Blob',
'ModalService',
'Notifications',
function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) {
var ctrl = this;
this.state = { this.state = {
path: '/' path: '/',
}; };
this.rename = function(file, newName) { this.rename = function (file, newName) {
var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
var newFilePath = this.state.path === '/' ? newName : this.state.path + '/' + newName; var newFilePath = this.state.path === '/' ? newName : this.state.path + '/' + newName;
VolumeBrowserService.rename(this.volumeId, filePath, newFilePath) VolumeBrowserService.rename(this.volumeId, filePath, newFilePath)
.then(function success() { .then(function success() {
Notifications.success('File successfully renamed', newFilePath); Notifications.success('File successfully renamed', newFilePath);
return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path); return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path);
}) })
.then(function success(data) { .then(function success(data) {
ctrl.files = data; ctrl.files = data;
}) })
.catch(function error(err) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to rename file'); Notifications.error('Failure', err, 'Unable to rename file');
}); });
}; };
this.delete = function(file) { this.delete = function (file) {
var filePath = this.state.path === '/' ? file : this.state.path + '/' + file; var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
ModalService.confirmDeletion( ModalService.confirmDeletion('Are you sure that you want to delete ' + filePath + ' ?', function onConfirm(confirmed) {
'Are you sure that you want to delete ' + filePath + ' ?', if (!confirmed) {
function onConfirm(confirmed) { return;
if(!confirmed) { return; } }
deleteFile(filePath); deleteFile(filePath);
}
);
};
this.download = function(file) {
var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
VolumeBrowserService.get(this.volumeId, filePath)
.then(function success(data) {
var downloadData = new Blob([data.file]);
FileSaver.saveAs(downloadData, file);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to download file');
});
};
this.up = function() {
var parentFolder = parentPath(this.state.path);
browse(parentFolder);
};
this.browse = function(folder) {
var path = buildPath(this.state.path, folder);
browse(path);
};
function deleteFile(file) {
VolumeBrowserService.delete(ctrl.volumeId, file)
.then(function success() {
Notifications.success('File successfully deleted', file);
return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path);
})
.then(function success(data) {
ctrl.files = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to delete file');
});
}
function browse(path) {
VolumeBrowserService.ls(ctrl.volumeId, path)
.then(function success(data) {
ctrl.state.path = path;
ctrl.files = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to browse volume');
});
}
this.onFileSelectedForUpload = function onFileSelectedForUpload(file) {
VolumeBrowserService.upload(ctrl.state.path, file, ctrl.volumeId)
.then(function onFileUpload() {
onFileUploaded();
})
.catch(function onFileUpload(err) {
Notifications.error('Failure', err, 'Unable to upload file');
}); });
}; };
function parentPath(path) { this.download = function (file) {
if (path.lastIndexOf('/') === 0) { var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
return '/'; VolumeBrowserService.get(this.volumeId, filePath)
.then(function success(data) {
var downloadData = new Blob([data.file]);
FileSaver.saveAs(downloadData, file);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to download file');
});
};
this.up = function () {
var parentFolder = parentPath(this.state.path);
browse(parentFolder);
};
this.browse = function (folder) {
var path = buildPath(this.state.path, folder);
browse(path);
};
function deleteFile(file) {
VolumeBrowserService.delete(ctrl.volumeId, file)
.then(function success() {
Notifications.success('File successfully deleted', file);
return VolumeBrowserService.ls(ctrl.volumeId, ctrl.state.path);
})
.then(function success(data) {
ctrl.files = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to delete file');
});
} }
var split = _.split(path, '/'); function browse(path) {
return _.join(_.slice(split, 0, split.length - 1), '/'); VolumeBrowserService.ls(ctrl.volumeId, path)
} .then(function success(data) {
ctrl.state.path = path;
function buildPath(parent, file) { ctrl.files = data;
if (parent === '/') { })
return parent + file; .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to browse volume');
});
} }
return parent + '/' + file;
}
this.onFileSelectedForUpload = function onFileSelectedForUpload(file) {
VolumeBrowserService.upload(ctrl.state.path, file, ctrl.volumeId)
.then(function onFileUpload() {
onFileUploaded();
})
.catch(function onFileUpload(err) {
Notifications.error('Failure', err, 'Unable to upload file');
});
};
this.$onInit = function() { function parentPath(path) {
HttpRequestHelper.setPortainerAgentTargetHeader(this.nodeName); if (path.lastIndexOf('/') === 0) {
VolumeBrowserService.ls(this.volumeId, this.state.path) return '/';
.then(function success(data) { }
ctrl.files = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to browse volume');
});
};
function onFileUploaded() { var split = _.split(path, '/');
refreshList(); return _.join(_.slice(split, 0, split.length - 1), '/');
} }
function refreshList() { function buildPath(parent, file) {
browse(ctrl.state.path); if (parent === '/') {
} return parent + file;
}
return parent + '/' + file;
}
this.$onInit = function () {
HttpRequestHelper.setPortainerAgentTargetHeader(this.nodeName);
VolumeBrowserService.ls(this.volumeId, this.state.path)
.then(function success(data) {
ctrl.files = data;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to browse volume');
});
};
}]); function onFileUploaded() {
refreshList();
}
function refreshList() {
browse(ctrl.state.path);
}
},
]);

View File

@ -1,5 +1,5 @@
export function AgentViewModel(data) { export function AgentViewModel(data) {
this.IPAddress = data.IPAddress; this.IPAddress = data.IPAddress;
this.NodeName = data.NodeName; this.NodeName = data.NodeName;
this.NodeRole = data.NodeRole; this.NodeRole = data.NodeRole;
} }

View File

@ -1,12 +1,19 @@
angular.module('portainer.agent') angular.module('portainer.agent').factory('Agent', [
.factory('Agent', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager', '$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'StateManager',
function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) {
'use strict'; 'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/agents', { return $resource(
endpointId: EndpointProvider.endpointID, API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/agents',
version: StateManager.getAgentApiVersion {
endpointId: EndpointProvider.endpointID,
version: StateManager.getAgentApiVersion,
},
{
query: { method: 'GET', isArray: true },
}
);
}, },
{ ]);
query: { method: 'GET', isArray: true }
});
}]);

View File

@ -1,27 +1,39 @@
import { browseGetResponse } from './response/browse'; import { browseGetResponse } from './response/browse';
angular.module('portainer.agent') angular.module('portainer.agent').factory('Browse', [
.factory('Browse', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager', '$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'StateManager',
function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) {
'use strict'; 'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/browse/:action', { return $resource(
endpointId: EndpointProvider.endpointID, API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/browse/:action',
version: StateManager.getAgentApiVersion {
endpointId: EndpointProvider.endpointID,
version: StateManager.getAgentApiVersion,
},
{
ls: {
method: 'GET',
isArray: true,
params: { action: 'ls' },
},
get: {
method: 'GET',
params: { action: 'get' },
transformResponse: browseGetResponse,
responseType: 'arraybuffer',
},
delete: {
method: 'DELETE',
params: { action: 'delete' },
},
rename: {
method: 'PUT',
params: { action: 'rename' },
},
}
);
}, },
{ ]);
ls: {
method: 'GET', isArray: true, params: { action: 'ls' }
},
get: {
method: 'GET', params: { action: 'get' },
transformResponse: browseGetResponse,
responseType: 'arraybuffer'
},
delete: {
method: 'DELETE', params: { action: 'delete' }
},
rename: {
method: 'PUT', params: { action: 'rename' }
}
});
}]);

View File

@ -1,16 +1,19 @@
angular.module('portainer.agent').factory('Host', [ angular.module('portainer.agent').factory('Host', [
'$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager', '$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'StateManager',
function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) {
'use strict'; 'use strict';
return $resource( return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/host/:action', API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/host/:action',
{ {
endpointId: EndpointProvider.endpointID, endpointId: EndpointProvider.endpointID,
version: StateManager.getAgentApiVersion version: StateManager.getAgentApiVersion,
}, },
{ {
info: { method: 'GET', params: { action: 'info' } } info: { method: 'GET', params: { action: 'info' } },
} }
); );
} },
]); ]);

View File

@ -1,11 +1,14 @@
angular.module('portainer.agent').factory('AgentPing', [ angular.module('portainer.agent').factory('AgentPing', [
'$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q', '$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'$q',
function AgentPingFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) { function AgentPingFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q) {
'use strict'; 'use strict';
return $resource( return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/ping', API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/ping',
{ {
endpointId: EndpointProvider.endpointID endpointId: EndpointProvider.endpointID,
}, },
{ {
ping: { ping: {
@ -13,8 +16,7 @@ angular.module('portainer.agent').factory('AgentPing', [
interceptor: { interceptor: {
response: function versionInterceptor(response) { response: function versionInterceptor(response) {
var instance = response.resource; var instance = response.resource;
var version = var version = response.headers('Portainer-Agent-Api-Version') || 1;
response.headers('Portainer-Agent-Api-Version') || 1;
instance.version = Number(version); instance.version = Number(version);
return instance; return instance;
}, },
@ -24,10 +26,10 @@ angular.module('portainer.agent').factory('AgentPing', [
return { version: 1 }; return { version: 1 };
} }
return $q.reject(error); return $q.reject(error);
} },
} },
} },
} }
); );
} },
]); ]);

View File

@ -1,10 +1,17 @@
angular.module('portainer.agent') angular.module('portainer.agent').factory('AgentVersion1', [
.factory('AgentVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { '$resource',
'use strict'; 'API_ENDPOINT_ENDPOINTS',
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents', { 'EndpointProvider',
endpointId: EndpointProvider.endpointID function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents',
{
endpointId: EndpointProvider.endpointID,
},
{
query: { method: 'GET', isArray: true },
}
);
}, },
{ ]);
query: { method: 'GET', isArray: true }
});
}]);

View File

@ -1,25 +1,37 @@
import { browseGetResponse } from '../response/browse'; import { browseGetResponse } from '../response/browse';
angular.module('portainer.agent') angular.module('portainer.agent').factory('BrowseVersion1', [
.factory('BrowseVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { '$resource',
'use strict'; 'API_ENDPOINT_ENDPOINTS',
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:volumeID/:action', { 'EndpointProvider',
endpointId: EndpointProvider.endpointID function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:volumeID/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
ls: {
method: 'GET',
isArray: true,
params: { action: 'ls' },
},
get: {
method: 'GET',
params: { action: 'get' },
transformResponse: browseGetResponse,
responseType: 'arraybuffer',
},
delete: {
method: 'DELETE',
params: { action: 'delete' },
},
rename: {
method: 'PUT',
params: { action: 'rename' },
},
}
);
}, },
{ ]);
ls: {
method: 'GET', isArray: true, params: { action: 'ls' }
},
get: {
method: 'GET', params: { action: 'get' },
transformResponse: browseGetResponse,
responseType: 'arraybuffer'
},
delete: {
method: 'DELETE', params: { action: 'delete' }
},
rename: {
method: 'PUT', params: { action: 'rename' }
}
});
}]);

View File

@ -1,7 +1,12 @@
import { AgentViewModel } from '../models/agent'; import { AgentViewModel } from '../models/agent';
angular.module('portainer.agent').factory('AgentService', [ angular.module('portainer.agent').factory('AgentService', [
'$q', 'Agent', 'AgentVersion1', 'HttpRequestHelper', 'Host', 'StateManager', '$q',
'Agent',
'AgentVersion1',
'HttpRequestHelper',
'Host',
'StateManager',
function AgentServiceFactory($q, Agent, AgentVersion1, HttpRequestHelper, Host, StateManager) { function AgentServiceFactory($q, Agent, AgentVersion1, HttpRequestHelper, Host, StateManager) {
'use strict'; 'use strict';
var service = {}; var service = {};
@ -24,10 +29,11 @@ angular.module('portainer.agent').factory('AgentService', [
var agentVersion = getAgentApiVersion(); var agentVersion = getAgentApiVersion();
var service = agentVersion > 1 ? Agent : AgentVersion1; var service = agentVersion > 1 ? Agent : AgentVersion1;
service.query({ version: agentVersion }) service
.query({ version: agentVersion })
.$promise.then(function success(data) { .$promise.then(function success(data) {
var agents = data.map(function(item) { var agents = data.map(function (item) {
return new AgentViewModel(item); return new AgentViewModel(item);
}); });
deferred.resolve(agents); deferred.resolve(agents);
@ -40,5 +46,5 @@ angular.module('portainer.agent').factory('AgentService', [
} }
return service; return service;
} },
]); ]);

View File

@ -1,5 +1,10 @@
angular.module('portainer.agent').factory('HostBrowserService', [ angular.module('portainer.agent').factory('HostBrowserService', [
'Browse', 'Upload', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', '$q', 'StateManager', 'Browse',
'Upload',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'$q',
'StateManager',
function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q, StateManager) { function HostBrowserServiceFactory(Browse, Upload, API_ENDPOINT_ENDPOINTS, EndpointProvider, $q, StateManager) {
var service = {}; var service = {};
@ -24,7 +29,7 @@ angular.module('portainer.agent').factory('HostBrowserService', [
function rename(path, newPath) { function rename(path, newPath) {
var payload = { var payload = {
CurrentFilePath: path, CurrentFilePath: path,
NewFilePath: newPath NewFilePath: newPath,
}; };
return Browse.rename({}, payload).$promise; return Browse.rename({}, payload).$promise;
} }
@ -32,21 +37,15 @@ angular.module('portainer.agent').factory('HostBrowserService', [
function upload(path, file, onProgress) { function upload(path, file, onProgress) {
var deferred = $q.defer(); var deferred = $q.defer();
var agentVersion = StateManager.getAgentApiVersion(); var agentVersion = StateManager.getAgentApiVersion();
var url = var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker' + (agentVersion > 1 ? '/v' + agentVersion : '') + '/browse/put';
API_ENDPOINT_ENDPOINTS +
'/' +
EndpointProvider.endpointID() +
'/docker' +
(agentVersion > 1 ? '/v' + agentVersion : '') +
'/browse/put';
Upload.upload({ Upload.upload({
url: url, url: url,
data: { file: file, Path: path } data: { file: file, Path: path },
}).then(deferred.resolve, deferred.reject, onProgress); }).then(deferred.resolve, deferred.reject, onProgress);
return deferred.promise; return deferred.promise;
} }
return service; return service;
} },
]); ]);

View File

@ -10,5 +10,5 @@ angular.module('portainer.agent').service('AgentPingService', [
} }
return service; return service;
} },
]); ]);

View File

@ -1,5 +1,11 @@
angular.module('portainer.agent').factory('VolumeBrowserService', [ angular.module('portainer.agent').factory('VolumeBrowserService', [
'StateManager', 'Browse', 'BrowseVersion1', '$q', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'Upload', 'StateManager',
'Browse',
'BrowseVersion1',
'$q',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
'Upload',
function VolumeBrowserServiceFactory(StateManager, Browse, BrowseVersion1, $q, API_ENDPOINT_ENDPOINTS, EndpointProvider, Upload) { function VolumeBrowserServiceFactory(StateManager, Browse, BrowseVersion1, $q, API_ENDPOINT_ENDPOINTS, EndpointProvider, Upload) {
'use strict'; 'use strict';
var service = {}; var service = {};
@ -14,22 +20,22 @@ angular.module('portainer.agent').factory('VolumeBrowserService', [
return agentVersion > 1 ? Browse : BrowseVersion1; return agentVersion > 1 ? Browse : BrowseVersion1;
} }
service.ls = function(volumeId, path) { service.ls = function (volumeId, path) {
return getBrowseService().ls({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; return getBrowseService().ls({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise;
}; };
service.get = function(volumeId, path) { service.get = function (volumeId, path) {
return getBrowseService().get({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; return getBrowseService().get({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise;
}; };
service.delete = function(volumeId, path) { service.delete = function (volumeId, path) {
return getBrowseService().delete({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise; return getBrowseService().delete({ volumeID: volumeId, path: path, version: getAgentApiVersion() }).$promise;
}; };
service.rename = function(volumeId, path, newPath) { service.rename = function (volumeId, path, newPath) {
var payload = { var payload = {
CurrentFilePath: path, CurrentFilePath: path,
NewFilePath: newPath NewFilePath: newPath,
}; };
return getBrowseService().rename({ volumeID: volumeId, version: getAgentApiVersion() }, payload).$promise; return getBrowseService().rename({ volumeID: volumeId, version: getAgentApiVersion() }, payload).$promise;
}; };
@ -37,26 +43,19 @@ angular.module('portainer.agent').factory('VolumeBrowserService', [
service.upload = function upload(path, file, volumeId, onProgress) { service.upload = function upload(path, file, volumeId, onProgress) {
var deferred = $q.defer(); var deferred = $q.defer();
var agentVersion = StateManager.getAgentApiVersion(); var agentVersion = StateManager.getAgentApiVersion();
if (agentVersion <2) { if (agentVersion < 2) {
deferred.reject('upload is not supported on this agent version'); deferred.reject('upload is not supported on this agent version');
return; return;
} }
var url = var url = API_ENDPOINT_ENDPOINTS + '/' + EndpointProvider.endpointID() + '/docker' + '/v' + agentVersion + '/browse/put?volumeID=' + volumeId;
API_ENDPOINT_ENDPOINTS +
'/' +
EndpointProvider.endpointID() +
'/docker' +
'/v' + agentVersion +
'/browse/put?volumeID=' +
volumeId;
Upload.upload({ Upload.upload({
url: url, url: url,
data: { file: file, Path: path } data: { file: file, Path: path },
}).then(deferred.resolve, deferred.reject, onProgress); }).then(deferred.resolve, deferred.reject, onProgress);
return deferred.promise; return deferred.promise;
}; };
return service; return service;
} },
]); ]);

Some files were not shown because too many files have changed in this diff Show More