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:
- 'eslint:recommended'
- prettier
plugins:
- import
parserOptions:
ecmaVersion: 2018
@ -17,276 +21,9 @@ parserOptions:
ecmaFeatures:
modules: true
# # http://eslint.org/docs/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-debugger: error
# no-dupe-args: error
# no-dupe-keys: error
# no-duplicate-case: error
# no-empty-character-class: error
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-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-return: off
# 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
import/order: error

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ portainer-checksum.txt
api/cmd/portainer/portainer*
.tmp
.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
Want to contribute but don't know where to start?
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
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
## Commit Message Format

View File

@ -3,15 +3,14 @@
</p>
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer 'Image size')
[![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)
[![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 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
@ -29,36 +28,31 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
## Getting started
* [Deploy Portainer](https://portainer.readthedocs.io/en/latest/deployment.html)
* [Documentation](https://portainer.readthedocs.io)
- [Deploy Portainer](https://www.portainer.io/installation/)
- [Documentation](https://www.portainer.io/documentation/)
## 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
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/
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/
- Issues: https://github.com/portainer/portainer/issues
- FAQ: https://www.portainer.io/documentation/faqs/
- Slack (chat): https://portainer.io/slack/
## 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 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 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://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
* 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
**_Portainer_** has full support for the following Docker versions:
* 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
Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported.
## 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)
rdash-angular: Copyright (c) [2014] [Elliot Hesp]
rdash-angular: Copyright (c) [2014][elliot hesp]

View File

@ -5,6 +5,9 @@ import (
"path"
"time"
"github.com/portainer/portainer/api/bolt/edgegroup"
"github.com/portainer/portainer/api/bolt/edgestack"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/tunnelserver"
"github.com/boltdb/bolt"
@ -42,8 +45,11 @@ type Store struct {
fileService portainer.FileService
RoleService *role.Service
DockerHubService *dockerhub.Service
EdgeGroupService *edgegroup.Service
EdgeStackService *edgestack.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
@ -121,6 +127,7 @@ func (store *Store) MigrateData() error {
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
@ -128,6 +135,7 @@ func (store *Store) MigrateData() error {
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
TemplateService: store.TemplateService,
UserService: store.UserService,
@ -160,6 +168,18 @@ func (store *Store) initServices() error {
}
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)
if err != nil {
return err
@ -172,6 +192,12 @@ func (store *Store) initServices() error {
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.db)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.db)
if err != nil {
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{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Tags: []string{},
TagIDs: []portainer.TagID{},
}
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/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/endpointgroup"
"github.com/portainer/portainer/api/bolt/endpointrelation"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/registry"
"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/settings"
"github.com/portainer/portainer/api/bolt/stack"
"github.com/portainer/portainer/api/bolt/tag"
"github.com/portainer/portainer/api/bolt/teammembership"
"github.com/portainer/portainer/api/bolt/template"
"github.com/portainer/portainer/api/bolt/user"
@ -25,6 +27,7 @@ type (
db *bolt.DB
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
@ -32,6 +35,7 @@ type (
scheduleService *schedule.Service
settingsService *settings.Service
stackService *stack.Service
tagService *tag.Service
teamMembershipService *teammembership.Service
templateService *template.Service
userService *user.Service
@ -45,6 +49,7 @@ type (
DatabaseVersion int
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
@ -52,6 +57,7 @@ type (
ScheduleService *schedule.Service
SettingsService *settings.Service
StackService *stack.Service
TagService *tag.Service
TeamMembershipService *teammembership.Service
TemplateService *template.Service
UserService *user.Service
@ -67,12 +73,14 @@ func NewMigrator(parameters *Parameters) *Migrator {
currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,
scheduleService: parameters.ScheduleService,
settingsService: parameters.SettingsService,
tagService: parameters.TagService,
teamMembershipService: parameters.TeamMembershipService,
templateService: parameters.TemplateService,
stackService: parameters.StackService,
@ -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)
}

View File

@ -52,6 +52,19 @@ func (service *Service) Tags() ([]portainer.Tag, error) {
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.
func (service *Service) CreateTag(tag *portainer.Tag) 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.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := internal.Itob(int(ID))

View File

@ -179,7 +179,7 @@ func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tun
}
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)
if err != nil {
return err

View File

@ -1,6 +1,7 @@
package cli
import (
"log"
"time"
"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(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints (deprecated)").String(),
NoAuth: kingpin.Flag("no-auth", "Disable authentication (deprecated)").Default(defaultNoAuth).Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
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(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots").Default(defaultSnapshot).Bool(),
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source (deprecated)").Default(defaultSyncInterval).String(),
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots (deprecated)").Default(defaultSnapshot).Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
TemplateFile: kingpin.Flag("template-file", "Path to the 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()
@ -76,6 +77,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
if *flags.EndpointURL != "" && *flags.ExternalEndpoints != "" {
return errEndpointExcludeExternal
}
@ -116,6 +119,28 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
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 {
if endpointURL != "" {
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {

View File

@ -397,7 +397,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@ -440,7 +440,7 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@ -661,8 +661,11 @@ func main() {
UserService: store.UserService,
TeamService: store.TeamService,
TeamMembershipService: store.TeamMembershipService,
EdgeGroupService: store.EdgeGroupService,
EdgeStackService: store.EdgeStackService,
EndpointService: store.EndpointService,
EndpointGroupService: store.EndpointGroupService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,

View File

@ -81,7 +81,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portain
}
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(
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
if endpoint.Type == portainer.EdgeAgentEnvironment {
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)

View File

@ -29,6 +29,8 @@ const (
ComposeStorePath = "compose"
// ComposeFileDefaultName represents the default name of a compose file.
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 = "portainer.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
}
// 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
// ExtensionRegistryManagementStorePath and stores a new file from bytes.
// 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
Description string
AssociatedEndpoints []portainer.EndpointID
Tags []string
TagIDs []portainer.TagID
}
func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Name) {
return portainer.Error("Invalid endpoint group name")
}
if payload.Tags == nil {
payload.Tags = []string{}
if payload.TagIDs == nil {
payload.TagIDs = []portainer.TagID{}
}
return nil
}
@ -40,7 +40,7 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
Description: payload.Description,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
}
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}
}
err = handler.updateEndpointRelations(&endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relations changes inside the database", err}
}
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)
}

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}
}
_, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
} else if err != nil {
@ -46,6 +46,11 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
if err != nil {
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)
}

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}
}
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)
}

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}
}
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)
}

View File

@ -13,7 +13,7 @@ import (
type endpointGroupUpdatePayload struct {
Name string
Description string
Tags []string
TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
}
@ -50,8 +50,44 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
endpointGroup.Description = payload.Description
}
if payload.Tags != nil {
endpointGroup.Tags = payload.Tags
tagsChanged := false
if payload.TagIDs != nil {
payloadTagSet := portainer.TagSet(payload.TagIDs)
endpointGroupTagSet := portainer.TagSet((endpointGroup.TagIDs))
union := portainer.TagUnion(payloadTagSet, endpointGroupTagSet)
intersection := portainer.TagIntersection(payloadTagSet, endpointGroupTagSet)
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
@ -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)
}

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.
type Handler struct {
*mux.Router
AuthorizationService *portainer.AuthorizationService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
AuthorizationService *portainer.AuthorizationService
EndpointRelationService portainer.EndpointRelationService
TagService portainer.TagService
}
// NewHandler creates a handler to manage endpoint group operations.

View File

@ -32,7 +32,7 @@ type endpointCreatePayload struct {
AzureApplicationID string
AzureTenantID string
AzureAuthenticationKey string
Tags []string
TagIDs []portainer.TagID
}
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
@ -54,14 +54,14 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
}
payload.GroupID = groupID
var tags []string
err = request.RetrieveMultiPartFormJSONValue(r, "Tags", &tags, true)
var tagIDs []portainer.TagID
err = request.RetrieveMultiPartFormJSONValue(r, "TagIds", &tagIDs, true)
if err != nil {
return portainer.Error("Invalid Tags parameter")
return portainer.Error("Invalid TagIds parameter")
}
payload.Tags = tags
if payload.Tags == nil {
payload.Tags = make([]string, 0)
payload.TagIDs = tagIDs
if payload.TagIDs == nil {
payload.TagIDs = make([]portainer.TagID, 0)
}
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
@ -146,6 +146,38 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
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)
}
@ -187,7 +219,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
AzureCredentials: credentials,
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@ -232,7 +264,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
EdgeKey: edgeKey,
@ -278,7 +310,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@ -322,7 +354,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload)
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
TagIDs: payload.TagIDs,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
}
@ -377,6 +409,20 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
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
}

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)
}
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"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"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)
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()
if err != nil {
@ -46,12 +55,32 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
if endpointIDs != nil {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
}
if groupID != 0 {
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
}
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)
@ -97,17 +126,17 @@ func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID po
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)
for _, endpoint := range endpoints {
if endpointMatchSearchCriteria(&endpoint, searchCriteria) {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, searchCriteria) {
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
@ -115,7 +144,7 @@ func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGro
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) {
return true
}
@ -129,8 +158,7 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range endpoint.Tags {
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
@ -139,14 +167,14 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria st
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 {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
for _, tag := range group.Tags {
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
@ -156,3 +184,106 @@ func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGrou
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
import (
"errors"
"net/http"
httperror "github.com/portainer/libhttp/error"
@ -10,12 +9,18 @@ import (
"github.com/portainer/portainer/api"
)
type stackStatusResponse struct {
ID portainer.EdgeStackID
Version int
}
type endpointStatusInspectResponse struct {
Status string `json:"status"`
Port int `json:"port"`
Schedules []portainer.EdgeSchedule `json:"schedules"`
CheckinInterval int `json:"checkin"`
Credentials string `json:"credentials"`
Stacks []stackStatusResponse `json:"stacks"`
}
// 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}
}
if endpoint.Type != portainer.EdgeAgentEnvironment {
return &httperror.HandlerError{http.StatusInternalServerError, "Status unavailable for non Edge agent endpoints", errors.New("Status unavailable")}
}
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")}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}
if endpoint.EdgeID == "" {
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
endpoint.EdgeID = edgeIdentifier
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)
}
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)
}

View File

@ -24,7 +24,7 @@ type endpointUpdatePayload struct {
AzureApplicationID *string
AzureTenantID *string
AzureAuthenticationKey *string
Tags []string
TagIDs []portainer.TagID
UserAccessPolicies portainer.UserAccessPolicies
TeamAccessPolicies portainer.TeamAccessPolicies
}
@ -69,12 +69,52 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.PublicURL = *payload.PublicURL
}
groupIDChanged := false
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 {
endpoint.Tags = payload.Tags
tagsChanged := false
if payload.TagIDs != nil {
payloadTagSet := portainer.TagSet(payload.TagIDs)
endpointTagSet := portainer.TagSet((endpoint.TagIDs))
union := portainer.TagUnion(payloadTagSet, endpointTagSet)
intersection := portainer.TagIntersection(payloadTagSet, endpointTagSet)
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
@ -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)
}

