mirror of https://github.com/portainer/portainer
commit
13f712d06d
273
.eslintrc.yml
273
.eslintrc.yml
|
@ -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
|
||||
|
|
|
@ -5,3 +5,4 @@ portainer-checksum.txt
|
|||
api/cmd/portainer/portainer*
|
||||
.tmp
|
||||
.vscode
|
||||
.eslintcache
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"printWidth": 180,
|
||||
"singleQuote": true,
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
36
README.md
36
README.md
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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://") {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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{
|
||||
|
|
1458
api/portainer.go
1458
api/portainer.go
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"packageName": "portainer",
|
||||
"packageVersion": "1.23.2",
|
||||
"packageVersion": "1.24.0",
|
||||
"projectName": "portainer"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -2,6 +2,6 @@ angular.module('portainer.agent').component('fileUploader', {
|
|||
templateUrl: './file-uploader.html',
|
||||
controller: 'FileUploaderController',
|
||||
bindings: {
|
||||
uploadFile: '<onFileSelected'
|
||||
}
|
||||
uploadFile: '<onFileSelected',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -17,6 +17,6 @@ angular.module('portainer.agent').component('filesDatatable', {
|
|||
delete: '&',
|
||||
|
||||
isUploadAllowed: '<',
|
||||
onFileSelectedForUpload: '<'
|
||||
}
|
||||
onFileSelectedForUpload: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
angular.module('portainer.agent').component('hostBrowser', {
|
||||
controller: 'HostBrowserController',
|
||||
templateUrl: './host-browser.html',
|
||||
bindings: {}
|
||||
bindings: {},
|
||||
});
|
||||
|
|
|
@ -2,6 +2,6 @@ angular.module('portainer.agent').component('nodeSelector', {
|
|||
templateUrl: './nodeSelector.html',
|
||||
controller: 'NodeSelectorController',
|
||||
bindings: {
|
||||
model: '='
|
||||
}
|
||||
model: '=',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
||||
}]);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -4,6 +4,6 @@ angular.module('portainer.agent').component('volumeBrowser', {
|
|||
bindings: {
|
||||
volumeId: '<',
|
||||
nodeName: '<',
|
||||
isUploadEnabled: '<'
|
||||
}
|
||||
isUploadEnabled: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}]);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
}
|
||||
});
|
||||
}]);
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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' } },
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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' },
|
||||
},
|
||||
}
|
||||
});
|
||||
}]);
|
||||
);
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -10,5 +10,5 @@ angular.module('portainer.agent').service('AgentPingService', [
|
|||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
|
27
app/app.js
27
app/app.js
|
@ -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
Loading…
Reference in New Issue