View File

@ -29,15 +29,19 @@ type Handler struct {
*mux.Router
authorizeEndpointManagement bool
requestBouncer *security.RequestBouncer
AuthorizationService *portainer.AuthorizationService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
FileService portainer.FileService
ProxyManager *proxy.Manager
Snapshotter portainer.Snapshotter
JobService portainer.JobService
ProxyManager *proxy.Manager
ReverseTunnelService portainer.ReverseTunnelService
SettingsService portainer.SettingsService
AuthorizationService *portainer.AuthorizationService
Snapshotter portainer.Snapshotter
TagService portainer.TagService
}
// 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)
h.Handle("/endpoints/{id}/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
return h
}

View File

@ -4,6 +4,10 @@ import (
"net/http"
"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/schedules"
@ -37,6 +41,10 @@ import (
type Handler struct {
AuthHandler *auth.Handler
DockerHubHandler *dockerhub.Handler
EdgeGroupsHandler *edgegroups.Handler
EdgeStacksHandler *edgestacks.Handler
EdgeTemplatesHandler *edgetemplates.Handler
EndpointEdgeHandler *endpointedge.Handler
EndpointGroupHandler *endpointgroups.Handler
EndpointHandler *endpoints.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)
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
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"):
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
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)
case strings.Contains(r.URL.Path, "/azure/"):
http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r)
case strings.Contains(r.URL.Path, "/edge/"):
http.StripPrefix("/api/endpoints", h.EndpointEdgeHandler).ServeHTTP(w, r)
default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
}

View File

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

View File

@ -24,6 +24,7 @@ type settingsUpdatePayload struct {
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
}
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
}
if payload.EnableEdgeComputeFeatures != nil {
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
}
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
if err != nil {

View File

@ -9,7 +9,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
// 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}
}
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)
if externalStack {
return handler.deleteExternalStack(r, w, stackID)
return handler.deleteExternalStack(r, w, stackID, securityContext)
}
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}
}
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)
if err != nil {
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)
}
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)
if err != nil && err != portainer.ErrObjectNotFound {
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}
}
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))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}

View File

@ -13,6 +13,11 @@ import (
type Handler struct {
*mux.Router
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.
@ -23,7 +28,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/tags",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
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}",
bouncer.AdminAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)

View File

@ -42,6 +42,8 @@ func (handler *Handler) tagCreate(w http.ResponseWriter, r *http.Request) *httpe
tag := &portainer.Tag{
Name: payload.Name,
EndpointGroups: map[portainer.EndpointGroupID]bool{},
Endpoints: map[portainer.EndpointID]bool{},
}
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 {
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 {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the tag from the database", err}
}
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 {
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 {
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) {
if endpoint.Type == portainer.EdgeAgentEnvironment {
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)

View File

@ -158,7 +158,7 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
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)
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
}

View File

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

View File

@ -2,12 +2,14 @@ package docker
import (
"context"
"errors"
"net/http"
"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/security"
)
const (
@ -87,3 +89,40 @@ func (transport *Transport) volumeInspectOperation(response *http.Response, exec
func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} {
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
import (
"errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
@ -143,6 +145,24 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp
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 {
tokenData, err := RetrieveTokenData(r)
if err != nil {

View File

@ -3,11 +3,15 @@ package http
import (
"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/roles"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/handler"
"github.com/portainer/portainer/api/http/handler/auth"
@ -55,8 +59,11 @@ type Server struct {
Snapshotter portainer.Snapshotter
RoleService portainer.RoleService
DockerHubService portainer.DockerHubService
EdgeGroupService portainer.EdgeGroupService
EdgeStackService portainer.EdgeStackService
EndpointService portainer.EndpointService
EndpointGroupService portainer.EndpointGroupService
EndpointRelationService portainer.EndpointRelationService
FileService portainer.FileService
GitService portainer.GitService
JWTService portainer.JWTService
@ -144,21 +151,54 @@ func (server *Server) Start() error {
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
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)
endpointHandler.AuthorizationService = authorizationService
endpointHandler.EdgeGroupService = server.EdgeGroupService
endpointHandler.EdgeStackService = server.EdgeStackService
endpointHandler.EndpointService = server.EndpointService
endpointHandler.EndpointGroupService = server.EndpointGroupService
endpointHandler.EndpointRelationService = server.EndpointRelationService
endpointHandler.FileService = server.FileService
endpointHandler.ProxyManager = proxyManager
endpointHandler.Snapshotter = server.Snapshotter
endpointHandler.JobService = server.JobService
endpointHandler.ProxyManager = proxyManager
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
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)
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
endpointGroupHandler.EndpointService = server.EndpointService
endpointGroupHandler.AuthorizationService = authorizationService
endpointGroupHandler.EdgeGroupService = server.EdgeGroupService
endpointGroupHandler.EdgeStackService = server.EdgeStackService
endpointGroupHandler.EndpointService = server.EndpointService
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
endpointGroupHandler.EndpointRelationService = server.EndpointRelationService
endpointGroupHandler.TagService = server.TagService
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
endpointProxyHandler.EndpointService = server.EndpointService
@ -221,6 +261,11 @@ func (server *Server) Start() error {
stackHandler.ExtensionService = server.ExtensionService
var tagHandler = tags.NewHandler(requestBouncer)
tagHandler.EdgeGroupService = server.EdgeGroupService
tagHandler.EdgeStackService = server.EdgeStackService
tagHandler.EndpointService = server.EndpointService
tagHandler.EndpointGroupService = server.EndpointGroupService
tagHandler.EndpointRelationService = server.EndpointRelationService
tagHandler.TagService = server.TagService
var teamHandler = teams.NewHandler(requestBouncer)
@ -266,8 +311,12 @@ func (server *Server) Start() error {
RoleHandler: roleHandler,
AuthHandler: authHandler,
DockerHubHandler: dockerHubHandler,
EdgeGroupsHandler: edgeGroupsHandler,
EdgeStacksHandler: edgeStacksHandler,
EdgeTemplatesHandler: edgeTemplatesHandler,
EndpointGroupHandler: endpointGroupHandler,
EndpointHandler: endpointHandler,
EndpointEdgeHandler: endpointEdgeHandler,
EndpointProxyHandler: endpointProxyHandler,
FileHandler: fileHandler,
MOTDHandler: motdHandler,

View File

@ -39,7 +39,7 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentEnvironment {
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{

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

View File

@ -1,5 +1,5 @@
{
"packageName": "portainer",
"packageVersion": "1.23.2",
"packageVersion": "1.24.0",
"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 './azure/_module';
import './docker/__module';
import './edge/__module';
import './portainer/__module';
angular.module('portainer', [
@ -29,15 +30,16 @@ angular.module('portainer', [
'portainer.agent',
'portainer.azure',
'portainer.docker',
'portainer.edge',
'portainer.extensions',
'portainer.integrations',
'rzModule',
'moment-picker'
'moment-picker',
]);
if (require) {
var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im);
req.keys().forEach(function(key) {
req.keys().forEach(function (key) {
req(key);
});
}

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,6 @@ angular.module('portainer.agent').component('filesDatatable', {
delete: '&',
isUploadAllowed: '<',
onFileSelectedForUpload: '<'
}
onFileSelectedForUpload: '<',
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
angular.module('portainer.agent')
.controller('NodeSelectorController', ['AgentService', 'Notifications', function (AgentService, Notifications) {
angular.module('portainer.agent').controller('NodeSelectorController', [
'AgentService',
'Notifications',
function (AgentService, Notifications) {
var ctrl = this;
this.$onInit = function() {
this.$onInit = function () {
AgentService.agents()
.then(function success(data) {
ctrl.agents = data;
@ -14,5 +16,5 @@ angular.module('portainer.agent')
Notifications.error('Failure', err, 'Unable to load agents');
});
};
}]);
},
]);

View File

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

View File

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

View File

@ -1,15 +1,20 @@
import _ from 'lodash-es';
angular.module('portainer.agent')
.controller('VolumeBrowserController', ['HttpRequestHelper', 'VolumeBrowserService', 'FileSaver', 'Blob', 'ModalService', 'Notifications',
function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) {
angular.module('portainer.agent').controller('VolumeBrowserController', [
'HttpRequestHelper',
'VolumeBrowserService',
'FileSaver',
'Blob',
'ModalService',
'Notifications',
function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) {
var ctrl = this;
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 newFilePath = this.state.path === '/' ? newName : this.state.path + '/' + newName;
@ -26,19 +31,18 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
});
};
this.delete = function(file) {
this.delete = function (file) {
var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
ModalService.confirmDeletion(
'Are you sure that you want to delete ' + filePath + ' ?',
function onConfirm(confirmed) {
if(!confirmed) { return; }
deleteFile(filePath);
ModalService.confirmDeletion('Are you sure that you want to delete ' + filePath + ' ?', function onConfirm(confirmed) {
if (!confirmed) {
return;
}
);
deleteFile(filePath);
});
};
this.download = function(file) {
this.download = function (file) {
var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
VolumeBrowserService.get(this.volumeId, filePath)
.then(function success(data) {
@ -50,12 +54,12 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
});
};
this.up = function() {
this.up = function () {
var parentFolder = parentPath(this.state.path);
browse(parentFolder);
};
this.browse = function(folder) {
this.browse = function (folder) {
var path = buildPath(this.state.path, folder);
browse(path);
};
@ -74,7 +78,6 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
});
}
function browse(path) {
VolumeBrowserService.ls(ctrl.volumeId, path)
.then(function success(data) {
@ -112,8 +115,7 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
return parent + '/' + file;
}
this.$onInit = function() {
this.$onInit = function () {
HttpRequestHelper.setPortainerAgentTargetHeader(this.nodeName);
VolumeBrowserService.ls(this.volumeId, this.state.path)
.then(function success(data) {
@ -131,7 +133,5 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
function refreshList() {
browse(ctrl.state.path);
}
}]);
},
]);

View File

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

View File

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

View File

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

View File

@ -1,10 +1,17 @@
angular.module('portainer.agent')
.factory('AgentVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
angular.module('portainer.agent').factory('AgentVersion1', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents', {
endpointId: EndpointProvider.endpointID
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';
angular.module('portainer.agent')
.factory('BrowseVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
angular.module('portainer.agent').factory('BrowseVersion1', [
'$resource',
'API_ENDPOINT_ENDPOINTS',
'EndpointProvider',
function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
'use strict';
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:volumeID/:action', {
endpointId: EndpointProvider.endpointID
return $resource(
API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/browse/:volumeID/:action',
{
endpointId: EndpointProvider.endpointID,
},
{
ls: {
method: 'GET', isArray: true, params: { action: 'ls' }
method: 'GET',
isArray: true,
params: { action: 'ls' },
},
get: {
method: 'GET', params: { action: 'get' },
method: 'GET',
params: { action: 'get' },
transformResponse: browseGetResponse,
responseType: 'arraybuffer'
responseType: 'arraybuffer',
},
delete: {
method: 'DELETE', params: { action: 'delete' }
method: 'DELETE',
params: { action: 'delete' },
},
rename: {
method: 'PUT', params: { action: 'rename' }
method: 'PUT',
params: { action: 'rename' },
},
}
});
}]);
);
},
]);

View File

@ -1,7 +1,12 @@
import { AgentViewModel } from '../models/agent';
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) {
'use strict';
var service = {};
@ -25,9 +30,10 @@ angular.module('portainer.agent').factory('AgentService', [
var agentVersion = getAgentApiVersion();
var service = agentVersion > 1 ? Agent : AgentVersion1;
service.query({ version: agentVersion })
service
.query({ version: agentVersion })
.$promise.then(function success(data) {
var agents = data.map(function(item) {
var agents = data.map(function (item) {
return new AgentViewModel(item);
});
deferred.resolve(agents);
@ -40,5 +46,5 @@ angular.module('portainer.agent').factory('AgentService', [
}
return service;
}
},
]);

View File

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

View File

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

View File

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

View File

@ -1,9 +1,17 @@
import $ from 'jquery';
import '@babel/polyfill'
import '@babel/polyfill';
angular.module('portainer')
.run(['$rootScope', '$state', '$interval', 'LocalStorage', 'EndpointProvider', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
angular.module('portainer').run([
'$rootScope',
'$state',
'$interval',
'LocalStorage',
'EndpointProvider',
'SystemService',
'cfpLoadingBar',
'$transitions',
'HttpRequestHelper',
function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
'use strict';
EndpointProvider.initialize();
@ -19,18 +27,18 @@ function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemS
}
};
$transitions.onBefore({}, function() {
$transitions.onBefore({}, function () {
HttpRequestHelper.resetAgentHeaders();
});
$state.defaultErrorHandler(function() {
$state.defaultErrorHandler(function () {
// Do not log transitionTo errors
});
// Keep-alive Edge endpoints by sending a ping request every minute
$interval(function() {
$interval(function () {
ping(EndpointProvider, SystemService);
}, 60 * 1000)
}, 60 * 1000);
$(document).ajaxSend(function (event, jqXhr, jqOpts) {
const type = jqOpts.type === 'POST' || jqOpts.type === 'PUT' || jqOpts.type === 'PATCH';
@ -40,7 +48,8 @@ function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemS
}
jqXhr.setRequestHeader('Authorization', 'Bearer ' + LocalStorage.getJWT());
});
}]);
},
]);
function ping(EndpointProvider, SystemService) {
let endpoint = EndpointProvider.currentEndpoint();

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