Compare commits
292 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df3886fd25 | ||
|
|
f347d97daf | ||
|
|
d5cee5b8b1 | ||
|
|
4da6824bc7 | ||
|
|
80b6b6e300 | ||
|
|
484dab5932 | ||
|
|
f8bd075ce4 | ||
|
|
cd58c16b4e | ||
|
|
5ebb03cb4e | ||
|
|
dffcd3fdfd | ||
|
|
3f7687e78a | ||
|
|
0f58ece899 | ||
|
|
b0ad212858 | ||
|
|
7eb2fd3424 | ||
|
|
4c0d8ce732 | ||
|
|
e1cc4bc9a1 | ||
|
|
f2682d82b6 | ||
|
|
d3576fe8e6 | ||
|
|
13f712d06d | ||
|
|
bfcdeecac9 | ||
|
|
babc509115 | ||
|
|
ecbee3ee3d | ||
|
|
10772a3ecd | ||
|
|
2260107811 | ||
|
|
42e7db0ae7 | ||
|
|
ebac85b462 | ||
|
|
8eac1d2221 | ||
|
|
8e09b935cd | ||
|
|
9dcd223134 | ||
|
|
29c0584454 | ||
|
|
5c274f5b0c | ||
|
|
b3af91cea3 | ||
|
|
c8f55ac896 | ||
|
|
659e4486db | ||
|
|
cc091ee589 | ||
|
|
8046fb0438 | ||
|
|
7fa73d1147 | ||
|
|
bfd6cca33f | ||
|
|
7fe7ce1a0a | ||
|
|
7f0ce61413 | ||
|
|
3de533042d | ||
|
|
b2f36a3bbe | ||
|
|
3d5bdab620 | ||
|
|
fee20248ea | ||
|
|
f525c8d022 | ||
|
|
bba622a500 | ||
|
|
cf5056d9c0 | ||
|
|
6663073be1 | ||
|
|
18a38d597a | ||
|
|
aeea88be36 | ||
|
|
6da38d466b | ||
|
|
2542d30a09 | ||
|
|
df13f3b4cc | ||
|
|
db8b3d6e5a | ||
|
|
dd6262cf69 | ||
|
|
edd86f2506 | ||
|
|
fe89a4fc01 | ||
|
|
00bef100ee | ||
|
|
ae7f46c8ef | ||
|
|
f34d27df0f | ||
|
|
78558f9c8e | ||
|
|
5a3caab9c4 | ||
|
|
5396a069f2 | ||
|
|
2a92fcb802 | ||
|
|
2c400eb3b4 | ||
|
|
a11a348893 | ||
|
|
d022853059 | ||
|
|
bfdb4dba12 | ||
|
|
8d7bae0560 | ||
|
|
e0d83db609 | ||
|
|
ad5f51964c | ||
|
|
9cc8448418 | ||
|
|
b2cc6be007 | ||
|
|
f228b28639 | ||
|
|
be0b01611f | ||
|
|
bcda7e2d7e | ||
|
|
d0e998ddc4 | ||
|
|
1f7e5fec4f | ||
|
|
d3a625e22f | ||
|
|
eff1b79a4a | ||
|
|
0330b16776 | ||
|
|
97a0ea4a31 | ||
|
|
167d4319b5 | ||
|
|
6f59f130a1 | ||
|
|
cc8d3c8639 | ||
|
|
f4c461d7fb | ||
|
|
6c492d2290 | ||
|
|
8bea0988dd | ||
|
|
8dda67c8d0 | ||
|
|
7365afa1bb | ||
|
|
1ef29f2671 | ||
|
|
fa5bb9b1be | ||
|
|
2ba195adaa | ||
|
|
9da08bc792 | ||
|
|
17bc17f638 | ||
|
|
efae49d92b | ||
|
|
58c00401e9 | ||
|
|
e9f6861df0 | ||
|
|
d7b4e4aba1 | ||
|
|
bba13f69ad | ||
|
|
36020dd8bc | ||
|
|
b7eca7ce17 | ||
|
|
2189deb3bd | ||
|
|
29b7eeef5a | ||
|
|
f6cefb3318 | ||
|
|
a42619a442 | ||
|
|
1465825988 | ||
|
|
2d576394d0 | ||
|
|
f79dae3e27 | ||
|
|
badb6ee50f | ||
|
|
c2e1129804 | ||
|
|
3b1a8e4bba | ||
|
|
dd0c80e915 | ||
|
|
5ab63bd151 | ||
|
|
ea1ca76f70 | ||
|
|
e19bc8abc7 | ||
|
|
61c38534a7 | ||
|
|
7f54584ed6 | ||
|
|
1a65dbf85f | ||
|
|
a3a83d1d7e | ||
|
|
a41ca1fd46 | ||
|
|
130c188717 | ||
|
|
a85f0058ee | ||
|
|
8b0eb71d69 | ||
|
|
1f90a091a8 | ||
|
|
b8be795505 | ||
|
|
4239db7b34 | ||
|
|
81c0bf0632 | ||
|
|
9decbce511 | ||
|
|
914b46f813 | ||
|
|
19d4db13be | ||
|
|
198e92c734 | ||
|
|
03d9d6afbb | ||
|
|
c559b6b55c | ||
|
|
0175490161 | ||
|
|
310b6b34da | ||
|
|
07db1ca16e | ||
|
|
36de0aee7b | ||
|
|
c6e9d8e616 | ||
|
|
dbef3a0508 | ||
|
|
91c83eccd2 | ||
|
|
542b76912a | ||
|
|
53942b741a | ||
|
|
accca0f2a6 | ||
|
|
f67e866e7e | ||
|
|
2445a5aed5 | ||
|
|
8a8cef9b20 | ||
|
|
e20a139c5a | ||
|
|
774380fb44 | ||
|
|
7234c443e8 | ||
|
|
3632e07654 | ||
|
|
80ad5079f7 | ||
|
|
4fad28590d | ||
|
|
8de507a15d | ||
|
|
19810b9f4e | ||
|
|
ab2acea463 | ||
|
|
521a36e629 | ||
|
|
182f3734d0 | ||
|
|
d717ad947b | ||
|
|
9aa52a6975 | ||
|
|
ef4c138e03 | ||
|
|
68fe5d6906 | ||
|
|
b0f48ee3ad | ||
|
|
2912e78f68 | ||
|
|
fb6f6738d9 | ||
|
|
f7480c4ad4 | ||
|
|
1fbe6a12f1 | ||
|
|
b7c38b9569 | ||
|
|
6c996377f5 | ||
|
|
81e9484dd3 | ||
|
|
3ab0422361 | ||
|
|
d4fa4d8a52 | ||
|
|
ed70d0fb2b | ||
|
|
ea05d96c73 | ||
|
|
b034a60724 | ||
|
|
646038cd0f | ||
|
|
42d4e1e11c | ||
|
|
b84fa9db2f | ||
|
|
7509283072 | ||
|
|
1f68aad07f | ||
|
|
07505fabcc | ||
|
|
a5e5983c28 | ||
|
|
baa64ca927 | ||
|
|
8e922dbfc6 | ||
|
|
7d76bc89e7 | ||
|
|
7ebb3e62dd | ||
|
|
52704e681b | ||
|
|
ec19faaa24 | ||
|
|
628d4960cc | ||
|
|
2b48f1e49a | ||
|
|
849ff8cf9b | ||
|
|
a90fa857ee | ||
|
|
c34e83cafd | ||
|
|
ea6cddcfd3 | ||
|
|
96155ac97f | ||
|
|
c12ce5a5c7 | ||
|
|
552c897b3b | ||
|
|
24013bc524 | ||
|
|
3afeb13891 | ||
|
|
e11df28df6 | ||
|
|
a33dbd1e91 | ||
|
|
b537a9ad0d | ||
|
|
5b844fd40a | ||
|
|
7f2ef8fb06 | ||
|
|
a6692ee526 | ||
|
|
0b2a76d75a | ||
|
|
8cb18f9877 | ||
|
|
448003aaa4 | ||
|
|
12a512f01f | ||
|
|
2252ab9da7 | ||
|
|
7338e5fabd | ||
|
|
5b91b1a6c9 | ||
|
|
66b6a6cbbd | ||
|
|
1089846fd6 | ||
|
|
fbcffb7969 | ||
|
|
2bf125c8cc | ||
|
|
9ec83bb065 | ||
|
|
64d382f612 | ||
|
|
4fcd2e8afe | ||
|
|
16234aa0c1 | ||
|
|
03c82cac69 | ||
|
|
cc487ae68a | ||
|
|
90d3f3a358 | ||
|
|
d52a1a870c | ||
|
|
0b7500827b | ||
|
|
f71a565acc | ||
|
|
92a615d7b6 | ||
|
|
c432ead45f | ||
|
|
a856053338 | ||
|
|
afda5d07bf | ||
|
|
693182fbd3 | ||
|
|
d1fee6f119 | ||
|
|
4084e7c8ec | ||
|
|
f20526d662 | ||
|
|
3d4af7c54f | ||
|
|
1138fd5ab1 | ||
|
|
6591498ab9 | ||
|
|
7a8a54c96a | ||
|
|
b3c7c76be2 | ||
|
|
fb69ffa764 | ||
|
|
96f266adf6 | ||
|
|
f3b9668629 | ||
|
|
71b1da8d32 | ||
|
|
09cf55a7dc | ||
|
|
ead160f792 | ||
|
|
144e0ae07e | ||
|
|
67de71a18f | ||
|
|
9f817749c1 | ||
|
|
36198b57a5 | ||
|
|
70f9e37eab | ||
|
|
24f69f0185 | ||
|
|
4a2e9a892d | ||
|
|
1e4bedde4b | ||
|
|
9db07e7f4e | ||
|
|
d4b8d9947d | ||
|
|
9194ddcd03 | ||
|
|
e1edf37770 | ||
|
|
82b73c06b4 | ||
|
|
ae286998ab | ||
|
|
ffc3ed67e2 | ||
|
|
6252be4a08 | ||
|
|
75481d928e | ||
|
|
d65d65803f | ||
|
|
6a8fc253bd | ||
|
|
1fa8921c11 | ||
|
|
ba3892aebf | ||
|
|
27fc700d6f | ||
|
|
3187cb0ada | ||
|
|
b462a15921 | ||
|
|
11a646a076 | ||
|
|
4c1edaf251 | ||
|
|
b45c4f8bea | ||
|
|
968f070b0b | ||
|
|
e679483ffd | ||
|
|
494ef6c392 | ||
|
|
7951c1d150 | ||
|
|
0083bdb6a2 | ||
|
|
e65881223d | ||
|
|
f0c9058568 | ||
|
|
afd654e4a8 | ||
|
|
0ef116d6b5 | ||
|
|
ee64a53782 | ||
|
|
4a0c899df5 | ||
|
|
840603bb8c | ||
|
|
5fe79621a6 | ||
|
|
d3b9822105 | ||
|
|
9163bcae25 | ||
|
|
17dad6e36f | ||
|
|
75b836453a | ||
|
|
b8866d487f | ||
|
|
dfb3b3aaa1 | ||
|
|
09fdc9781c |
2
.babelrc
2
.babelrc
@@ -5,7 +5,7 @@
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"useBuiltIns": "usage"
|
||||
"useBuiltIns": "entry"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
275
.eslintrc.yml
275
.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: error
|
||||
# no-debugger: error
|
||||
# no-dupe-args: error
|
||||
# no-dupe-keys: error
|
||||
# no-duplicate-case: error
|
||||
# no-empty-character-class: error
|
||||
no-control-regex: off
|
||||
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
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -21,24 +21,22 @@ Also, be sure to check our FAQ and documentation first: https://portainer.readth
|
||||
-->
|
||||
|
||||
**Bug description**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
Briefly describe what you were expecting.
|
||||
**Portainer Logs**
|
||||
Provide the logs of your Portainer container or Service.
|
||||
You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Technical details:**
|
||||
|
||||
* Portainer version:
|
||||
* Docker version (managed by Portainer):
|
||||
* Platform (windows/linux):
|
||||
|
||||
55
.github/stale.yml
vendored
Normal file
55
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
# Config for Stalebot, limited to only `issues`
|
||||
only: issues
|
||||
|
||||
# Issues config
|
||||
issues:
|
||||
daysUntilStale: 60
|
||||
daysUntilClose: 7
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 30
|
||||
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- kind/enhancement
|
||||
- kind/feature
|
||||
- kind/question
|
||||
- kind/style
|
||||
- kind/workaround
|
||||
- bug/need-confirmation
|
||||
- bug/confirmed
|
||||
- status/discuss
|
||||
|
||||
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: true
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: true
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: true
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: status/stale
|
||||
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been marked as stale as it has not had recent activity,
|
||||
it will be closed if no further activity occurs in the next 7 days.
|
||||
If you believe that it has been incorrectly labelled as stale,
|
||||
leave a comment and the label will be removed.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
# unmarkComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: >
|
||||
Since no further activity has appeared on this issue it will be closed.
|
||||
If you believe that it has been incorrectly closed, leave a comment
|
||||
and mention @itsconquest. One of our staff will then review the issue.
|
||||
|
||||
Note - If it is an old bug report, make sure that it is reproduceable in the
|
||||
latest version of Portainer as it may have already been fixed.
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ dist
|
||||
portainer-checksum.txt
|
||||
api/cmd/portainer/portainer*
|
||||
.tmp
|
||||
.vscode
|
||||
.vscode
|
||||
.eslintcache
|
||||
13
.prettierrc
Normal file
13
.prettierrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"printWidth": 180,
|
||||
"singleQuote": true,
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"options": {
|
||||
"parser": "angular"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,30 +6,16 @@ Some basic conventions for contributing to this project.
|
||||
|
||||
Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
|
||||
|
||||
* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
|
||||
* Develop in a topic branch, not master/develop
|
||||
- Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
|
||||
- Develop in a topic branch, not master/develop
|
||||
|
||||
When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
|
||||
When creating a new branch, prefix it with the _type_ of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
|
||||
|
||||
For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -51,14 +37,14 @@ Lines should not exceed 100 characters. This allows the message to be easier to
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
* **feat**: A new feature
|
||||
* **fix**: A bug fix
|
||||
* **docs**: Documentation only changes
|
||||
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
||||
- **feat**: A new feature
|
||||
- **fix**: A bug fix
|
||||
- **docs**: Documentation only changes
|
||||
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
|
||||
semi-colons, etc)
|
||||
* **refactor**: A code change that neither fixes a bug or adds a feature
|
||||
* **test**: Adding missing tests
|
||||
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
|
||||
- **refactor**: A code change that neither fixes a bug or adds a feature
|
||||
- **test**: Adding missing tests
|
||||
- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
|
||||
generation
|
||||
|
||||
### Scope
|
||||
@@ -71,9 +57,9 @@ You can use the **area** label tag associated on the issue here (for `area/conta
|
||||
|
||||
The subject contains succinct description of the change:
|
||||
|
||||
* use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
* don't capitalize first letter
|
||||
* no dot (.) at the end
|
||||
- use the imperative, present tense: "change" not "changed" nor "changes"
|
||||
- don't capitalize first letter
|
||||
- no dot (.) at the end
|
||||
|
||||
## Contribution process
|
||||
|
||||
|
||||
38
README.md
38
README.md
@@ -3,15 +3,14 @@
|
||||
</p>
|
||||
|
||||
[](https://hub.docker.com/r/portainer/portainer/)
|
||||
[](http://microbadger.com/images/portainer/portainer "Image size")
|
||||
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
||||
[](http://microbadger.com/images/portainer/portainer 'Image size')
|
||||
[](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
|
||||
[](https://codeclimate.com/github/portainer/portainer)
|
||||
[](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 your 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,32 +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>.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -64,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]
|
||||
|
||||
154
api/access_control.go
Normal file
154
api/access_control.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package portainer
|
||||
|
||||
// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the
|
||||
// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter.
|
||||
func NewPrivateResourceControl(resourceIdentifier string, resourceType ResourceControlType, userID UserID) *ResourceControl {
|
||||
return &ResourceControl{
|
||||
Type: resourceType,
|
||||
ResourceID: resourceIdentifier,
|
||||
SubResourceIDs: []string{},
|
||||
UserAccesses: []UserResourceAccess{
|
||||
{
|
||||
UserID: userID,
|
||||
AccessLevel: ReadWriteAccessLevel,
|
||||
},
|
||||
},
|
||||
TeamAccesses: []TeamResourceAccess{},
|
||||
AdministratorsOnly: false,
|
||||
Public: false,
|
||||
System: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSystemResourceControl will create a new public resource control with the System flag set to true.
|
||||
// These kind of resource control are not persisted and are created on the fly by the Portainer API.
|
||||
func NewSystemResourceControl(resourceIdentifier string, resourceType ResourceControlType) *ResourceControl {
|
||||
return &ResourceControl{
|
||||
Type: resourceType,
|
||||
ResourceID: resourceIdentifier,
|
||||
SubResourceIDs: []string{},
|
||||
UserAccesses: []UserResourceAccess{},
|
||||
TeamAccesses: []TeamResourceAccess{},
|
||||
AdministratorsOnly: false,
|
||||
Public: true,
|
||||
System: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPublicResourceControl will create a new public resource control.
|
||||
func NewPublicResourceControl(resourceIdentifier string, resourceType ResourceControlType) *ResourceControl {
|
||||
return &ResourceControl{
|
||||
Type: resourceType,
|
||||
ResourceID: resourceIdentifier,
|
||||
SubResourceIDs: []string{},
|
||||
UserAccesses: []UserResourceAccess{},
|
||||
TeamAccesses: []TeamResourceAccess{},
|
||||
AdministratorsOnly: false,
|
||||
Public: true,
|
||||
System: false,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRestrictedResourceControl will create a new resource control with user and team accesses restrictions.
|
||||
func NewRestrictedResourceControl(resourceIdentifier string, resourceType ResourceControlType, userIDs []UserID, teamIDs []TeamID) *ResourceControl {
|
||||
userAccesses := make([]UserResourceAccess, 0)
|
||||
teamAccesses := make([]TeamResourceAccess, 0)
|
||||
|
||||
for _, id := range userIDs {
|
||||
access := UserResourceAccess{
|
||||
UserID: id,
|
||||
AccessLevel: ReadWriteAccessLevel,
|
||||
}
|
||||
|
||||
userAccesses = append(userAccesses, access)
|
||||
}
|
||||
|
||||
for _, id := range teamIDs {
|
||||
access := TeamResourceAccess{
|
||||
TeamID: id,
|
||||
AccessLevel: ReadWriteAccessLevel,
|
||||
}
|
||||
|
||||
teamAccesses = append(teamAccesses, access)
|
||||
}
|
||||
|
||||
return &ResourceControl{
|
||||
Type: resourceType,
|
||||
ResourceID: resourceIdentifier,
|
||||
SubResourceIDs: []string{},
|
||||
UserAccesses: userAccesses,
|
||||
TeamAccesses: teamAccesses,
|
||||
AdministratorsOnly: false,
|
||||
Public: false,
|
||||
System: false,
|
||||
}
|
||||
}
|
||||
|
||||
// DecorateStacks will iterate through a list of stacks, check for an associated resource control for each
|
||||
// stack and decorate the stack element if a resource control is found.
|
||||
func DecorateStacks(stacks []Stack, resourceControls []ResourceControl) []Stack {
|
||||
for idx, stack := range stacks {
|
||||
|
||||
resourceControl := GetResourceControlByResourceIDAndType(stack.Name, StackResourceControl, resourceControls)
|
||||
if resourceControl != nil {
|
||||
stacks[idx].ResourceControl = resourceControl
|
||||
}
|
||||
}
|
||||
|
||||
return stacks
|
||||
}
|
||||
|
||||
// FilterAuthorizedStacks returns a list of decorated stacks filtered through resource control access checks.
|
||||
func FilterAuthorizedStacks(stacks []Stack, user *User, userTeamIDs []TeamID, rbacEnabled bool) []Stack {
|
||||
authorizedStacks := make([]Stack, 0)
|
||||
|
||||
for _, stack := range stacks {
|
||||
_, ok := user.EndpointAuthorizations[stack.EndpointID][EndpointResourcesAccess]
|
||||
if rbacEnabled && ok {
|
||||
authorizedStacks = append(authorizedStacks, stack)
|
||||
continue
|
||||
}
|
||||
|
||||
if stack.ResourceControl != nil && UserCanAccessResource(user.ID, userTeamIDs, stack.ResourceControl) {
|
||||
authorizedStacks = append(authorizedStacks, stack)
|
||||
}
|
||||
}
|
||||
|
||||
return authorizedStacks
|
||||
}
|
||||
|
||||
// UserCanAccessResource will valide that a user has permissions defined in the specified resource control
|
||||
// based on its identifier and the team(s) he is part of.
|
||||
func UserCanAccessResource(userID UserID, userTeamIDs []TeamID, resourceControl *ResourceControl) bool {
|
||||
for _, authorizedUserAccess := range resourceControl.UserAccesses {
|
||||
if userID == authorizedUserAccess.UserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, authorizedTeamAccess := range resourceControl.TeamAccesses {
|
||||
for _, userTeamID := range userTeamIDs {
|
||||
if userTeamID == authorizedTeamAccess.TeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resourceControl.Public
|
||||
}
|
||||
|
||||
// GetResourceControlByResourceIDAndType retrieves the first matching resource control in a set of resource controls
|
||||
// based on the specified id and resource type parameters.
|
||||
func GetResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType, resourceControls []ResourceControl) *ResourceControl {
|
||||
for _, resourceControl := range resourceControls {
|
||||
if resourceID == resourceControl.ResourceID && resourceType == resourceControl.Type {
|
||||
return &resourceControl
|
||||
}
|
||||
for _, subResourceID := range resourceControl.SubResourceIDs {
|
||||
if resourceID == subResourceID {
|
||||
return &resourceControl
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,32 +17,38 @@ func UnzipArchive(archiveData []byte, dest string) error {
|
||||
}
|
||||
|
||||
for _, zipFile := range zipReader.File {
|
||||
|
||||
f, err := zipFile.Open()
|
||||
err := extractFileFromArchive(zipFile, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fpath := filepath.Join(dest, zipFile.Name)
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outFile.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFileFromArchive(file *zip.File, dest string) error {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fpath := filepath.Join(dest, file.Name)
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return outFile.Close()
|
||||
}
|
||||
|
||||
795
api/authorizations.go
Normal file
795
api/authorizations.go
Normal file
@@ -0,0 +1,795 @@
|
||||
package portainer
|
||||
|
||||
// AuthorizationService represents a service used to
|
||||
// update authorizations associated to a user or team.
|
||||
type AuthorizationService struct {
|
||||
endpointService EndpointService
|
||||
endpointGroupService EndpointGroupService
|
||||
registryService RegistryService
|
||||
roleService RoleService
|
||||
teamMembershipService TeamMembershipService
|
||||
userService UserService
|
||||
}
|
||||
|
||||
// AuthorizationServiceParameters are the required parameters
|
||||
// used to create a new AuthorizationService.
|
||||
type AuthorizationServiceParameters struct {
|
||||
EndpointService EndpointService
|
||||
EndpointGroupService EndpointGroupService
|
||||
RegistryService RegistryService
|
||||
RoleService RoleService
|
||||
TeamMembershipService TeamMembershipService
|
||||
UserService UserService
|
||||
}
|
||||
|
||||
// NewAuthorizationService returns a point to a new AuthorizationService instance.
|
||||
func NewAuthorizationService(parameters *AuthorizationServiceParameters) *AuthorizationService {
|
||||
return &AuthorizationService{
|
||||
endpointService: parameters.EndpointService,
|
||||
endpointGroupService: parameters.EndpointGroupService,
|
||||
registryService: parameters.RegistryService,
|
||||
roleService: parameters.RoleService,
|
||||
teamMembershipService: parameters.TeamMembershipService,
|
||||
userService: parameters.UserService,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultEndpointAuthorizationsForEndpointAdministratorRole returns the default endpoint authorizations
|
||||
// associated to the endpoint administrator role.
|
||||
func DefaultEndpointAuthorizationsForEndpointAdministratorRole() Authorizations {
|
||||
return map[Authorization]bool{
|
||||
OperationDockerContainerArchiveInfo: true,
|
||||
OperationDockerContainerList: true,
|
||||
OperationDockerContainerExport: true,
|
||||
OperationDockerContainerChanges: true,
|
||||
OperationDockerContainerInspect: true,
|
||||
OperationDockerContainerTop: true,
|
||||
OperationDockerContainerLogs: true,
|
||||
OperationDockerContainerStats: true,
|
||||
OperationDockerContainerAttachWebsocket: true,
|
||||
OperationDockerContainerArchive: true,
|
||||
OperationDockerContainerCreate: true,
|
||||
OperationDockerContainerPrune: true,
|
||||
OperationDockerContainerKill: true,
|
||||
OperationDockerContainerPause: true,
|
||||
OperationDockerContainerUnpause: true,
|
||||
OperationDockerContainerRestart: true,
|
||||
OperationDockerContainerStart: true,
|
||||
OperationDockerContainerStop: true,
|
||||
OperationDockerContainerWait: true,
|
||||
OperationDockerContainerResize: true,
|
||||
OperationDockerContainerAttach: true,
|
||||
OperationDockerContainerExec: true,
|
||||
OperationDockerContainerRename: true,
|
||||
OperationDockerContainerUpdate: true,
|
||||
OperationDockerContainerPutContainerArchive: true,
|
||||
OperationDockerContainerDelete: true,
|
||||
OperationDockerImageList: true,
|
||||
OperationDockerImageSearch: true,
|
||||
OperationDockerImageGetAll: true,
|
||||
OperationDockerImageGet: true,
|
||||
OperationDockerImageHistory: true,
|
||||
OperationDockerImageInspect: true,
|
||||
OperationDockerImageLoad: true,
|
||||
OperationDockerImageCreate: true,
|
||||
OperationDockerImagePrune: true,
|
||||
OperationDockerImagePush: true,
|
||||
OperationDockerImageTag: true,
|
||||
OperationDockerImageDelete: true,
|
||||
OperationDockerImageCommit: true,
|
||||
OperationDockerImageBuild: true,
|
||||
OperationDockerNetworkList: true,
|
||||
OperationDockerNetworkInspect: true,
|
||||
OperationDockerNetworkCreate: true,
|
||||
OperationDockerNetworkConnect: true,
|
||||
OperationDockerNetworkDisconnect: true,
|
||||
OperationDockerNetworkPrune: true,
|
||||
OperationDockerNetworkDelete: true,
|
||||
OperationDockerVolumeList: true,
|
||||
OperationDockerVolumeInspect: true,
|
||||
OperationDockerVolumeCreate: true,
|
||||
OperationDockerVolumePrune: true,
|
||||
OperationDockerVolumeDelete: true,
|
||||
OperationDockerExecInspect: true,
|
||||
OperationDockerExecStart: true,
|
||||
OperationDockerExecResize: true,
|
||||
OperationDockerSwarmInspect: true,
|
||||
OperationDockerSwarmUnlockKey: true,
|
||||
OperationDockerSwarmInit: true,
|
||||
OperationDockerSwarmJoin: true,
|
||||
OperationDockerSwarmLeave: true,
|
||||
OperationDockerSwarmUpdate: true,
|
||||
OperationDockerSwarmUnlock: true,
|
||||
OperationDockerNodeList: true,
|
||||
OperationDockerNodeInspect: true,
|
||||
OperationDockerNodeUpdate: true,
|
||||
OperationDockerNodeDelete: true,
|
||||
OperationDockerServiceList: true,
|
||||
OperationDockerServiceInspect: true,
|
||||
OperationDockerServiceLogs: true,
|
||||
OperationDockerServiceCreate: true,
|
||||
OperationDockerServiceUpdate: true,
|
||||
OperationDockerServiceDelete: true,
|
||||
OperationDockerSecretList: true,
|
||||
OperationDockerSecretInspect: true,
|
||||
OperationDockerSecretCreate: true,
|
||||
OperationDockerSecretUpdate: true,
|
||||
OperationDockerSecretDelete: true,
|
||||
OperationDockerConfigList: true,
|
||||
OperationDockerConfigInspect: true,
|
||||
OperationDockerConfigCreate: true,
|
||||
OperationDockerConfigUpdate: true,
|
||||
OperationDockerConfigDelete: true,
|
||||
OperationDockerTaskList: true,
|
||||
OperationDockerTaskInspect: true,
|
||||
OperationDockerTaskLogs: true,
|
||||
OperationDockerPluginList: true,
|
||||
OperationDockerPluginPrivileges: true,
|
||||
OperationDockerPluginInspect: true,
|
||||
OperationDockerPluginPull: true,
|
||||
OperationDockerPluginCreate: true,
|
||||
OperationDockerPluginEnable: true,
|
||||
OperationDockerPluginDisable: true,
|
||||
OperationDockerPluginPush: true,
|
||||
OperationDockerPluginUpgrade: true,
|
||||
OperationDockerPluginSet: true,
|
||||
OperationDockerPluginDelete: true,
|
||||
OperationDockerSessionStart: true,
|
||||
OperationDockerDistributionInspect: true,
|
||||
OperationDockerBuildPrune: true,
|
||||
OperationDockerBuildCancel: true,
|
||||
OperationDockerPing: true,
|
||||
OperationDockerInfo: true,
|
||||
OperationDockerVersion: true,
|
||||
OperationDockerEvents: true,
|
||||
OperationDockerSystem: true,
|
||||
OperationDockerUndefined: true,
|
||||
OperationDockerAgentPing: true,
|
||||
OperationDockerAgentList: true,
|
||||
OperationDockerAgentHostInfo: true,
|
||||
OperationDockerAgentBrowseDelete: true,
|
||||
OperationDockerAgentBrowseGet: true,
|
||||
OperationDockerAgentBrowseList: true,
|
||||
OperationDockerAgentBrowsePut: true,
|
||||
OperationDockerAgentBrowseRename: true,
|
||||
OperationDockerAgentUndefined: true,
|
||||
OperationPortainerResourceControlCreate: true,
|
||||
OperationPortainerResourceControlUpdate: true,
|
||||
OperationPortainerStackList: true,
|
||||
OperationPortainerStackInspect: true,
|
||||
OperationPortainerStackFile: true,
|
||||
OperationPortainerStackCreate: true,
|
||||
OperationPortainerStackMigrate: true,
|
||||
OperationPortainerStackUpdate: true,
|
||||
OperationPortainerStackDelete: true,
|
||||
OperationPortainerWebsocketExec: true,
|
||||
OperationPortainerWebhookList: true,
|
||||
OperationPortainerWebhookCreate: true,
|
||||
OperationPortainerWebhookDelete: true,
|
||||
OperationIntegrationStoridgeAdmin: true,
|
||||
EndpointResourcesAccess: true,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultEndpointAuthorizationsForHelpDeskRole returns the default endpoint authorizations
|
||||
// associated to the helpdesk role.
|
||||
func DefaultEndpointAuthorizationsForHelpDeskRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||
authorizations := map[Authorization]bool{
|
||||
OperationDockerContainerArchiveInfo: true,
|
||||
OperationDockerContainerList: true,
|
||||
OperationDockerContainerChanges: true,
|
||||
OperationDockerContainerInspect: true,
|
||||
OperationDockerContainerTop: true,
|
||||
OperationDockerContainerLogs: true,
|
||||
OperationDockerContainerStats: true,
|
||||
OperationDockerImageList: true,
|
||||
OperationDockerImageSearch: true,
|
||||
OperationDockerImageGetAll: true,
|
||||
OperationDockerImageGet: true,
|
||||
OperationDockerImageHistory: true,
|
||||
OperationDockerImageInspect: true,
|
||||
OperationDockerNetworkList: true,
|
||||
OperationDockerNetworkInspect: true,
|
||||
OperationDockerVolumeList: true,
|
||||
OperationDockerVolumeInspect: true,
|
||||
OperationDockerSwarmInspect: true,
|
||||
OperationDockerNodeList: true,
|
||||
OperationDockerNodeInspect: true,
|
||||
OperationDockerServiceList: true,
|
||||
OperationDockerServiceInspect: true,
|
||||
OperationDockerServiceLogs: true,
|
||||
OperationDockerSecretList: true,
|
||||
OperationDockerSecretInspect: true,
|
||||
OperationDockerConfigList: true,
|
||||
OperationDockerConfigInspect: true,
|
||||
OperationDockerTaskList: true,
|
||||
OperationDockerTaskInspect: true,
|
||||
OperationDockerTaskLogs: true,
|
||||
OperationDockerPluginList: true,
|
||||
OperationDockerDistributionInspect: true,
|
||||
OperationDockerPing: true,
|
||||
OperationDockerInfo: true,
|
||||
OperationDockerVersion: true,
|
||||
OperationDockerEvents: true,
|
||||
OperationDockerSystem: true,
|
||||
OperationDockerAgentPing: true,
|
||||
OperationDockerAgentList: true,
|
||||
OperationDockerAgentHostInfo: true,
|
||||
OperationPortainerStackList: true,
|
||||
OperationPortainerStackInspect: true,
|
||||
OperationPortainerStackFile: true,
|
||||
OperationPortainerWebhookList: true,
|
||||
EndpointResourcesAccess: true,
|
||||
}
|
||||
|
||||
if volumeBrowsingAuthorizations {
|
||||
authorizations[OperationDockerAgentBrowseGet] = true
|
||||
authorizations[OperationDockerAgentBrowseList] = true
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
// DefaultEndpointAuthorizationsForStandardUserRole returns the default endpoint authorizations
|
||||
// associated to the standard user role.
|
||||
func DefaultEndpointAuthorizationsForStandardUserRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||
authorizations := map[Authorization]bool{
|
||||
OperationDockerContainerArchiveInfo: true,
|
||||
OperationDockerContainerList: true,
|
||||
OperationDockerContainerExport: true,
|
||||
OperationDockerContainerChanges: true,
|
||||
OperationDockerContainerInspect: true,
|
||||
OperationDockerContainerTop: true,
|
||||
OperationDockerContainerLogs: true,
|
||||
OperationDockerContainerStats: true,
|
||||
OperationDockerContainerAttachWebsocket: true,
|
||||
OperationDockerContainerArchive: true,
|
||||
OperationDockerContainerCreate: true,
|
||||
OperationDockerContainerKill: true,
|
||||
OperationDockerContainerPause: true,
|
||||
OperationDockerContainerUnpause: true,
|
||||
OperationDockerContainerRestart: true,
|
||||
OperationDockerContainerStart: true,
|
||||
OperationDockerContainerStop: true,
|
||||
OperationDockerContainerWait: true,
|
||||
OperationDockerContainerResize: true,
|
||||
OperationDockerContainerAttach: true,
|
||||
OperationDockerContainerExec: true,
|
||||
OperationDockerContainerRename: true,
|
||||
OperationDockerContainerUpdate: true,
|
||||
OperationDockerContainerPutContainerArchive: true,
|
||||
OperationDockerContainerDelete: true,
|
||||
OperationDockerImageList: true,
|
||||
OperationDockerImageSearch: true,
|
||||
OperationDockerImageGetAll: true,
|
||||
OperationDockerImageGet: true,
|
||||
OperationDockerImageHistory: true,
|
||||
OperationDockerImageInspect: true,
|
||||
OperationDockerImageLoad: true,
|
||||
OperationDockerImageCreate: true,
|
||||
OperationDockerImagePush: true,
|
||||
OperationDockerImageTag: true,
|
||||
OperationDockerImageDelete: true,
|
||||
OperationDockerImageCommit: true,
|
||||
OperationDockerImageBuild: true,
|
||||
OperationDockerNetworkList: true,
|
||||
OperationDockerNetworkInspect: true,
|
||||
OperationDockerNetworkCreate: true,
|
||||
OperationDockerNetworkConnect: true,
|
||||
OperationDockerNetworkDisconnect: true,
|
||||
OperationDockerNetworkDelete: true,
|
||||
OperationDockerVolumeList: true,
|
||||
OperationDockerVolumeInspect: true,
|
||||
OperationDockerVolumeCreate: true,
|
||||
OperationDockerVolumeDelete: true,
|
||||
OperationDockerExecInspect: true,
|
||||
OperationDockerExecStart: true,
|
||||
OperationDockerExecResize: true,
|
||||
OperationDockerSwarmInspect: true,
|
||||
OperationDockerSwarmUnlockKey: true,
|
||||
OperationDockerSwarmInit: true,
|
||||
OperationDockerSwarmJoin: true,
|
||||
OperationDockerSwarmLeave: true,
|
||||
OperationDockerSwarmUpdate: true,
|
||||
OperationDockerSwarmUnlock: true,
|
||||
OperationDockerNodeList: true,
|
||||
OperationDockerNodeInspect: true,
|
||||
OperationDockerNodeUpdate: true,
|
||||
OperationDockerNodeDelete: true,
|
||||
OperationDockerServiceList: true,
|
||||
OperationDockerServiceInspect: true,
|
||||
OperationDockerServiceLogs: true,
|
||||
OperationDockerServiceCreate: true,
|
||||
OperationDockerServiceUpdate: true,
|
||||
OperationDockerServiceDelete: true,
|
||||
OperationDockerSecretList: true,
|
||||
OperationDockerSecretInspect: true,
|
||||
OperationDockerSecretCreate: true,
|
||||
OperationDockerSecretUpdate: true,
|
||||
OperationDockerSecretDelete: true,
|
||||
OperationDockerConfigList: true,
|
||||
OperationDockerConfigInspect: true,
|
||||
OperationDockerConfigCreate: true,
|
||||
OperationDockerConfigUpdate: true,
|
||||
OperationDockerConfigDelete: true,
|
||||
OperationDockerTaskList: true,
|
||||
OperationDockerTaskInspect: true,
|
||||
OperationDockerTaskLogs: true,
|
||||
OperationDockerPluginList: true,
|
||||
OperationDockerPluginPrivileges: true,
|
||||
OperationDockerPluginInspect: true,
|
||||
OperationDockerPluginPull: true,
|
||||
OperationDockerPluginCreate: true,
|
||||
OperationDockerPluginEnable: true,
|
||||
OperationDockerPluginDisable: true,
|
||||
OperationDockerPluginPush: true,
|
||||
OperationDockerPluginUpgrade: true,
|
||||
OperationDockerPluginSet: true,
|
||||
OperationDockerPluginDelete: true,
|
||||
OperationDockerSessionStart: true,
|
||||
OperationDockerDistributionInspect: true,
|
||||
OperationDockerBuildPrune: true,
|
||||
OperationDockerBuildCancel: true,
|
||||
OperationDockerPing: true,
|
||||
OperationDockerInfo: true,
|
||||
OperationDockerVersion: true,
|
||||
OperationDockerEvents: true,
|
||||
OperationDockerSystem: true,
|
||||
OperationDockerUndefined: true,
|
||||
OperationDockerAgentPing: true,
|
||||
OperationDockerAgentList: true,
|
||||
OperationDockerAgentHostInfo: true,
|
||||
OperationDockerAgentUndefined: true,
|
||||
OperationPortainerResourceControlUpdate: true,
|
||||
OperationPortainerStackList: true,
|
||||
OperationPortainerStackInspect: true,
|
||||
OperationPortainerStackFile: true,
|
||||
OperationPortainerStackCreate: true,
|
||||
OperationPortainerStackMigrate: true,
|
||||
OperationPortainerStackUpdate: true,
|
||||
OperationPortainerStackDelete: true,
|
||||
OperationPortainerWebsocketExec: true,
|
||||
OperationPortainerWebhookList: true,
|
||||
OperationPortainerWebhookCreate: true,
|
||||
}
|
||||
|
||||
if volumeBrowsingAuthorizations {
|
||||
authorizations[OperationDockerAgentBrowseGet] = true
|
||||
authorizations[OperationDockerAgentBrowseList] = true
|
||||
authorizations[OperationDockerAgentBrowseDelete] = true
|
||||
authorizations[OperationDockerAgentBrowsePut] = true
|
||||
authorizations[OperationDockerAgentBrowseRename] = true
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
// DefaultEndpointAuthorizationsForReadOnlyUserRole returns the default endpoint authorizations
|
||||
// associated to the readonly user role.
|
||||
func DefaultEndpointAuthorizationsForReadOnlyUserRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||
authorizations := map[Authorization]bool{
|
||||
OperationDockerContainerArchiveInfo: true,
|
||||
OperationDockerContainerList: true,
|
||||
OperationDockerContainerChanges: true,
|
||||
OperationDockerContainerInspect: true,
|
||||
OperationDockerContainerTop: true,
|
||||
OperationDockerContainerLogs: true,
|
||||
OperationDockerContainerStats: true,
|
||||
OperationDockerImageList: true,
|
||||
OperationDockerImageSearch: true,
|
||||
OperationDockerImageGetAll: true,
|
||||
OperationDockerImageGet: true,
|
||||
OperationDockerImageHistory: true,
|
||||
OperationDockerImageInspect: true,
|
||||
OperationDockerNetworkList: true,
|
||||
OperationDockerNetworkInspect: true,
|
||||
OperationDockerVolumeList: true,
|
||||
OperationDockerVolumeInspect: true,
|
||||
OperationDockerSwarmInspect: true,
|
||||
OperationDockerNodeList: true,
|
||||
OperationDockerNodeInspect: true,
|
||||
OperationDockerServiceList: true,
|
||||
OperationDockerServiceInspect: true,
|
||||
OperationDockerServiceLogs: true,
|
||||
OperationDockerSecretList: true,
|
||||
OperationDockerSecretInspect: true,
|
||||
OperationDockerConfigList: true,
|
||||
OperationDockerConfigInspect: true,
|
||||
OperationDockerTaskList: true,
|
||||
OperationDockerTaskInspect: true,
|
||||
OperationDockerTaskLogs: true,
|
||||
OperationDockerPluginList: true,
|
||||
OperationDockerDistributionInspect: true,
|
||||
OperationDockerPing: true,
|
||||
OperationDockerInfo: true,
|
||||
OperationDockerVersion: true,
|
||||
OperationDockerEvents: true,
|
||||
OperationDockerSystem: true,
|
||||
OperationDockerAgentPing: true,
|
||||
OperationDockerAgentList: true,
|
||||
OperationDockerAgentHostInfo: true,
|
||||
OperationPortainerStackList: true,
|
||||
OperationPortainerStackInspect: true,
|
||||
OperationPortainerStackFile: true,
|
||||
OperationPortainerWebhookList: true,
|
||||
}
|
||||
|
||||
if volumeBrowsingAuthorizations {
|
||||
authorizations[OperationDockerAgentBrowseGet] = true
|
||||
authorizations[OperationDockerAgentBrowseList] = true
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
|
||||
func DefaultPortainerAuthorizations() Authorizations {
|
||||
return map[Authorization]bool{
|
||||
OperationPortainerDockerHubInspect: true,
|
||||
OperationPortainerEndpointGroupList: true,
|
||||
OperationPortainerEndpointList: true,
|
||||
OperationPortainerEndpointInspect: true,
|
||||
OperationPortainerEndpointExtensionAdd: true,
|
||||
OperationPortainerEndpointExtensionRemove: true,
|
||||
OperationPortainerExtensionList: true,
|
||||
OperationPortainerMOTD: true,
|
||||
OperationPortainerRegistryList: true,
|
||||
OperationPortainerRegistryInspect: true,
|
||||
OperationPortainerTeamList: true,
|
||||
OperationPortainerTemplateList: true,
|
||||
OperationPortainerTemplateInspect: true,
|
||||
OperationPortainerUserList: true,
|
||||
OperationPortainerUserInspect: true,
|
||||
OperationPortainerUserMemberships: true,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateVolumeBrowsingAuthorizations will update all the volume browsing authorizations for each role (except endpoint administrator)
|
||||
// based on the specified removeAuthorizations parameter. If removeAuthorizations is set to true, all
|
||||
// the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations
|
||||
// will be reset based for each role.
|
||||
func (service AuthorizationService) UpdateVolumeBrowsingAuthorizations(remove bool) error {
|
||||
roles, err := service.roleService.Roles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
// all roles except endpoint administrator
|
||||
if role.ID != RoleID(1) {
|
||||
updateRoleVolumeBrowsingAuthorizations(&role, remove)
|
||||
|
||||
err := service.roleService.UpdateRole(role.ID, &role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateRoleVolumeBrowsingAuthorizations(role *Role, removeAuthorizations bool) {
|
||||
if !removeAuthorizations {
|
||||
delete(role.Authorizations, OperationDockerAgentBrowseDelete)
|
||||
delete(role.Authorizations, OperationDockerAgentBrowseGet)
|
||||
delete(role.Authorizations, OperationDockerAgentBrowseList)
|
||||
delete(role.Authorizations, OperationDockerAgentBrowsePut)
|
||||
delete(role.Authorizations, OperationDockerAgentBrowseRename)
|
||||
return
|
||||
}
|
||||
|
||||
role.Authorizations[OperationDockerAgentBrowseGet] = true
|
||||
role.Authorizations[OperationDockerAgentBrowseList] = true
|
||||
|
||||
// Standard-user
|
||||
if role.ID == RoleID(3) {
|
||||
role.Authorizations[OperationDockerAgentBrowseDelete] = true
|
||||
role.Authorizations[OperationDockerAgentBrowsePut] = true
|
||||
role.Authorizations[OperationDockerAgentBrowseRename] = true
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team
|
||||
func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) error {
|
||||
endpoints, err := service.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for policyTeamID := range endpoint.TeamAccessPolicies {
|
||||
if policyTeamID == teamID {
|
||||
delete(endpoint.TeamAccessPolicies, policyTeamID)
|
||||
|
||||
err := service.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpointGroups, err := service.endpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for policyTeamID := range endpointGroup.TeamAccessPolicies {
|
||||
if policyTeamID == teamID {
|
||||
delete(endpointGroup.TeamAccessPolicies, policyTeamID)
|
||||
|
||||
err := service.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registries, err := service.registryService.Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
for policyTeamID := range registry.TeamAccessPolicies {
|
||||
if policyTeamID == teamID {
|
||||
delete(registry.TeamAccessPolicies, policyTeamID)
|
||||
|
||||
err := service.registryService.UpdateRegistry(registry.ID, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return service.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
|
||||
func (service *AuthorizationService) RemoveUserAccessPolicies(userID UserID) error {
|
||||
endpoints, err := service.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for policyUserID := range endpoint.UserAccessPolicies {
|
||||
if policyUserID == userID {
|
||||
delete(endpoint.UserAccessPolicies, policyUserID)
|
||||
|
||||
err := service.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpointGroups, err := service.endpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for policyUserID := range endpointGroup.UserAccessPolicies {
|
||||
if policyUserID == userID {
|
||||
delete(endpointGroup.UserAccessPolicies, policyUserID)
|
||||
|
||||
err := service.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registries, err := service.registryService.Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
for policyUserID := range registry.UserAccessPolicies {
|
||||
if policyUserID == userID {
|
||||
delete(registry.UserAccessPolicies, policyUserID)
|
||||
|
||||
err := service.registryService.UpdateRegistry(registry.ID, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users.
|
||||
func (service *AuthorizationService) UpdateUsersAuthorizations() error {
|
||||
users, err := service.userService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
err := service.updateUserAuthorizations(user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *AuthorizationService) updateUserAuthorizations(userID UserID) error {
|
||||
user, err := service.userService.User(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointAuthorizations, err := service.getAuthorizations(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.EndpointAuthorizations = endpointAuthorizations
|
||||
|
||||
return service.userService.UpdateUser(userID, user)
|
||||
}
|
||||
|
||||
func (service *AuthorizationService) getAuthorizations(user *User) (EndpointAuthorizations, error) {
|
||||
endpointAuthorizations := EndpointAuthorizations{}
|
||||
if user.Role == AdministratorRole {
|
||||
return endpointAuthorizations, nil
|
||||
}
|
||||
|
||||
userMemberships, err := service.teamMembershipService.TeamMembershipsByUserID(user.ID)
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpoints, err := service.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpointGroups, err := service.endpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
roles, err := service.roleService.Roles()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships)
|
||||
|
||||
return endpointAuthorizations, nil
|
||||
}
|
||||
|
||||
func getUserEndpointAuthorizations(user *User, endpoints []Endpoint, endpointGroups []EndpointGroup, roles []Role, userMemberships []TeamMembership) EndpointAuthorizations {
|
||||
endpointAuthorizations := make(EndpointAuthorizations)
|
||||
|
||||
groupUserAccessPolicies := map[EndpointGroupID]UserAccessPolicies{}
|
||||
groupTeamAccessPolicies := map[EndpointGroupID]TeamAccessPolicies{}
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies
|
||||
groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
continue
|
||||
}
|
||||
|
||||
authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
continue
|
||||
}
|
||||
|
||||
authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
continue
|
||||
}
|
||||
|
||||
authorizations = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
}
|
||||
}
|
||||
|
||||
return endpointAuthorizations
|
||||
}
|
||||
|
||||
func getAuthorizationsFromUserEndpointPolicy(user *User, endpoint *Endpoint, roles []Role) Authorizations {
|
||||
policyRoles := make([]RoleID, 0)
|
||||
|
||||
policy, ok := endpoint.UserAccessPolicies[user.ID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromUserEndpointGroupPolicy(user *User, endpoint *Endpoint, roles []Role, groupAccessPolicies map[EndpointGroupID]UserAccessPolicies) Authorizations {
|
||||
policyRoles := make([]RoleID, 0)
|
||||
|
||||
policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromTeamEndpointPolicies(memberships []TeamMembership, endpoint *Endpoint, roles []Role) Authorizations {
|
||||
policyRoles := make([]RoleID, 0)
|
||||
|
||||
for _, membership := range memberships {
|
||||
policy, ok := endpoint.TeamAccessPolicies[membership.TeamID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []TeamMembership, endpoint *Endpoint, roles []Role, groupAccessPolicies map[EndpointGroupID]TeamAccessPolicies) Authorizations {
|
||||
policyRoles := make([]RoleID, 0)
|
||||
|
||||
for _, membership := range memberships {
|
||||
policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromRoles(roleIdentifiers []RoleID, roles []Role) Authorizations {
|
||||
var associatedRoles []Role
|
||||
|
||||
for _, id := range roleIdentifiers {
|
||||
for _, role := range roles {
|
||||
if role.ID == id {
|
||||
associatedRoles = append(associatedRoles, role)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var authorizations Authorizations
|
||||
highestPriority := 0
|
||||
for _, role := range associatedRoles {
|
||||
if role.Priority > highestPriority {
|
||||
highestPriority = role.Priority
|
||||
authorizations = role.Authorizations
|
||||
}
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
@@ -5,6 +5,11 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/edgegroup"
|
||||
"github.com/portainer/portainer/api/bolt/edgestack"
|
||||
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
||||
"github.com/portainer/portainer/api/bolt/tunnelserver"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/dockerhub"
|
||||
@@ -34,27 +39,31 @@ const (
|
||||
// Store defines the implementation of portainer.DataStore using
|
||||
// BoltDB as the storage system.
|
||||
type Store struct {
|
||||
path string
|
||||
db *bolt.DB
|
||||
checkForDataMigration bool
|
||||
fileService portainer.FileService
|
||||
RoleService *role.Service
|
||||
DockerHubService *dockerhub.Service
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
ExtensionService *extension.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
SettingsService *settings.Service
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TemplateService *template.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
ScheduleService *schedule.Service
|
||||
path string
|
||||
db *bolt.DB
|
||||
checkForDataMigration bool
|
||||
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
|
||||
SettingsService *settings.Service
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TemplateService *template.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
ScheduleService *schedule.Service
|
||||
}
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
@@ -114,19 +123,24 @@ func (store *Store) MigrateData() error {
|
||||
|
||||
if version < portainer.DBVersion {
|
||||
migratorParams := &migrator.Parameters{
|
||||
DB: store.db,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TemplateService: store.TemplateService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
DB: store.db,
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TagService: store.TagService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
TemplateService: store.TemplateService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
}
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
@@ -154,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
|
||||
@@ -166,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
|
||||
@@ -220,6 +252,12 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.TemplateService = templateService
|
||||
|
||||
tunnelServerService, err := tunnelserver.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.TunnelServerService = tunnelServerService
|
||||
|
||||
userService, err := user.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
94
api/bolt/edgegroup/edgegroup.go
Normal file
94
api/bolt/edgegroup/edgegroup.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package edgegroup
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edgegroups"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Edge group data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EdgeGroups return an array containing all the Edge groups.
|
||||
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
|
||||
var groups = make([]portainer.EdgeGroup, 0)
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var group portainer.EdgeGroup
|
||||
err := internal.UnmarshalObjectWithJsoniter(v, &group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return groups, err
|
||||
}
|
||||
|
||||
// EdgeGroup returns an Edge group by ID.
|
||||
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
|
||||
var group portainer.EdgeGroup
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// UpdateEdgeGroup updates an Edge group.
|
||||
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, group)
|
||||
}
|
||||
|
||||
// DeleteEdgeGroup deletes an Edge group.
|
||||
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
|
||||
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
|
||||
func (service *Service) CreateEdgeGroup(group *portainer.EdgeGroup) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
group.ID = portainer.EdgeGroupID(id)
|
||||
|
||||
data, err := internal.MarshalObject(group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(group.ID)), data)
|
||||
})
|
||||
}
|
||||
101
api/bolt/edgestack/edgestack.go
Normal file
101
api/bolt/edgestack/edgestack.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package edgestack
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edge_stack"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Edge stack data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EdgeStacks returns an array containing all edge stacks
|
||||
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
|
||||
var stacks = make([]portainer.EdgeStack, 0)
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var stack portainer.EdgeStack
|
||||
err := internal.UnmarshalObject(v, &stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
// EdgeStack returns an Edge stack by ID.
|
||||
func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
|
||||
var stack portainer.EdgeStack
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stack, nil
|
||||
}
|
||||
|
||||
// CreateEdgeStack assign an ID to a new Edge stack and saves it.
|
||||
func (service *Service) CreateEdgeStack(edgeStack *portainer.EdgeStack) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
if edgeStack.ID == 0 {
|
||||
id, _ := bucket.NextSequence()
|
||||
edgeStack.ID = portainer.EdgeStackID(id)
|
||||
}
|
||||
|
||||
data, err := internal.MarshalObject(edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(edgeStack.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateEdgeStack updates an Edge stack.
|
||||
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, edgeStack)
|
||||
}
|
||||
|
||||
// DeleteEdgeStack deletes an Edge stack.
|
||||
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an endpoint.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.db, BucketName)
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -64,7 +63,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var endpoint portainer.Endpoint
|
||||
err := internal.UnmarshalObject(v, &endpoint)
|
||||
err := internal.UnmarshalObjectWithJsoniter(v, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
68
api/bolt/endpointrelation/endpointrelation.go
Normal file
68
api/bolt/endpointrelation/endpointrelation.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package endpointrelation
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "endpoint_relations"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint relation data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EndpointRelation returns a Endpoint relation object by EndpointID
|
||||
func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
|
||||
var endpointRelation portainer.EndpointRelation
|
||||
identifier := internal.Itob(int(endpointID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &endpointRelation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &endpointRelation, nil
|
||||
}
|
||||
|
||||
// CreateEndpointRelation saves endpointRelation
|
||||
func (service *Service) CreateEndpointRelation(endpointRelation *portainer.EndpointRelation) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
data, err := internal.MarshalObject(endpointRelation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(endpointRelation.EndpointID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateEndpointRelation updates an Endpoint relation object
|
||||
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
identifier := internal.Itob(int(EndpointID))
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, endpointRelation)
|
||||
}
|
||||
|
||||
// DeleteEndpointRelation deletes an Endpoint relation object
|
||||
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
|
||||
identifier := internal.Itob(int(EndpointID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
382
api/bolt/init.go
382
api/bolt/init.go
@@ -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)
|
||||
@@ -32,141 +32,10 @@ func (store *Store) Init() error {
|
||||
|
||||
if len(roles) == 0 {
|
||||
environmentAdministratorRole := &portainer.Role{
|
||||
Name: "Endpoint administrator",
|
||||
Description: "Full control of all resources in an endpoint",
|
||||
Authorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationDockerContainerArchiveInfo: true,
|
||||
portainer.OperationDockerContainerList: true,
|
||||
portainer.OperationDockerContainerExport: true,
|
||||
portainer.OperationDockerContainerChanges: true,
|
||||
portainer.OperationDockerContainerInspect: true,
|
||||
portainer.OperationDockerContainerTop: true,
|
||||
portainer.OperationDockerContainerLogs: true,
|
||||
portainer.OperationDockerContainerStats: true,
|
||||
portainer.OperationDockerContainerAttachWebsocket: true,
|
||||
portainer.OperationDockerContainerArchive: true,
|
||||
portainer.OperationDockerContainerCreate: true,
|
||||
portainer.OperationDockerContainerPrune: true,
|
||||
portainer.OperationDockerContainerKill: true,
|
||||
portainer.OperationDockerContainerPause: true,
|
||||
portainer.OperationDockerContainerUnpause: true,
|
||||
portainer.OperationDockerContainerRestart: true,
|
||||
portainer.OperationDockerContainerStart: true,
|
||||
portainer.OperationDockerContainerStop: true,
|
||||
portainer.OperationDockerContainerWait: true,
|
||||
portainer.OperationDockerContainerResize: true,
|
||||
portainer.OperationDockerContainerAttach: true,
|
||||
portainer.OperationDockerContainerExec: true,
|
||||
portainer.OperationDockerContainerRename: true,
|
||||
portainer.OperationDockerContainerUpdate: true,
|
||||
portainer.OperationDockerContainerPutContainerArchive: true,
|
||||
portainer.OperationDockerContainerDelete: true,
|
||||
portainer.OperationDockerImageList: true,
|
||||
portainer.OperationDockerImageSearch: true,
|
||||
portainer.OperationDockerImageGetAll: true,
|
||||
portainer.OperationDockerImageGet: true,
|
||||
portainer.OperationDockerImageHistory: true,
|
||||
portainer.OperationDockerImageInspect: true,
|
||||
portainer.OperationDockerImageLoad: true,
|
||||
portainer.OperationDockerImageCreate: true,
|
||||
portainer.OperationDockerImagePrune: true,
|
||||
portainer.OperationDockerImagePush: true,
|
||||
portainer.OperationDockerImageTag: true,
|
||||
portainer.OperationDockerImageDelete: true,
|
||||
portainer.OperationDockerImageCommit: true,
|
||||
portainer.OperationDockerImageBuild: true,
|
||||
portainer.OperationDockerNetworkList: true,
|
||||
portainer.OperationDockerNetworkInspect: true,
|
||||
portainer.OperationDockerNetworkCreate: true,
|
||||
portainer.OperationDockerNetworkConnect: true,
|
||||
portainer.OperationDockerNetworkDisconnect: true,
|
||||
portainer.OperationDockerNetworkPrune: true,
|
||||
portainer.OperationDockerNetworkDelete: true,
|
||||
portainer.OperationDockerVolumeList: true,
|
||||
portainer.OperationDockerVolumeInspect: true,
|
||||
portainer.OperationDockerVolumeCreate: true,
|
||||
portainer.OperationDockerVolumePrune: true,
|
||||
portainer.OperationDockerVolumeDelete: true,
|
||||
portainer.OperationDockerExecInspect: true,
|
||||
portainer.OperationDockerExecStart: true,
|
||||
portainer.OperationDockerExecResize: true,
|
||||
portainer.OperationDockerSwarmInspect: true,
|
||||
portainer.OperationDockerSwarmUnlockKey: true,
|
||||
portainer.OperationDockerSwarmInit: true,
|
||||
portainer.OperationDockerSwarmJoin: true,
|
||||
portainer.OperationDockerSwarmLeave: true,
|
||||
portainer.OperationDockerSwarmUpdate: true,
|
||||
portainer.OperationDockerSwarmUnlock: true,
|
||||
portainer.OperationDockerNodeList: true,
|
||||
portainer.OperationDockerNodeInspect: true,
|
||||
portainer.OperationDockerNodeUpdate: true,
|
||||
portainer.OperationDockerNodeDelete: true,
|
||||
portainer.OperationDockerServiceList: true,
|
||||
portainer.OperationDockerServiceInspect: true,
|
||||
portainer.OperationDockerServiceLogs: true,
|
||||
portainer.OperationDockerServiceCreate: true,
|
||||
portainer.OperationDockerServiceUpdate: true,
|
||||
portainer.OperationDockerServiceDelete: true,
|
||||
portainer.OperationDockerSecretList: true,
|
||||
portainer.OperationDockerSecretInspect: true,
|
||||
portainer.OperationDockerSecretCreate: true,
|
||||
portainer.OperationDockerSecretUpdate: true,
|
||||
portainer.OperationDockerSecretDelete: true,
|
||||
portainer.OperationDockerConfigList: true,
|
||||
portainer.OperationDockerConfigInspect: true,
|
||||
portainer.OperationDockerConfigCreate: true,
|
||||
portainer.OperationDockerConfigUpdate: true,
|
||||
portainer.OperationDockerConfigDelete: true,
|
||||
portainer.OperationDockerTaskList: true,
|
||||
portainer.OperationDockerTaskInspect: true,
|
||||
portainer.OperationDockerTaskLogs: true,
|
||||
portainer.OperationDockerPluginList: true,
|
||||
portainer.OperationDockerPluginPrivileges: true,
|
||||
portainer.OperationDockerPluginInspect: true,
|
||||
portainer.OperationDockerPluginPull: true,
|
||||
portainer.OperationDockerPluginCreate: true,
|
||||
portainer.OperationDockerPluginEnable: true,
|
||||
portainer.OperationDockerPluginDisable: true,
|
||||
portainer.OperationDockerPluginPush: true,
|
||||
portainer.OperationDockerPluginUpgrade: true,
|
||||
portainer.OperationDockerPluginSet: true,
|
||||
portainer.OperationDockerPluginDelete: true,
|
||||
portainer.OperationDockerSessionStart: true,
|
||||
portainer.OperationDockerDistributionInspect: true,
|
||||
portainer.OperationDockerBuildPrune: true,
|
||||
portainer.OperationDockerBuildCancel: true,
|
||||
portainer.OperationDockerPing: true,
|
||||
portainer.OperationDockerInfo: true,
|
||||
portainer.OperationDockerVersion: true,
|
||||
portainer.OperationDockerEvents: true,
|
||||
portainer.OperationDockerSystem: true,
|
||||
portainer.OperationDockerUndefined: true,
|
||||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentBrowseDelete: true,
|
||||
portainer.OperationDockerAgentBrowseGet: true,
|
||||
portainer.OperationDockerAgentBrowseList: true,
|
||||
portainer.OperationDockerAgentBrowsePut: true,
|
||||
portainer.OperationDockerAgentBrowseRename: true,
|
||||
portainer.OperationDockerAgentUndefined: true,
|
||||
portainer.OperationPortainerResourceControlCreate: true,
|
||||
portainer.OperationPortainerResourceControlUpdate: true,
|
||||
portainer.OperationPortainerResourceControlDelete: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
portainer.OperationPortainerStackCreate: true,
|
||||
portainer.OperationPortainerStackMigrate: true,
|
||||
portainer.OperationPortainerStackUpdate: true,
|
||||
portainer.OperationPortainerStackDelete: true,
|
||||
portainer.OperationPortainerWebsocketExec: true,
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
portainer.OperationPortainerWebhookCreate: true,
|
||||
portainer.OperationPortainerWebhookDelete: true,
|
||||
portainer.OperationIntegrationStoridgeAdmin: true,
|
||||
portainer.EndpointResourcesAccess: true,
|
||||
},
|
||||
Name: "Endpoint administrator",
|
||||
Description: "Full control of all resources in an endpoint",
|
||||
Priority: 1,
|
||||
Authorizations: portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
||||
@@ -175,57 +44,10 @@ func (store *Store) Init() error {
|
||||
}
|
||||
|
||||
environmentReadOnlyUserRole := &portainer.Role{
|
||||
Name: "Helpdesk",
|
||||
Description: "Read-only access of all resources in an endpoint",
|
||||
Authorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationDockerContainerArchiveInfo: true,
|
||||
portainer.OperationDockerContainerList: true,
|
||||
portainer.OperationDockerContainerChanges: true,
|
||||
portainer.OperationDockerContainerInspect: true,
|
||||
portainer.OperationDockerContainerTop: true,
|
||||
portainer.OperationDockerContainerLogs: true,
|
||||
portainer.OperationDockerContainerStats: true,
|
||||
portainer.OperationDockerImageList: true,
|
||||
portainer.OperationDockerImageSearch: true,
|
||||
portainer.OperationDockerImageGetAll: true,
|
||||
portainer.OperationDockerImageGet: true,
|
||||
portainer.OperationDockerImageHistory: true,
|
||||
portainer.OperationDockerImageInspect: true,
|
||||
portainer.OperationDockerNetworkList: true,
|
||||
portainer.OperationDockerNetworkInspect: true,
|
||||
portainer.OperationDockerVolumeList: true,
|
||||
portainer.OperationDockerVolumeInspect: true,
|
||||
portainer.OperationDockerSwarmInspect: true,
|
||||
portainer.OperationDockerNodeList: true,
|
||||
portainer.OperationDockerNodeInspect: true,
|
||||
portainer.OperationDockerServiceList: true,
|
||||
portainer.OperationDockerServiceInspect: true,
|
||||
portainer.OperationDockerServiceLogs: true,
|
||||
portainer.OperationDockerSecretList: true,
|
||||
portainer.OperationDockerSecretInspect: true,
|
||||
portainer.OperationDockerConfigList: true,
|
||||
portainer.OperationDockerConfigInspect: true,
|
||||
portainer.OperationDockerTaskList: true,
|
||||
portainer.OperationDockerTaskInspect: true,
|
||||
portainer.OperationDockerTaskLogs: true,
|
||||
portainer.OperationDockerPluginList: true,
|
||||
portainer.OperationDockerDistributionInspect: true,
|
||||
portainer.OperationDockerPing: true,
|
||||
portainer.OperationDockerInfo: true,
|
||||
portainer.OperationDockerVersion: true,
|
||||
portainer.OperationDockerEvents: true,
|
||||
portainer.OperationDockerSystem: true,
|
||||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentBrowseGet: true,
|
||||
portainer.OperationDockerAgentBrowseList: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
portainer.EndpointResourcesAccess: true,
|
||||
},
|
||||
Name: "Helpdesk",
|
||||
Description: "Read-only access of all resources in an endpoint",
|
||||
Priority: 2,
|
||||
Authorizations: portainer.DefaultEndpointAuthorizationsForHelpDeskRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
||||
@@ -234,134 +56,10 @@ func (store *Store) Init() error {
|
||||
}
|
||||
|
||||
standardUserRole := &portainer.Role{
|
||||
Name: "Standard user",
|
||||
Description: "Full control of assigned resources in an endpoint",
|
||||
Authorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationDockerContainerArchiveInfo: true,
|
||||
portainer.OperationDockerContainerList: true,
|
||||
portainer.OperationDockerContainerExport: true,
|
||||
portainer.OperationDockerContainerChanges: true,
|
||||
portainer.OperationDockerContainerInspect: true,
|
||||
portainer.OperationDockerContainerTop: true,
|
||||
portainer.OperationDockerContainerLogs: true,
|
||||
portainer.OperationDockerContainerStats: true,
|
||||
portainer.OperationDockerContainerAttachWebsocket: true,
|
||||
portainer.OperationDockerContainerArchive: true,
|
||||
portainer.OperationDockerContainerCreate: true,
|
||||
portainer.OperationDockerContainerKill: true,
|
||||
portainer.OperationDockerContainerPause: true,
|
||||
portainer.OperationDockerContainerUnpause: true,
|
||||
portainer.OperationDockerContainerRestart: true,
|
||||
portainer.OperationDockerContainerStart: true,
|
||||
portainer.OperationDockerContainerStop: true,
|
||||
portainer.OperationDockerContainerWait: true,
|
||||
portainer.OperationDockerContainerResize: true,
|
||||
portainer.OperationDockerContainerAttach: true,
|
||||
portainer.OperationDockerContainerExec: true,
|
||||
portainer.OperationDockerContainerRename: true,
|
||||
portainer.OperationDockerContainerUpdate: true,
|
||||
portainer.OperationDockerContainerPutContainerArchive: true,
|
||||
portainer.OperationDockerContainerDelete: true,
|
||||
portainer.OperationDockerImageList: true,
|
||||
portainer.OperationDockerImageSearch: true,
|
||||
portainer.OperationDockerImageGetAll: true,
|
||||
portainer.OperationDockerImageGet: true,
|
||||
portainer.OperationDockerImageHistory: true,
|
||||
portainer.OperationDockerImageInspect: true,
|
||||
portainer.OperationDockerImageLoad: true,
|
||||
portainer.OperationDockerImageCreate: true,
|
||||
portainer.OperationDockerImagePush: true,
|
||||
portainer.OperationDockerImageTag: true,
|
||||
portainer.OperationDockerImageDelete: true,
|
||||
portainer.OperationDockerImageCommit: true,
|
||||
portainer.OperationDockerImageBuild: true,
|
||||
portainer.OperationDockerNetworkList: true,
|
||||
portainer.OperationDockerNetworkInspect: true,
|
||||
portainer.OperationDockerNetworkCreate: true,
|
||||
portainer.OperationDockerNetworkConnect: true,
|
||||
portainer.OperationDockerNetworkDisconnect: true,
|
||||
portainer.OperationDockerNetworkDelete: true,
|
||||
portainer.OperationDockerVolumeList: true,
|
||||
portainer.OperationDockerVolumeInspect: true,
|
||||
portainer.OperationDockerVolumeCreate: true,
|
||||
portainer.OperationDockerVolumeDelete: true,
|
||||
portainer.OperationDockerExecInspect: true,
|
||||
portainer.OperationDockerExecStart: true,
|
||||
portainer.OperationDockerExecResize: true,
|
||||
portainer.OperationDockerSwarmInspect: true,
|
||||
portainer.OperationDockerSwarmUnlockKey: true,
|
||||
portainer.OperationDockerSwarmInit: true,
|
||||
portainer.OperationDockerSwarmJoin: true,
|
||||
portainer.OperationDockerSwarmLeave: true,
|
||||
portainer.OperationDockerSwarmUpdate: true,
|
||||
portainer.OperationDockerSwarmUnlock: true,
|
||||
portainer.OperationDockerNodeList: true,
|
||||
portainer.OperationDockerNodeInspect: true,
|
||||
portainer.OperationDockerNodeUpdate: true,
|
||||
portainer.OperationDockerNodeDelete: true,
|
||||
portainer.OperationDockerServiceList: true,
|
||||
portainer.OperationDockerServiceInspect: true,
|
||||
portainer.OperationDockerServiceLogs: true,
|
||||
portainer.OperationDockerServiceCreate: true,
|
||||
portainer.OperationDockerServiceUpdate: true,
|
||||
portainer.OperationDockerServiceDelete: true,
|
||||
portainer.OperationDockerSecretList: true,
|
||||
portainer.OperationDockerSecretInspect: true,
|
||||
portainer.OperationDockerSecretCreate: true,
|
||||
portainer.OperationDockerSecretUpdate: true,
|
||||
portainer.OperationDockerSecretDelete: true,
|
||||
portainer.OperationDockerConfigList: true,
|
||||
portainer.OperationDockerConfigInspect: true,
|
||||
portainer.OperationDockerConfigCreate: true,
|
||||
portainer.OperationDockerConfigUpdate: true,
|
||||
portainer.OperationDockerConfigDelete: true,
|
||||
portainer.OperationDockerTaskList: true,
|
||||
portainer.OperationDockerTaskInspect: true,
|
||||
portainer.OperationDockerTaskLogs: true,
|
||||
portainer.OperationDockerPluginList: true,
|
||||
portainer.OperationDockerPluginPrivileges: true,
|
||||
portainer.OperationDockerPluginInspect: true,
|
||||
portainer.OperationDockerPluginPull: true,
|
||||
portainer.OperationDockerPluginCreate: true,
|
||||
portainer.OperationDockerPluginEnable: true,
|
||||
portainer.OperationDockerPluginDisable: true,
|
||||
portainer.OperationDockerPluginPush: true,
|
||||
portainer.OperationDockerPluginUpgrade: true,
|
||||
portainer.OperationDockerPluginSet: true,
|
||||
portainer.OperationDockerPluginDelete: true,
|
||||
portainer.OperationDockerSessionStart: true,
|
||||
portainer.OperationDockerDistributionInspect: true,
|
||||
portainer.OperationDockerBuildPrune: true,
|
||||
portainer.OperationDockerBuildCancel: true,
|
||||
portainer.OperationDockerPing: true,
|
||||
portainer.OperationDockerInfo: true,
|
||||
portainer.OperationDockerVersion: true,
|
||||
portainer.OperationDockerEvents: true,
|
||||
portainer.OperationDockerSystem: true,
|
||||
portainer.OperationDockerUndefined: true,
|
||||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentBrowseDelete: true,
|
||||
portainer.OperationDockerAgentBrowseGet: true,
|
||||
portainer.OperationDockerAgentBrowseList: true,
|
||||
portainer.OperationDockerAgentBrowsePut: true,
|
||||
portainer.OperationDockerAgentBrowseRename: true,
|
||||
portainer.OperationDockerAgentUndefined: true,
|
||||
portainer.OperationPortainerResourceControlCreate: true,
|
||||
portainer.OperationPortainerResourceControlUpdate: true,
|
||||
portainer.OperationPortainerResourceControlDelete: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
portainer.OperationPortainerStackCreate: true,
|
||||
portainer.OperationPortainerStackMigrate: true,
|
||||
portainer.OperationPortainerStackUpdate: true,
|
||||
portainer.OperationPortainerStackDelete: true,
|
||||
portainer.OperationPortainerWebsocketExec: true,
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
portainer.OperationPortainerWebhookCreate: true,
|
||||
},
|
||||
Name: "Standard user",
|
||||
Description: "Full control of assigned resources in an endpoint",
|
||||
Priority: 3,
|
||||
Authorizations: portainer.DefaultEndpointAuthorizationsForStandardUserRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(standardUserRole)
|
||||
@@ -370,56 +68,10 @@ func (store *Store) Init() error {
|
||||
}
|
||||
|
||||
readOnlyUserRole := &portainer.Role{
|
||||
Name: "Read-only user",
|
||||
Description: "Read-only access of assigned resources in an endpoint",
|
||||
Authorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationDockerContainerArchiveInfo: true,
|
||||
portainer.OperationDockerContainerList: true,
|
||||
portainer.OperationDockerContainerChanges: true,
|
||||
portainer.OperationDockerContainerInspect: true,
|
||||
portainer.OperationDockerContainerTop: true,
|
||||
portainer.OperationDockerContainerLogs: true,
|
||||
portainer.OperationDockerContainerStats: true,
|
||||
portainer.OperationDockerImageList: true,
|
||||
portainer.OperationDockerImageSearch: true,
|
||||
portainer.OperationDockerImageGetAll: true,
|
||||
portainer.OperationDockerImageGet: true,
|
||||
portainer.OperationDockerImageHistory: true,
|
||||
portainer.OperationDockerImageInspect: true,
|
||||
portainer.OperationDockerNetworkList: true,
|
||||
portainer.OperationDockerNetworkInspect: true,
|
||||
portainer.OperationDockerVolumeList: true,
|
||||
portainer.OperationDockerVolumeInspect: true,
|
||||
portainer.OperationDockerSwarmInspect: true,
|
||||
portainer.OperationDockerNodeList: true,
|
||||
portainer.OperationDockerNodeInspect: true,
|
||||
portainer.OperationDockerServiceList: true,
|
||||
portainer.OperationDockerServiceInspect: true,
|
||||
portainer.OperationDockerServiceLogs: true,
|
||||
portainer.OperationDockerSecretList: true,
|
||||
portainer.OperationDockerSecretInspect: true,
|
||||
portainer.OperationDockerConfigList: true,
|
||||
portainer.OperationDockerConfigInspect: true,
|
||||
portainer.OperationDockerTaskList: true,
|
||||
portainer.OperationDockerTaskInspect: true,
|
||||
portainer.OperationDockerTaskLogs: true,
|
||||
portainer.OperationDockerPluginList: true,
|
||||
portainer.OperationDockerDistributionInspect: true,
|
||||
portainer.OperationDockerPing: true,
|
||||
portainer.OperationDockerInfo: true,
|
||||
portainer.OperationDockerVersion: true,
|
||||
portainer.OperationDockerEvents: true,
|
||||
portainer.OperationDockerSystem: true,
|
||||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentBrowseGet: true,
|
||||
portainer.OperationDockerAgentBrowseList: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
},
|
||||
Name: "Read-only user",
|
||||
Description: "Read-only access of assigned resources in an endpoint",
|
||||
Priority: 4,
|
||||
Authorizations: portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(readOnlyUserRole)
|
||||
|
||||
@@ -82,13 +82,15 @@ func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
|
||||
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
|
||||
var identifier int
|
||||
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
id := bucket.Sequence()
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
identifier = int(id)
|
||||
return nil
|
||||
})
|
||||
|
||||
identifier++
|
||||
return identifier
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// MarshalObject encodes an object to binary format
|
||||
@@ -13,3 +15,11 @@ func MarshalObject(object interface{}) ([]byte, error) {
|
||||
func UnmarshalObject(data []byte, object interface{}) error {
|
||||
return json.Unmarshal(data, object)
|
||||
}
|
||||
|
||||
// UnmarshalObjectWithJsoniter decodes an object from binary data
|
||||
// using the jsoniter library. It is mainly used to accelerate endpoint
|
||||
// decoding at the moment.
|
||||
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
|
||||
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
return jsoni.Unmarshal(data, &object)
|
||||
}
|
||||
|
||||
16
api/bolt/migrator/migrate_dbversion18.go
Normal file
16
api/bolt/migrator/migrate_dbversion18.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion19() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if legacySettings.EdgeAgentCheckinInterval == 0 {
|
||||
legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds
|
||||
}
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
67
api/bolt/migrator/migrate_dbversion19.go
Normal file
67
api/bolt/migrator/migrate_dbversion19.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateUsersToDBVersion20() error {
|
||||
authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
|
||||
EndpointService: m.endpointService,
|
||||
EndpointGroupService: m.endpointGroupService,
|
||||
RegistryService: m.registryService,
|
||||
RoleService: m.roleService,
|
||||
TeamMembershipService: m.teamMembershipService,
|
||||
UserService: m.userService,
|
||||
}
|
||||
|
||||
authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)
|
||||
return authorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion20() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.AllowVolumeBrowserForRegularUsers = false
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func (m *Migrator) updateSchedulesToDBVersion20() error {
|
||||
legacySchedules, err := m.scheduleService.Schedules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, schedule := range legacySchedules {
|
||||
if schedule.JobType == portainer.ScriptExecutionJobType {
|
||||
if schedule.CronExpression == "0 0 * * *" {
|
||||
schedule.CronExpression = "0 * * * *"
|
||||
} else if schedule.CronExpression == "0 0 0/2 * *" {
|
||||
schedule.CronExpression = "0 */2 * * *"
|
||||
} else if schedule.CronExpression == "0 0 0 * *" {
|
||||
schedule.CronExpression = "0 0 * * *"
|
||||
} else {
|
||||
revisedCronExpression := strings.Split(schedule.CronExpression, " ")
|
||||
if len(revisedCronExpression) == 5 {
|
||||
continue
|
||||
}
|
||||
|
||||
revisedCronExpression = revisedCronExpression[1:]
|
||||
schedule.CronExpression = strings.Join(revisedCronExpression, " ")
|
||||
}
|
||||
|
||||
err := m.scheduleService.UpdateSchedule(schedule.ID, &schedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
89
api/bolt/migrator/migrate_dbversion20.go
Normal file
89
api/bolt/migrator/migrate_dbversion20.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateResourceControlsToDBVersion22() error {
|
||||
legacyResourceControls, err := m.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resourceControl := range legacyResourceControls {
|
||||
resourceControl.AdministratorsOnly = false
|
||||
|
||||
err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, &resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
legacyUsers, err := m.userService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range legacyUsers {
|
||||
user.PortainerAuthorizations = portainer.DefaultPortainerAuthorizations()
|
||||
err = m.userService.UpdateUser(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpointAdministratorRole, err := m.roleService.Role(portainer.RoleID(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointAdministratorRole.Priority = 1
|
||||
endpointAdministratorRole.Authorizations = portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
|
||||
|
||||
err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)
|
||||
|
||||
helpDeskRole, err := m.roleService.Role(portainer.RoleID(2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
helpDeskRole.Priority = 2
|
||||
helpDeskRole.Authorizations = portainer.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
|
||||
|
||||
standardUserRole, err := m.roleService.Role(portainer.RoleID(3))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
standardUserRole.Priority = 3
|
||||
standardUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
|
||||
|
||||
readOnlyUserRole, err := m.roleService.Role(portainer.RoleID(4))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
readOnlyUserRole.Priority = 4
|
||||
readOnlyUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||
|
||||
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
|
||||
|
||||
authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
|
||||
EndpointService: m.endpointService,
|
||||
EndpointGroupService: m.endpointGroupService,
|
||||
RegistryService: m.registryService,
|
||||
RoleService: m.roleService,
|
||||
TeamMembershipService: m.teamMembershipService,
|
||||
UserService: m.userService,
|
||||
}
|
||||
|
||||
authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)
|
||||
return authorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
94
api/bolt/migrator/migrate_dbversion22.go
Normal file
94
api/bolt/migrator/migrate_dbversion22.go
Normal file
@@ -0,0 +1,94 @@
|
||||
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
|
||||
}
|
||||
14
api/bolt/migrator/migrate_dbversion23.go
Normal file
14
api/bolt/migrator/migrate_dbversion23.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion24() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.AllowDeviceMappingForRegularUsers = true
|
||||
legacySettings.AllowStackManagementForRegularUsers = true
|
||||
legacySettings.AllowHostNamespaceForRegularUsers = true
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
@@ -5,11 +5,16 @@ 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"
|
||||
"github.com/portainer/portainer/api/bolt/role"
|
||||
"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"
|
||||
"github.com/portainer/portainer/api/bolt/version"
|
||||
@@ -18,55 +23,70 @@ import (
|
||||
type (
|
||||
// Migrator defines a service to migrate data after a Portainer version update.
|
||||
Migrator struct {
|
||||
currentDBVersion int
|
||||
db *bolt.DB
|
||||
endpointGroupService *endpointgroup.Service
|
||||
endpointService *endpoint.Service
|
||||
extensionService *extension.Service
|
||||
registryService *registry.Service
|
||||
resourceControlService *resourcecontrol.Service
|
||||
settingsService *settings.Service
|
||||
stackService *stack.Service
|
||||
templateService *template.Service
|
||||
userService *user.Service
|
||||
versionService *version.Service
|
||||
fileService portainer.FileService
|
||||
currentDBVersion int
|
||||
db *bolt.DB
|
||||
endpointGroupService *endpointgroup.Service
|
||||
endpointService *endpoint.Service
|
||||
endpointRelationService *endpointrelation.Service
|
||||
extensionService *extension.Service
|
||||
registryService *registry.Service
|
||||
resourceControlService *resourcecontrol.Service
|
||||
roleService *role.Service
|
||||
scheduleService *schedule.Service
|
||||
settingsService *settings.Service
|
||||
stackService *stack.Service
|
||||
tagService *tag.Service
|
||||
teamMembershipService *teammembership.Service
|
||||
templateService *template.Service
|
||||
userService *user.Service
|
||||
versionService *version.Service
|
||||
fileService portainer.FileService
|
||||
}
|
||||
|
||||
// Parameters represents the required parameters to create a new Migrator instance.
|
||||
Parameters struct {
|
||||
DB *bolt.DB
|
||||
DatabaseVersion int
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
ExtensionService *extension.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
SettingsService *settings.Service
|
||||
StackService *stack.Service
|
||||
TemplateService *template.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
FileService portainer.FileService
|
||||
DB *bolt.DB
|
||||
DatabaseVersion int
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
ExtensionService *extension.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
ScheduleService *schedule.Service
|
||||
SettingsService *settings.Service
|
||||
StackService *stack.Service
|
||||
TagService *tag.Service
|
||||
TeamMembershipService *teammembership.Service
|
||||
TemplateService *template.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
FileService portainer.FileService
|
||||
}
|
||||
)
|
||||
|
||||
// NewMigrator creates a new Migrator.
|
||||
func NewMigrator(parameters *Parameters) *Migrator {
|
||||
return &Migrator{
|
||||
db: parameters.DB,
|
||||
currentDBVersion: parameters.DatabaseVersion,
|
||||
endpointGroupService: parameters.EndpointGroupService,
|
||||
endpointService: parameters.EndpointService,
|
||||
extensionService: parameters.ExtensionService,
|
||||
registryService: parameters.RegistryService,
|
||||
resourceControlService: parameters.ResourceControlService,
|
||||
settingsService: parameters.SettingsService,
|
||||
templateService: parameters.TemplateService,
|
||||
stackService: parameters.StackService,
|
||||
userService: parameters.UserService,
|
||||
versionService: parameters.VersionService,
|
||||
fileService: parameters.FileService,
|
||||
db: parameters.DB,
|
||||
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,
|
||||
userService: parameters.UserService,
|
||||
versionService: parameters.VersionService,
|
||||
fileService: parameters.FileService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,5 +269,66 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.0
|
||||
if m.currentDBVersion < 19 {
|
||||
err := m.updateSettingsToDBVersion19()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.22.1
|
||||
if m.currentDBVersion < 20 {
|
||||
err := m.updateUsersToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSchedulesToDBVersion20()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.0
|
||||
if m.currentDBVersion < 23 {
|
||||
err := m.updateTagsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDBVersion24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -42,9 +42,10 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
|
||||
return &resourceControl, nil
|
||||
}
|
||||
|
||||
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
|
||||
// to the main ResourceID or in SubResourceIDs
|
||||
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
|
||||
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
|
||||
// if no ResourceControl was found.
|
||||
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||
var resourceControl *portainer.ResourceControl
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
@@ -58,7 +59,7 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
|
||||
return err
|
||||
}
|
||||
|
||||
if rc.ResourceID == resourceID {
|
||||
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||
resourceControl = &rc
|
||||
break
|
||||
}
|
||||
@@ -71,10 +72,6 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
|
||||
}
|
||||
}
|
||||
|
||||
if resourceControl == nil {
|
||||
return portainer.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -66,18 +66,24 @@ func (service *Service) Roles() ([]portainer.Role, error) {
|
||||
}
|
||||
|
||||
// CreateRole creates a new Role.
|
||||
func (service *Service) CreateRole(set *portainer.Role) error {
|
||||
func (service *Service) CreateRole(role *portainer.Role) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
set.ID = portainer.RoleID(id)
|
||||
role.ID = portainer.RoleID(id)
|
||||
|
||||
data, err := internal.MarshalObject(set)
|
||||
data, err := internal.MarshalObject(role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(set.ID)), data)
|
||||
return bucket.Put(internal.Itob(int(role.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRole updates a role.
|
||||
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, role)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
48
api/bolt/tunnelserver/tunnelserver.go
Normal file
48
api/bolt/tunnelserver/tunnelserver.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package tunnelserver
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "tunnel_server"
|
||||
infoKey = "INFO"
|
||||
)
|
||||
|
||||
// Service represents a service for managing endpoint data.
|
||||
type Service struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(db *bolt.DB) (*Service, error) {
|
||||
err := internal.CreateBucket(db, BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Info retrieve the TunnelServerInfo object.
|
||||
func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
|
||||
var info portainer.TunnelServerInfo
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, []byte(infoKey), &info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// UpdateInfo persists a TunnelServerInfo object.
|
||||
func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error {
|
||||
return internal.UpdateObject(service.db, BucketName, []byte(infoKey), settings)
|
||||
}
|
||||
24
api/chisel/key.go
Normal file
24
api/chisel/key.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package chisel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateEdgeKey will generate a key that can be used by an Edge agent to register with a Portainer instance.
|
||||
// The key represents the following data in this particular format:
|
||||
// portainer_instance_url|tunnel_server_addr|tunnel_server_fingerprint|endpoint_ID
|
||||
// The key returned by this function is a base64 encoded version of the data.
|
||||
func (service *Service) GenerateEdgeKey(url, host string, endpointIdentifier int) string {
|
||||
keyInformation := []string{
|
||||
url,
|
||||
fmt.Sprintf("%s:%s", host, service.serverPort),
|
||||
service.serverFingerprint,
|
||||
strconv.Itoa(endpointIdentifier),
|
||||
}
|
||||
|
||||
key := strings.Join(keyInformation, "|")
|
||||
return base64.RawStdEncoding.EncodeToString([]byte(key))
|
||||
}
|
||||
47
api/chisel/schedules.go
Normal file
47
api/chisel/schedules.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package chisel
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// AddSchedule register a schedule inside the tunnel details associated to an endpoint.
|
||||
func (service *Service) AddSchedule(endpointID portainer.EndpointID, schedule *portainer.EdgeSchedule) {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
|
||||
existingScheduleIndex := -1
|
||||
for idx, existingSchedule := range tunnel.Schedules {
|
||||
if existingSchedule.ID == schedule.ID {
|
||||
existingScheduleIndex = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if existingScheduleIndex == -1 {
|
||||
tunnel.Schedules = append(tunnel.Schedules, *schedule)
|
||||
} else {
|
||||
tunnel.Schedules[existingScheduleIndex] = *schedule
|
||||
}
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
}
|
||||
|
||||
// RemoveSchedule will remove the specified schedule from each tunnel it was registered with.
|
||||
func (service *Service) RemoveSchedule(scheduleID portainer.ScheduleID) {
|
||||
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||
tunnelDetails := item.Val.(*portainer.TunnelDetails)
|
||||
|
||||
updatedSchedules := make([]portainer.EdgeSchedule, 0)
|
||||
for _, schedule := range tunnelDetails.Schedules {
|
||||
if schedule.ID == scheduleID {
|
||||
continue
|
||||
}
|
||||
updatedSchedules = append(updatedSchedules, schedule)
|
||||
}
|
||||
|
||||
tunnelDetails.Schedules = updatedSchedules
|
||||
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
|
||||
}
|
||||
}
|
||||
191
api/chisel/service.go
Normal file
191
api/chisel/service.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package chisel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
|
||||
chserver "github.com/jpillora/chisel/server"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
tunnelCleanupInterval = 10 * time.Second
|
||||
requiredTimeout = 15 * time.Second
|
||||
activeTimeout = 4*time.Minute + 30*time.Second
|
||||
)
|
||||
|
||||
// Service represents a service to manage the state of multiple reverse tunnels.
|
||||
// It is used to start a reverse tunnel server and to manage the connection status of each tunnel
|
||||
// connected to the tunnel server.
|
||||
type Service struct {
|
||||
serverFingerprint string
|
||||
serverPort string
|
||||
tunnelDetailsMap cmap.ConcurrentMap
|
||||
endpointService portainer.EndpointService
|
||||
tunnelServerService portainer.TunnelServerService
|
||||
snapshotter portainer.Snapshotter
|
||||
chiselServer *chserver.Server
|
||||
}
|
||||
|
||||
// NewService returns a pointer to a new instance of Service
|
||||
func NewService(endpointService portainer.EndpointService, tunnelServerService portainer.TunnelServerService) *Service {
|
||||
return &Service{
|
||||
tunnelDetailsMap: cmap.New(),
|
||||
endpointService: endpointService,
|
||||
tunnelServerService: tunnelServerService,
|
||||
}
|
||||
}
|
||||
|
||||
// StartTunnelServer starts a tunnel server on the specified addr and port.
|
||||
// It uses a seed to generate a new private/public key pair. If the seed cannot
|
||||
// be found inside the database, it will generate a new one randomly and persist it.
|
||||
// It starts the tunnel status verification process in the background.
|
||||
// The snapshotter is used in the tunnel status verification process.
|
||||
func (service *Service) StartTunnelServer(addr, port string, snapshotter portainer.Snapshotter) error {
|
||||
keySeed, err := service.retrievePrivateKeySeed()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := &chserver.Config{
|
||||
Reverse: true,
|
||||
KeySeed: keySeed,
|
||||
}
|
||||
|
||||
chiselServer, err := chserver.NewServer(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.serverFingerprint = chiselServer.GetFingerprint()
|
||||
service.serverPort = port
|
||||
|
||||
err = chiselServer.Start(addr, port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service.chiselServer = chiselServer
|
||||
|
||||
// TODO: work-around Chisel default behavior.
|
||||
// By default, Chisel will allow anyone to connect if no user exists.
|
||||
username, password := generateRandomCredentials()
|
||||
err = service.chiselServer.AddUser(username, password, "127.0.0.1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.snapshotter = snapshotter
|
||||
go service.startTunnelVerificationLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) retrievePrivateKeySeed() (string, error) {
|
||||
var serverInfo *portainer.TunnelServerInfo
|
||||
|
||||
serverInfo, err := service.tunnelServerService.Info()
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
keySeed := uniuri.NewLen(16)
|
||||
|
||||
serverInfo = &portainer.TunnelServerInfo{
|
||||
PrivateKeySeed: keySeed,
|
||||
}
|
||||
|
||||
err := service.tunnelServerService.UpdateInfo(serverInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return serverInfo.PrivateKeySeed, nil
|
||||
}
|
||||
|
||||
func (service *Service) startTunnelVerificationLoop() {
|
||||
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
|
||||
ticker := time.NewTicker(tunnelCleanupInterval)
|
||||
stopSignal := make(chan struct{})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
service.checkTunnels()
|
||||
case <-stopSignal:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) checkTunnels() {
|
||||
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||
tunnel := item.Val.(*portainer.TunnelDetails)
|
||||
|
||||
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
|
||||
continue
|
||||
}
|
||||
|
||||
elapsed := time.Since(tunnel.LastActivity)
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: endpoint tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
|
||||
continue
|
||||
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
|
||||
continue
|
||||
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
|
||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
|
||||
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
|
||||
}
|
||||
|
||||
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge endpoint (id: %s): %s", item.Key, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tunnel.Schedules) > 0 {
|
||||
endpointID, err := strconv.Atoi(item.Key)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [chisel,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||
} else {
|
||||
service.tunnelDetailsMap.Remove(item.Key)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {
|
||||
endpoint, err := service.endpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointURL := endpoint.URL
|
||||
endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
|
||||
snapshot, err := service.snapshotter.CreateSnapshot(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
endpoint.URL = endpointURL
|
||||
return service.endpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
}
|
||||
144
api/chisel/tunnel.go
Normal file
144
api/chisel/tunnel.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package chisel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
minAvailablePort = 49152
|
||||
maxAvailablePort = 65535
|
||||
)
|
||||
|
||||
// getUnusedPort is used to generate an unused random port in the dynamic port range.
|
||||
// Dynamic ports (also called private ports) are 49152 to 65535.
|
||||
func (service *Service) getUnusedPort() int {
|
||||
port := randomInt(minAvailablePort, maxAvailablePort)
|
||||
|
||||
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||
tunnel := item.Val.(*portainer.TunnelDetails)
|
||||
if tunnel.Port == port {
|
||||
return service.getUnusedPort()
|
||||
}
|
||||
}
|
||||
|
||||
return port
|
||||
}
|
||||
|
||||
func randomInt(min, max int) int {
|
||||
return min + rand.Intn(max-min)
|
||||
}
|
||||
|
||||
// GetTunnelDetails returns information about the tunnel associated to an endpoint.
|
||||
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
|
||||
if item, ok := service.tunnelDetailsMap.Get(key); ok {
|
||||
tunnelDetails := item.(*portainer.TunnelDetails)
|
||||
return tunnelDetails
|
||||
}
|
||||
|
||||
schedules := make([]portainer.EdgeSchedule, 0)
|
||||
return &portainer.TunnelDetails{
|
||||
Status: portainer.EdgeAgentIdle,
|
||||
Port: 0,
|
||||
Schedules: schedules,
|
||||
Credentials: "",
|
||||
}
|
||||
}
|
||||
|
||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
||||
// It sets the status to ACTIVE.
|
||||
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
tunnel.Status = portainer.EdgeAgentActive
|
||||
tunnel.Credentials = ""
|
||||
tunnel.LastActivity = time.Now()
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified endpoint.
|
||||
// It sets the status to IDLE.
|
||||
// It removes any existing credentials associated to the tunnel.
|
||||
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
|
||||
tunnel.Status = portainer.EdgeAgentIdle
|
||||
tunnel.Port = 0
|
||||
tunnel.LastActivity = time.Now()
|
||||
|
||||
credentials := tunnel.Credentials
|
||||
if credentials != "" {
|
||||
tunnel.Credentials = ""
|
||||
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
||||
}
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified endpoint.
|
||||
// It sets the status to REQUIRED.
|
||||
// If no port is currently associated to the tunnel, it will associate a random unused port to the tunnel
|
||||
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
|
||||
// Credentials are encrypted using the Edge ID associated to the endpoint.
|
||||
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
||||
tunnel := service.GetTunnelDetails(endpointID)
|
||||
|
||||
if tunnel.Port == 0 {
|
||||
endpoint, err := service.endpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunnel.Status = portainer.EdgeAgentManagementRequired
|
||||
tunnel.Port = service.getUnusedPort()
|
||||
tunnel.LastActivity = time.Now()
|
||||
|
||||
username, password := generateRandomCredentials()
|
||||
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
|
||||
err = service.chiselServer.AddUser(username, password, authorizedRemote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tunnel.Credentials = credentials
|
||||
|
||||
key := strconv.Itoa(int(endpointID))
|
||||
service.tunnelDetailsMap.Set(key, tunnel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateRandomCredentials() (string, string) {
|
||||
username := uniuri.NewLen(8)
|
||||
password := uniuri.NewLen(8)
|
||||
return username, password
|
||||
}
|
||||
|
||||
func encryptCredentials(username, password, key string) (string, error) {
|
||||
credentials := fmt.Sprintf("%s:%s", username, password)
|
||||
|
||||
encryptedCredentials, err := libcrypto.Encrypt([]byte(credentials), []byte(key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.RawStdEncoding.EncodeToString(encryptedCredentials), nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
@@ -33,11 +34,13 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
|
||||
flags := &portainer.CLIFlags{
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
|
||||
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
|
||||
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(),
|
||||
@@ -47,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()
|
||||
@@ -74,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
|
||||
}
|
||||
@@ -114,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://") {
|
||||
|
||||
@@ -3,21 +3,23 @@
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
||||
@@ -2,11 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/cron"
|
||||
@@ -20,8 +23,6 @@ import (
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/portainer/portainer/api/ldap"
|
||||
"github.com/portainer/portainer/api/libcompose"
|
||||
|
||||
"log"
|
||||
)
|
||||
|
||||
func initCLI() *portainer.CLIFlags {
|
||||
@@ -69,12 +70,51 @@ func initStore(dataStorePath string, fileService portainer.FileService) *bolt.St
|
||||
return store
|
||||
}
|
||||
|
||||
func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager {
|
||||
return libcompose.NewComposeStackManager(dataStorePath)
|
||||
func initDemoData(store *bolt.Store, cryptoService portainer.CryptoService) error {
|
||||
password, err := cryptoService.Hash("tryportainer")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
admin := &portainer.User{
|
||||
Username: "admin",
|
||||
Password: password,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
|
||||
err = store.UserService.CreateUser(admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localEndpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(1),
|
||||
Name: "local",
|
||||
URL: "unix:///var/run/docker.sock",
|
||||
PublicURL: "demo.portainer.io",
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
err = store.EndpointService.CreateEndpoint(localEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.SwarmStackManager, error) {
|
||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService)
|
||||
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
|
||||
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
|
||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||
@@ -101,11 +141,11 @@ func initLDAPService() portainer.LDAPService {
|
||||
}
|
||||
|
||||
func initGitService() portainer.GitService {
|
||||
return &git.Service{}
|
||||
return git.NewService()
|
||||
}
|
||||
|
||||
func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory {
|
||||
return docker.NewClientFactory(signatureService)
|
||||
func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
|
||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter {
|
||||
@@ -196,7 +236,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
||||
return scheduleService.CreateSchedule(endpointSyncSchedule)
|
||||
}
|
||||
|
||||
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error {
|
||||
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error {
|
||||
schedules, err := scheduleService.Schedules()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -213,6 +253,13 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if schedule.EdgeSchedule != nil {
|
||||
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||
reverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -251,6 +298,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||
LogoURL: *flags.Logo,
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
LDAPSettings: portainer.LDAPSettings{
|
||||
AnonymousMode: true,
|
||||
AutoCreateUsers: true,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
@@ -260,11 +308,16 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
EnableHostManagementFeatures: false,
|
||||
SnapshotInterval: *flags.SnapshotInterval,
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
EnableHostManagementFeatures: false,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
SnapshotInterval: *flags.SnapshotInterval,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
}
|
||||
|
||||
if *flags.Templates != "" {
|
||||
@@ -386,7 +439,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{},
|
||||
}
|
||||
@@ -429,7 +482,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{},
|
||||
}
|
||||
@@ -479,21 +532,11 @@ func initJobService(dockerClientFactory *docker.ClientFactory) portainer.JobServ
|
||||
func initExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) (portainer.ExtensionManager, error) {
|
||||
extensionManager := exec.NewExtensionManager(fileService, extensionService)
|
||||
|
||||
extensions, err := extensionService.Extensions()
|
||||
err := extensionManager.StartExtensions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, extension := range extensions {
|
||||
err := extensionManager.EnableExtension(&extension, extension.License.LicenseKey)
|
||||
if err != nil {
|
||||
log.Printf("Unable to enable extension: %s [extension: %s]", err.Error(), extension.Name)
|
||||
extension.Enabled = false
|
||||
extension.License.Valid = false
|
||||
extensionService.Persist(&extension)
|
||||
}
|
||||
}
|
||||
|
||||
return extensionManager, nil
|
||||
}
|
||||
|
||||
@@ -528,6 +571,8 @@ func main() {
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
|
||||
initDemoData(store, cryptoService)
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
err := initKeyPair(fileService, digitalSignatureService)
|
||||
@@ -540,7 +585,9 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
clientFactory := initClientFactory(digitalSignatureService)
|
||||
reverseTunnelService := chisel.NewService(store.EndpointService, store.TunnelServerService)
|
||||
|
||||
clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
|
||||
jobService := initJobService(clientFactory)
|
||||
|
||||
@@ -551,12 +598,12 @@ func main() {
|
||||
endpointManagement = false
|
||||
}
|
||||
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Data)
|
||||
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
|
||||
|
||||
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
|
||||
if err != nil {
|
||||
@@ -570,7 +617,7 @@ func main() {
|
||||
|
||||
jobScheduler := initJobScheduler()
|
||||
|
||||
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService)
|
||||
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService, reverseTunnelService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -607,7 +654,7 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
adminPasswordHash, err = cryptoService.Hash(string(content))
|
||||
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -622,28 +669,12 @@ func main() {
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
log.Printf("Creating admin user with password hash %s", adminPasswordHash)
|
||||
log.Println("Created admin user with the given password.")
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationPortainerDockerHubInspect: true,
|
||||
portainer.OperationPortainerEndpointGroupList: true,
|
||||
portainer.OperationPortainerEndpointList: true,
|
||||
portainer.OperationPortainerEndpointInspect: true,
|
||||
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||
portainer.OperationPortainerExtensionList: true,
|
||||
portainer.OperationPortainerMOTD: true,
|
||||
portainer.OperationPortainerRegistryList: true,
|
||||
portainer.OperationPortainerRegistryInspect: true,
|
||||
portainer.OperationPortainerTeamList: true,
|
||||
portainer.OperationPortainerTemplateList: true,
|
||||
portainer.OperationPortainerTemplateInspect: true,
|
||||
portainer.OperationPortainerUserList: true,
|
||||
portainer.OperationPortainerUserMemberships: true,
|
||||
},
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
err := store.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
@@ -658,44 +689,53 @@ func main() {
|
||||
go terminateIfNoAdminCreated(store.UserService)
|
||||
}
|
||||
|
||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
AuthDisabled: *flags.NoAuth,
|
||||
EndpointManagement: endpointManagement,
|
||||
RoleService: store.RoleService,
|
||||
UserService: store.UserService,
|
||||
TeamService: store.TeamService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
RegistryService: store.RegistryService,
|
||||
DockerHubService: store.DockerHubService,
|
||||
StackService: store.StackService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
TagService: store.TagService,
|
||||
TemplateService: store.TemplateService,
|
||||
WebhookService: store.WebhookService,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
ExtensionManager: extensionManager,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
GitService: gitService,
|
||||
SignatureService: digitalSignatureService,
|
||||
JobScheduler: jobScheduler,
|
||||
Snapshotter: snapshotter,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
DockerClientFactory: clientFactory,
|
||||
JobService: jobService,
|
||||
ReverseTunnelService: reverseTunnelService,
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
AuthDisabled: *flags.NoAuth,
|
||||
EndpointManagement: endpointManagement,
|
||||
RoleService: store.RoleService,
|
||||
UserService: store.UserService,
|
||||
TeamService: store.TeamService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
EdgeGroupService: store.EdgeGroupService,
|
||||
EdgeStackService: store.EdgeStackService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
RegistryService: store.RegistryService,
|
||||
DockerHubService: store.DockerHubService,
|
||||
StackService: store.StackService,
|
||||
ScheduleService: store.ScheduleService,
|
||||
TagService: store.TagService,
|
||||
TemplateService: store.TemplateService,
|
||||
WebhookService: store.WebhookService,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
ExtensionManager: extensionManager,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
GitService: gitService,
|
||||
SignatureService: digitalSignatureService,
|
||||
JobScheduler: jobScheduler,
|
||||
Snapshotter: snapshotter,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
DockerClientFactory: clientFactory,
|
||||
JobService: jobService,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||
|
||||
@@ -53,7 +53,7 @@ func (runner *SnapshotJobRunner) Run() {
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package cron
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/robfig/cron"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// JobScheduler represents a service for managing crons
|
||||
@@ -19,7 +19,8 @@ func NewJobScheduler() *JobScheduler {
|
||||
|
||||
// ScheduleJob schedules the execution of a job via a runner
|
||||
func (scheduler *JobScheduler) ScheduleJob(runner portainer.JobRunner) error {
|
||||
return scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner)
|
||||
_, err := scheduler.cron.AddJob(runner.GetSchedule().CronExpression, runner)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateSystemJobSchedule updates the first occurence of the specified
|
||||
@@ -35,7 +36,7 @@ func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType
|
||||
|
||||
for _, entry := range cronEntries {
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType {
|
||||
err := newCron.AddJob(newCronExpression, entry.Job)
|
||||
_, err := newCron.AddJob(newCronExpression, entry.Job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -69,7 +70,7 @@ func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) err
|
||||
jobRunner = entry.Job
|
||||
}
|
||||
|
||||
err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
|
||||
_, err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -111,7 +113,7 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||
message = service.secret
|
||||
}
|
||||
|
||||
hash := HashFromBytes([]byte(message))
|
||||
hash := libcrypto.HashFromBytes([]byte(message))
|
||||
|
||||
r := big.NewInt(0)
|
||||
s := big.NewInt(0)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import "crypto/md5"
|
||||
|
||||
// HashFromBytes returns the hash of the specified data
|
||||
func HashFromBytes(data []byte) []byte {
|
||||
digest := md5.New()
|
||||
digest.Write(data)
|
||||
return digest.Sum(nil)
|
||||
}
|
||||
@@ -6,6 +6,24 @@ import (
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// CreateTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
|
||||
func CreateServerTLSConfiguration() *tls.Config {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
// loaded from memory.
|
||||
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
)
|
||||
|
||||
const (
|
||||
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
||||
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
||||
defaultDockerRequestTimeout = 60
|
||||
dockerClientVersion = "1.37"
|
||||
)
|
||||
|
||||
// ClientFactory is used to create Docker clients
|
||||
type ClientFactory struct {
|
||||
signatureService portainer.DigitalSignatureService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewClientFactory returns a new instance of a ClientFactory
|
||||
func NewClientFactory(signatureService portainer.DigitalSignatureService) *ClientFactory {
|
||||
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *ClientFactory {
|
||||
return &ClientFactory{
|
||||
signatureService: signatureService,
|
||||
signatureService: signatureService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +39,8 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
|
||||
return nil, unsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
||||
} else if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
@@ -45,7 +52,7 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
|
||||
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,11 +64,33 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithHTTPClient(httpCli),
|
||||
)
|
||||
}
|
||||
|
||||
func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portainer.ReverseTunnelService, nodeName string) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := map[string]string{}
|
||||
if nodeName != "" {
|
||||
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||
}
|
||||
|
||||
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpointURL),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithHTTPClient(httpCli),
|
||||
client.WithHTTPHeaders(headers),
|
||||
)
|
||||
}
|
||||
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
if err != nil {
|
||||
@@ -84,7 +113,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithHTTPClient(httpCli),
|
||||
client.WithHTTPHeaders(headers),
|
||||
)
|
||||
@@ -103,6 +132,6 @@ func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 30 * time.Second,
|
||||
Timeout: defaultDockerRequestTimeout * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
@@ -10,7 +12,7 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
|
||||
func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
|
||||
_, err := cli.Ping(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -22,44 +24,44 @@ func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
|
||||
|
||||
err = snapshotInfo(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine information] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
if snapshot.Swarm {
|
||||
err = snapshotSwarmServices(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm services] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotNodes(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot Swarm nodes] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = snapshotContainers(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot containers] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotImages(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot images] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotVolumes(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot volumes] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotNetworks(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot networks] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
err = snapshotVersion(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [docker,snapshot] [message: unable to snapshot engine version] [endpoint: %s] [err: %s]", endpoint.Name, err)
|
||||
}
|
||||
|
||||
snapshot.Time = time.Now().Unix()
|
||||
@@ -125,6 +127,8 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
|
||||
|
||||
runningContainers := 0
|
||||
stoppedContainers := 0
|
||||
healthyContainers := 0
|
||||
unhealthyContainers := 0
|
||||
stacks := make(map[string]struct{})
|
||||
for _, container := range containers {
|
||||
if container.State == "exited" {
|
||||
@@ -133,6 +137,12 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
|
||||
runningContainers++
|
||||
}
|
||||
|
||||
if strings.Contains(container.Status, "(healthy)") {
|
||||
healthyContainers++
|
||||
} else if strings.Contains(container.Status, "(unhealthy)") {
|
||||
unhealthyContainers++
|
||||
}
|
||||
|
||||
for k, v := range container.Labels {
|
||||
if k == "com.docker.compose.project" {
|
||||
stacks[v] = struct{}{}
|
||||
@@ -142,6 +152,8 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
|
||||
|
||||
snapshot.RunningContainerCount = runningContainers
|
||||
snapshot.StoppedContainerCount = stoppedContainers
|
||||
snapshot.HealthyContainerCount = healthyContainers
|
||||
snapshot.UnhealthyContainerCount = unhealthyContainers
|
||||
snapshot.StackCount += len(stacks)
|
||||
snapshot.SnapshotRaw.Containers = containers
|
||||
return nil
|
||||
|
||||
@@ -24,5 +24,5 @@ func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*p
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
return snapshot(cli)
|
||||
return snapshot(cli, endpoint)
|
||||
}
|
||||
|
||||
54
api/edgegroup.go
Normal file
54
api/edgegroup.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package portainer
|
||||
|
||||
// EdgeGroupRelatedEndpoints returns a list of endpoints related to this Edge group
|
||||
func EdgeGroupRelatedEndpoints(edgeGroup *EdgeGroup, endpoints []Endpoint, endpointGroups []EndpointGroup) []EndpointID {
|
||||
if !edgeGroup.Dynamic {
|
||||
return edgeGroup.Endpoints
|
||||
}
|
||||
|
||||
endpointIDs := []EndpointID{}
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type != EdgeAgentEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
var endpointGroup EndpointGroup
|
||||
for _, group := range endpointGroups {
|
||||
if endpoint.GroupID == group.ID {
|
||||
endpointGroup = group
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if edgeGroupRelatedToEndpoint(edgeGroup, &endpoint, &endpointGroup) {
|
||||
endpointIDs = append(endpointIDs, endpoint.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return endpointIDs
|
||||
}
|
||||
|
||||
// edgeGroupRelatedToEndpoint returns true is edgeGroup is associated with endpoint
|
||||
func edgeGroupRelatedToEndpoint(edgeGroup *EdgeGroup, endpoint *Endpoint, endpointGroup *EndpointGroup) bool {
|
||||
if !edgeGroup.Dynamic {
|
||||
for _, endpointID := range edgeGroup.Endpoints {
|
||||
if endpoint.ID == endpointID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
endpointTags := TagSet(endpoint.TagIDs)
|
||||
if endpointGroup.TagIDs != nil {
|
||||
endpointTags = TagUnion(endpointTags, TagSet(endpointGroup.TagIDs))
|
||||
}
|
||||
edgeGroupTags := TagSet(edgeGroup.TagIDs)
|
||||
|
||||
if edgeGroup.PartialMatch {
|
||||
intersection := TagIntersection(endpointTags, edgeGroupTags)
|
||||
return len(intersection) != 0
|
||||
}
|
||||
|
||||
return TagContains(edgeGroupTags, endpointTags)
|
||||
}
|
||||
27
api/edgestack.go
Normal file
27
api/edgestack.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package portainer
|
||||
|
||||
import "errors"
|
||||
|
||||
// EdgeStackRelatedEndpoints returns a list of endpoints related to this Edge stack
|
||||
func EdgeStackRelatedEndpoints(edgeGroupIDs []EdgeGroupID, endpoints []Endpoint, endpointGroups []EndpointGroup, edgeGroups []EdgeGroup) ([]EndpointID, error) {
|
||||
edgeStackEndpoints := []EndpointID{}
|
||||
|
||||
for _, edgeGroupID := range edgeGroupIDs {
|
||||
var edgeGroup *EdgeGroup
|
||||
|
||||
for _, group := range edgeGroups {
|
||||
if group.ID == edgeGroupID {
|
||||
edgeGroup = &group
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if edgeGroup == nil {
|
||||
return nil, errors.New("Edge group was not found")
|
||||
}
|
||||
|
||||
edgeStackEndpoints = append(edgeStackEndpoints, EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)...)
|
||||
}
|
||||
|
||||
return edgeStackEndpoints, nil
|
||||
}
|
||||
25
api/endpoint.go
Normal file
25
api/endpoint.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package portainer
|
||||
|
||||
// EndpointRelatedEdgeStacks returns a list of Edge stacks related to this Endpoint
|
||||
func EndpointRelatedEdgeStacks(endpoint *Endpoint, endpointGroup *EndpointGroup, edgeGroups []EdgeGroup, edgeStacks []EdgeStack) []EdgeStackID {
|
||||
relatedEdgeGroupsSet := map[EdgeGroupID]bool{}
|
||||
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
if edgeGroupRelatedToEndpoint(&edgeGroup, endpoint, endpointGroup) {
|
||||
relatedEdgeGroupsSet[edgeGroup.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
relatedEdgeStacks := []EdgeStackID{}
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for _, edgeGroupID := range edgeStack.EdgeGroups {
|
||||
if relatedEdgeGroupsSet[edgeGroupID] {
|
||||
relatedEdgeStacks = append(relatedEdgeStacks, edgeStack.ID)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relatedEdgeStacks
|
||||
|
||||
}
|
||||
@@ -89,6 +89,11 @@ const (
|
||||
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
|
||||
)
|
||||
|
||||
// Demo errors.
|
||||
const (
|
||||
ErrNotAvailableInDemo = Error("This feature is not available in the demo version of Portainer")
|
||||
)
|
||||
|
||||
// Extension errors.
|
||||
const (
|
||||
ErrExtensionAlreadyEnabled = Error("This extension is already enabled")
|
||||
|
||||
@@ -4,19 +4,26 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
"github.com/orcaman/concurrent-map"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
)
|
||||
|
||||
var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/extensions/"
|
||||
var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/"
|
||||
var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`)
|
||||
|
||||
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
||||
portainer.RegistryManagementExtension: "extension-registry-management",
|
||||
@@ -46,20 +53,11 @@ func processKey(ID portainer.ExtensionID) string {
|
||||
}
|
||||
|
||||
func buildExtensionURL(extension *portainer.Extension) string {
|
||||
extensionURL := extensionDownloadBaseURL
|
||||
extensionURL += extensionBinaryMap[extension.ID]
|
||||
extensionURL += "-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
extensionURL += "-" + extension.Version
|
||||
extensionURL += ".zip"
|
||||
return extensionURL
|
||||
return fmt.Sprintf("%s%s-%s-%s-%s.zip", extensionDownloadBaseURL, extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
||||
}
|
||||
|
||||
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
|
||||
|
||||
extensionFilename := extensionBinaryMap[extension.ID]
|
||||
extensionFilename += "-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
extensionFilename += "-" + extension.Version
|
||||
|
||||
extensionFilename := fmt.Sprintf("%s-%s-%s-%s", extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
||||
if runtime.GOOS == "windows" {
|
||||
extensionFilename += ".exe"
|
||||
}
|
||||
@@ -72,11 +70,20 @@ func buildExtensionPath(binaryPath string, extension *portainer.Extension) strin
|
||||
}
|
||||
|
||||
// FetchExtensionDefinitions will fetch the list of available
|
||||
// extension definitions from the official Portainer assets server
|
||||
// extension definitions from the official Portainer assets server.
|
||||
// If it cannot retrieve the data from the Internet it will fallback to the locally cached
|
||||
// manifest file.
|
||||
func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
|
||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 30)
|
||||
var extensionData []byte
|
||||
|
||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extensions manifest via Internet. Extensions will be retrieved from local cache and might not be up to date] [err: %s]", err)
|
||||
|
||||
extensionData, err = manager.fileService.GetFileContent(portainer.LocalExtensionManifestFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var extensions []portainer.Extension
|
||||
@@ -88,6 +95,37 @@ func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extens
|
||||
return extensions, nil
|
||||
}
|
||||
|
||||
// InstallExtension will install the extension from an archive. It will extract the extension version number from
|
||||
// the archive file name first and return an error if the file name is not valid (cannot find extension version).
|
||||
// It will then extract the archive and execute the EnableExtension function to enable the extension.
|
||||
// Since we're missing information about this extension (stored on Portainer.io server) we need to assume
|
||||
// default information based on the extension ID.
|
||||
func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error {
|
||||
extensionVersion := extensionVersionRegexp.FindString(archiveFileName)
|
||||
if extensionVersion == "" {
|
||||
return errors.New("invalid extension archive filename: unable to retrieve extension version")
|
||||
}
|
||||
|
||||
err := manager.fileService.ExtractExtensionArchive(extensionArchive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch extension.ID {
|
||||
case portainer.RegistryManagementExtension:
|
||||
extension.Name = "Registry Manager"
|
||||
case portainer.OAuthAuthenticationExtension:
|
||||
extension.Name = "External Authentication"
|
||||
case portainer.RBACExtension:
|
||||
extension.Name = "Role-Based Access Control"
|
||||
}
|
||||
extension.ShortDescription = "Extension enabled offline"
|
||||
extension.Version = extensionVersion
|
||||
extension.Available = true
|
||||
|
||||
return manager.EnableExtension(extension, licenseKey)
|
||||
}
|
||||
|
||||
// EnableExtension will check for the existence of the extension binary on the filesystem
|
||||
// first. If it does not exist, it will download it from the official Portainer assets server.
|
||||
// After installing the binary on the filesystem, it will execute the binary in license check
|
||||
@@ -144,6 +182,61 @@ func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension
|
||||
return manager.fileService.RemoveDirectory(extensionBinaryPath)
|
||||
}
|
||||
|
||||
// StartExtensions will retrieve the extensions definitions from the Internet and check if a new version of each
|
||||
// extension is available. If so, it will automatically install the new version of the extension. If no update is
|
||||
// available it will simply start the extension.
|
||||
// The purpose of this function is to be ran at startup, as such most of the error handling won't block the program execution
|
||||
// and will log warning messages instead.
|
||||
func (manager *ExtensionManager) StartExtensions() error {
|
||||
extensions, err := manager.extensionService.Extensions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
definitions, err := manager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extension information from Internet. Skipping extensions update check.] [err: %s]", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager.updateAndStartExtensions(extensions, definitions)
|
||||
}
|
||||
|
||||
func (manager *ExtensionManager) updateAndStartExtensions(extensions []portainer.Extension, definitions []portainer.Extension) error {
|
||||
for _, definition := range definitions {
|
||||
for _, extension := range extensions {
|
||||
if extension.ID == definition.ID {
|
||||
definitionVersion := semver.New(definition.Version)
|
||||
extensionVersion := semver.New(extension.Version)
|
||||
|
||||
if extensionVersion.LessThan(*definitionVersion) {
|
||||
log.Printf("[INFO] [exec,extensions] [message: new version detected, updating extension] [extension: %s] [current_version: %s] [available_version: %s]", extension.Name, extension.Version, definition.Version)
|
||||
err := manager.UpdateExtension(&extension, definition.Version)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to update extension automatically] [extension: %s] [current_version: %s] [available_version: %s] [err: %s]", extension.Name, extension.Version, definition.Version, err)
|
||||
}
|
||||
} else {
|
||||
err := manager.EnableExtension(&extension, extension.License.LicenseKey)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to start extension] [extension: %s] [err: %s]", extension.Name, err)
|
||||
extension.Enabled = false
|
||||
extension.License.Valid = false
|
||||
}
|
||||
}
|
||||
|
||||
err := manager.extensionService.Persist(&extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateExtension will download the new extension binary from the official Portainer assets
|
||||
// server, disable the previous extension via DisableExtension, trigger a license check
|
||||
// and then start the extension process and add it to the processes map
|
||||
@@ -193,7 +286,8 @@ func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
||||
|
||||
err := licenseCheckProcess.Run()
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid extension license key")
|
||||
log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
|
||||
return nil, errors.New("invalid extension license key")
|
||||
}
|
||||
|
||||
output := string(cmdOutput.Bytes())
|
||||
@@ -203,8 +297,12 @@ func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
||||
|
||||
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
|
||||
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
|
||||
extensionProcess.Stdout = os.Stdout
|
||||
extensionProcess.Stderr = os.Stderr
|
||||
|
||||
err := extensionProcess.Start()
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] [exec,extension] [message: unable to start extension process] [err: %s]", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package exec
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -13,20 +14,22 @@ import (
|
||||
|
||||
// SwarmStackManager represents a service for managing stacks.
|
||||
type SwarmStackManager struct {
|
||||
binaryPath string
|
||||
dataPath string
|
||||
signatureService portainer.DigitalSignatureService
|
||||
fileService portainer.FileService
|
||||
binaryPath string
|
||||
dataPath string
|
||||
signatureService portainer.DigitalSignatureService
|
||||
fileService portainer.FileService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
||||
// It also updates the configuration of the Docker CLI binary.
|
||||
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*SwarmStackManager, error) {
|
||||
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
|
||||
manager := &SwarmStackManager{
|
||||
binaryPath: binaryPath,
|
||||
dataPath: dataPath,
|
||||
signatureService: signatureService,
|
||||
fileService: fileService,
|
||||
binaryPath: binaryPath,
|
||||
dataPath: dataPath,
|
||||
signatureService: signatureService,
|
||||
fileService: fileService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
}
|
||||
|
||||
err := manager.updateDockerCLIConfiguration(dataPath)
|
||||
@@ -39,7 +42,7 @@ func NewSwarmStackManager(binaryPath, dataPath string, signatureService portaine
|
||||
|
||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
||||
@@ -55,7 +58,7 @@ func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registri
|
||||
|
||||
// Logout executes the docker logout command.
|
||||
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
args = append(args, "logout")
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -63,7 +66,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
|
||||
if prune {
|
||||
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
||||
@@ -82,7 +85,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
|
||||
|
||||
// Remove executes the docker stack rm command.
|
||||
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
@@ -106,7 +109,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||
// Assume Linux as a default
|
||||
command := path.Join(binaryPath, "docker")
|
||||
|
||||
@@ -116,7 +119,14 @@ func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portaine
|
||||
|
||||
args := make([]string, 0)
|
||||
args = append(args, "--config", dataPath)
|
||||
args = append(args, "-H", endpoint.URL)
|
||||
|
||||
endpointURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
args = append(args, "-H", endpointURL)
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
args = append(args, "--tls")
|
||||
|
||||
@@ -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.
|
||||
@@ -87,12 +89,7 @@ func (service *Service) GetBinaryFolder() string {
|
||||
// ExtractExtensionArchive extracts the content of an extension archive
|
||||
// specified as raw data into the binary store on the filesystem
|
||||
func (service *Service) ExtractExtensionArchive(data []byte) error {
|
||||
err := archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
|
||||
}
|
||||
|
||||
// RemoveDirectory removes a directory on the filesystem.
|
||||
@@ -126,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.
|
||||
|
||||
@@ -1,21 +1,37 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport/client"
|
||||
githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Git.
|
||||
type Service struct{}
|
||||
type Service struct {
|
||||
httpsCli *http.Client
|
||||
}
|
||||
|
||||
// NewService initializes a new service.
|
||||
func NewService(dataStorePath string) (*Service, error) {
|
||||
service := &Service{}
|
||||
func NewService() *Service {
|
||||
httpsCli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
Timeout: 300 * time.Second,
|
||||
}
|
||||
|
||||
return service, nil
|
||||
client.InstallProtocol("https", githttp.NewClient(httpsCli))
|
||||
|
||||
return &Service{
|
||||
httpsCli: httpsCli,
|
||||
}
|
||||
}
|
||||
|
||||
// ClonePublicRepository clones a public git repository using the specified URL in the specified
|
||||
@@ -32,7 +48,7 @@ func (service *Service) ClonePrivateRepositoryWithBasicAuth(repositoryURL, refer
|
||||
return cloneRepository(repositoryURL, referenceName, destination)
|
||||
}
|
||||
|
||||
func cloneRepository(repositoryURL, referenceName string, destination string) error {
|
||||
func cloneRepository(repositoryURL, referenceName, destination string) error {
|
||||
options := &git.CloneOptions{
|
||||
URL: repositoryURL,
|
||||
}
|
||||
|
||||
40
api/go.mod
Normal file
40
api/go.mod
Normal file
@@ -0,0 +1,40 @@
|
||||
module github.com/portainer/portainer/api
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.4.14
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||
github.com/boltdb/bolt v1.3.1
|
||||
github.com/containerd/containerd v1.3.1 // indirect
|
||||
github.com/coreos/go-semver v0.3.0
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
|
||||
github.com/docker/docker v0.0.0-00010101000000-000000000000
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/imdario/mergo v0.3.8 // indirect
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
|
||||
github.com/json-iterator/go v1.1.8
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/mattn/go-shellwords v1.0.6 // indirect
|
||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/portainer/libcompose v0.5.3
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/robfig/cron/v3 v3.0.0
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-00010101000000-000000000000 // indirect
|
||||
gopkg.in/ldap.v2 v2.5.1
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
)
|
||||
|
||||
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
|
||||
|
||||
replace gopkg.in/asn1-ber.v1 => github.com/go-asn1-ber/asn1-ber v1.3.1
|
||||
286
api/go.sum
Normal file
286
api/go.sum
Normal file
@@ -0,0 +1,286 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.3.8 h1:dvxbxtpTIjdAbx2OtL26p4eq0iEvys/U5yrsTJb3NZI=
|
||||
github.com/Microsoft/go-winio v0.3.8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/hcsshim v0.8.6 h1:ZfF0+zZeYdzMIVMZHKtDKJvLHj76XCuVae/jNkjj0IA=
|
||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2 h1:axBiC50cNZOs7ygH5BgQp4N+aYrZ2DNpWZ1KG3VOSOM=
|
||||
github.com/andrew-d/go-termutil v0.0.0-20150726205930-009166a695a2/go.mod h1:jnzFpU88PccN/tPPhCpnNU8mZphvKxYM9lLNkd8e+os=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/containerd v1.3.1 h1:LdbWxLhkAIxGO7h3mATHkyav06WuDs/yTWxIljJOTks=
|
||||
github.com/containerd/containerd v1.3.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
|
||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
|
||||
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/docker/cli v0.0.0-20190711175710-5b38d82aa076/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9 h1:Q6D6b2iRKhvtL3Wj9p0SyPOvUDJ1ht62mbiBoNJ3Aus=
|
||||
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
|
||||
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
|
||||
github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292 h1:qQ7mw+CVWpRj5DWBL4CVHtBbGQdlPCj4j1evDh0ethw=
|
||||
github.com/docker/engine v1.4.2-0.20191127222017-3152f9436292/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
||||
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203 h1:QeBh8wW8pIZKlXxlMOQ8hSCMdJA+2Z/bD/iDyCAS8XU=
|
||||
github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
||||
github.com/docker/engine v1.13.1 h1:Cks33UT9YBW5Xyc3MtGDq2IPgqfJtJ+qkFaxc2b0Euc=
|
||||
github.com/docker/engine v1.13.1/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
|
||||
github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o=
|
||||
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA=
|
||||
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf700kykAImbLiEMU0Q3QN9hQ26Js1pU=
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v0.0.0-20160317213430-0eeaf8392f5b/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 h1:l5rH/CnVVu+HPxjtxjM90nHrm4nov3j3RF9/62UjgLs=
|
||||
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669/go.mod h1:kOeLNvjNBGSV3uYtFjvb72+fnZCMFJF1XDvRIjdom0g=
|
||||
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389 h1:K3JsoRqX6C4gmTvY4jqtFGCfK8uToj9DMahciJaoWwE=
|
||||
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389/go.mod h1:wHQUFFnFySoqdAOzjHkTvb4DsVM1h/73PS9l2vnioRM=
|
||||
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 h1:7ufdyC3aMxFcCv+ABZy/dmIVGKFoGNBCqOgLYPIckD8=
|
||||
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82/go.mod h1:w8buj+yNfmLEP0ENlbG/FRnK6bVmuhqXnukYCs9sDvY=
|
||||
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 h1:0c9jcgBtHRtDU//jTrcCgWG6UHjMZytiq/3WhraNgUM=
|
||||
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9/go.mod h1:1ffp+CRe0eAwwRb0/BownUAjMBsmTLwgAvRbfj9dRwE=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok=
|
||||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c h1:N7A4JCA2G+j5fuFxCsJqjFU/sZe0mj8H0sSoSwbaikw=
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c/go.mod h1:Nn5wlyECw3iJrzi0AhIWg+AJUb4PlRQVW4/3XHH1LZA=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v0.0.0-20150511174710-5cf931ef8f76/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-shellwords v1.0.6 h1:9Jok5pILi5S1MnDirGVTufYGtksUs/V2BWUP3ZkeUUI=
|
||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microsoft/go-winio v0.4.8 h1:N4SmTFXUK7/jnn/UG/gm2mrHiYu9LVGvtsvULyody/c=
|
||||
github.com/microsoft/go-winio v0.4.8/go.mod h1:kcIxxtKZE55DEncT/EOvFiygPobhUWpSDqDb47poQOU=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
|
||||
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM=
|
||||
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449 h1:Aq8iG72akPb/kszE7ksZ5ldV+JYPYii/KZOxlpJF07s=
|
||||
github.com/opencontainers/image-spec v0.0.0-20170515205857-f03dbe35d449/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c h1:iOMba/KmaXgSX5PFKu1u6s+DZXiq+EzPayawa76w6aA=
|
||||
github.com/opencontainers/runc v0.0.0-20161109192122-51371867a01c/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw=
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/libcompose v0.5.3 h1:tE4WcPuGvo+NKeDkDWpwNavNLZ5GHIJ4RvuZXsI9uI8=
|
||||
github.com/portainer/libcompose v0.5.3/go.mod h1:7SKd/ho69rRKHDFSDUwkbMcol2TMKU5OslDsajr8Ro8=
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2 h1:0PfgGLys9yHr4rtnirg0W0Cjvv6/DzxBIZk5sV59208=
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2/go.mod h1:/wIeGwJOMYc1JplE/OvYMO5korce39HddIfI8VKGyAM=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
|
||||
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.1.0 h1:ngVtJC9TY/lg0AA/1k48FYhBrhRoFlEmWzsehpNAaZg=
|
||||
github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8=
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
|
||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c h1:jceGD5YNJGgGMkJz79agzOln1K9TaZUjv5ird16qniQ=
|
||||
golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.22.1 h1:/7cs52RnTJmD43s3uxzlq2U7nqVTd/37viQwMrMNlOM=
|
||||
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
|
||||
gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -87,6 +88,7 @@ func Get(url string, timeout int) ([]byte, error) {
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
log.Printf("[ERROR] [http,client] [message: unexpected status code] [status_code: %d]", response.StatusCode)
|
||||
return nil, errInvalidResponseStatus
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
|
||||
}
|
||||
|
||||
if err == portainer.ErrObjectNotFound && settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
if err == portainer.ErrObjectNotFound && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
|
||||
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", portainer.ErrUnauthorized}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,11 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
@@ -98,25 +103,9 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationPortainerDockerHubInspect: true,
|
||||
portainer.OperationPortainerEndpointGroupList: true,
|
||||
portainer.OperationPortainerEndpointList: true,
|
||||
portainer.OperationPortainerEndpointInspect: true,
|
||||
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||
portainer.OperationPortainerExtensionList: true,
|
||||
portainer.OperationPortainerMOTD: true,
|
||||
portainer.OperationPortainerRegistryList: true,
|
||||
portainer.OperationPortainerRegistryInspect: true,
|
||||
portainer.OperationPortainerTeamList: true,
|
||||
portainer.OperationPortainerTemplateList: true,
|
||||
portainer.OperationPortainerTemplateInspect: true,
|
||||
portainer.OperationPortainerUserList: true,
|
||||
portainer.OperationPortainerUserMemberships: true,
|
||||
},
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
@@ -129,64 +118,24 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
PortainerAuthorizations: user.PortainerAuthorizations,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
}
|
||||
|
||||
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return handler.persistAndWriteToken(w, tokenData)
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpointAuthorizations, err := handler.getAuthorizations(user)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve authorizations associated to the user", err}
|
||||
}
|
||||
tokenData.EndpointAuthorizations = endpointAuthorizations
|
||||
|
||||
return handler.persistAndWriteToken(w, tokenData)
|
||||
}
|
||||
|
||||
func (handler *Handler) getAuthorizations(user *portainer.User) (portainer.EndpointAuthorizations, error) {
|
||||
endpointAuthorizations := portainer.EndpointAuthorizations{}
|
||||
if user.Role == portainer.AdministratorRole {
|
||||
return endpointAuthorizations, nil
|
||||
}
|
||||
|
||||
userMemberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(user.ID)
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
roles, err := handler.RoleService.Roles()
|
||||
if err != nil {
|
||||
return endpointAuthorizations, err
|
||||
}
|
||||
|
||||
endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships)
|
||||
|
||||
return endpointAuthorizations, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
@@ -231,6 +180,7 @@ func (handler *Handler) addUserIntoTeams(user *portainer.User, settings *portain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -111,25 +111,9 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
if user == nil {
|
||||
user = &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationPortainerDockerHubInspect: true,
|
||||
portainer.OperationPortainerEndpointGroupList: true,
|
||||
portainer.OperationPortainerEndpointList: true,
|
||||
portainer.OperationPortainerEndpointInspect: true,
|
||||
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||
portainer.OperationPortainerExtensionList: true,
|
||||
portainer.OperationPortainerMOTD: true,
|
||||
portainer.OperationPortainerRegistryList: true,
|
||||
portainer.OperationPortainerRegistryInspect: true,
|
||||
portainer.OperationPortainerTeamList: true,
|
||||
portainer.OperationPortainerTemplateList: true,
|
||||
portainer.OperationPortainerTemplateInspect: true,
|
||||
portainer.OperationPortainerUserList: true,
|
||||
portainer.OperationPortainerUserMemberships: true,
|
||||
},
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: portainer.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
@@ -149,6 +133,11 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package auth
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func getUserEndpointAuthorizations(user *portainer.User, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, roles []portainer.Role, userMemberships []portainer.TeamMembership) portainer.EndpointAuthorizations {
|
||||
endpointAuthorizations := make(portainer.EndpointAuthorizations)
|
||||
|
||||
groupUserAccessPolicies := map[portainer.EndpointGroupID]portainer.UserAccessPolicies{}
|
||||
groupTeamAccessPolicies := map[portainer.EndpointGroupID]portainer.TeamAccessPolicies{}
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies
|
||||
groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
continue
|
||||
}
|
||||
|
||||
authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
continue
|
||||
}
|
||||
|
||||
authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles)
|
||||
if len(authorizations) > 0 {
|
||||
endpointAuthorizations[endpoint.ID] = authorizations
|
||||
continue
|
||||
}
|
||||
|
||||
endpointAuthorizations[endpoint.ID] = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
|
||||
}
|
||||
|
||||
return endpointAuthorizations
|
||||
}
|
||||
|
||||
func getAuthorizationsFromUserEndpointPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
|
||||
policyRoles := make([]portainer.RoleID, 0)
|
||||
|
||||
policy, ok := endpoint.UserAccessPolicies[user.ID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromUserEndpointGroupPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.UserAccessPolicies) portainer.Authorizations {
|
||||
policyRoles := make([]portainer.RoleID, 0)
|
||||
|
||||
policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromTeamEndpointPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
|
||||
policyRoles := make([]portainer.RoleID, 0)
|
||||
|
||||
for _, membership := range memberships {
|
||||
policy, ok := endpoint.TeamAccessPolicies[membership.TeamID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.TeamAccessPolicies) portainer.Authorizations {
|
||||
policyRoles := make([]portainer.RoleID, 0)
|
||||
|
||||
for _, membership := range memberships {
|
||||
policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID]
|
||||
if ok {
|
||||
policyRoles = append(policyRoles, policy.RoleID)
|
||||
}
|
||||
}
|
||||
|
||||
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||
}
|
||||
|
||||
func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []portainer.Role) portainer.Authorizations {
|
||||
var roleAuthorizations []portainer.Authorizations
|
||||
for _, id := range roleIdentifiers {
|
||||
for _, role := range roles {
|
||||
if role.ID == id {
|
||||
roleAuthorizations = append(roleAuthorizations, role.Authorizations)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processedAuthorizations := make(portainer.Authorizations)
|
||||
if len(roleAuthorizations) > 0 {
|
||||
processedAuthorizations = roleAuthorizations[0]
|
||||
for idx, authorizations := range roleAuthorizations {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
processedAuthorizations = mergeAuthorizations(processedAuthorizations, authorizations)
|
||||
}
|
||||
}
|
||||
|
||||
return processedAuthorizations
|
||||
}
|
||||
|
||||
func mergeAuthorizations(a, b portainer.Authorizations) portainer.Authorizations {
|
||||
c := make(map[portainer.Authorization]bool)
|
||||
|
||||
for k := range b {
|
||||
if _, ok := a[k]; ok {
|
||||
c[k] = true
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -34,6 +34,7 @@ type Handler struct {
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
RoleService portainer.RoleService
|
||||
ProxyManager *proxy.Manager
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
|
||||
@@ -25,9 +25,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
104
api/http/handler/edgegroups/associated_endpoints.go
Normal file
104
api/http/handler/edgegroups/associated_endpoints.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type endpointSetType map[portainer.EndpointID]bool
|
||||
|
||||
func (handler *Handler) getEndpointsByTags(tagIDs []portainer.TagID, partialMatch bool) ([]portainer.EndpointID, error) {
|
||||
if len(tagIDs) == 0 {
|
||||
return []portainer.EndpointID{}, nil
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupEndpoints := mapEndpointGroupToEndpoints(endpoints)
|
||||
|
||||
tags := []portainer.Tag{}
|
||||
for _, tagID := range tagIDs {
|
||||
tag, err := handler.TagService.Tag(tagID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, *tag)
|
||||
}
|
||||
|
||||
setsOfEndpoints := mapTagsToEndpoints(tags, groupEndpoints)
|
||||
|
||||
var endpointSet endpointSetType
|
||||
if partialMatch {
|
||||
endpointSet = setsUnion(setsOfEndpoints)
|
||||
} else {
|
||||
endpointSet = setsIntersection(setsOfEndpoints)
|
||||
}
|
||||
|
||||
results := []portainer.EndpointID{}
|
||||
for _, endpoint := range endpoints {
|
||||
if _, ok := endpointSet[endpoint.ID]; ok && endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
results = append(results, endpoint.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType {
|
||||
groupEndpoints := map[portainer.EndpointGroupID]endpointSetType{}
|
||||
for _, endpoint := range endpoints {
|
||||
groupID := endpoint.GroupID
|
||||
if groupEndpoints[groupID] == nil {
|
||||
groupEndpoints[groupID] = endpointSetType{}
|
||||
}
|
||||
groupEndpoints[groupID][endpoint.ID] = true
|
||||
}
|
||||
return groupEndpoints
|
||||
}
|
||||
|
||||
func mapTagsToEndpoints(tags []portainer.Tag, groupEndpoints map[portainer.EndpointGroupID]endpointSetType) []endpointSetType {
|
||||
sets := []endpointSetType{}
|
||||
for _, tag := range tags {
|
||||
set := tag.Endpoints
|
||||
for groupID := range tag.EndpointGroups {
|
||||
for endpointID := range groupEndpoints[groupID] {
|
||||
set[endpointID] = true
|
||||
}
|
||||
}
|
||||
sets = append(sets, set)
|
||||
}
|
||||
|
||||
return sets
|
||||
}
|
||||
|
||||
func setsIntersection(sets []endpointSetType) endpointSetType {
|
||||
if len(sets) == 0 {
|
||||
return endpointSetType{}
|
||||
}
|
||||
|
||||
intersectionSet := sets[0]
|
||||
|
||||
for _, set := range sets {
|
||||
for endpointID := range intersectionSet {
|
||||
if !set[endpointID] {
|
||||
delete(intersectionSet, endpointID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return intersectionSet
|
||||
}
|
||||
|
||||
func setsUnion(sets []endpointSetType) endpointSetType {
|
||||
unionSet := endpointSetType{}
|
||||
|
||||
for _, set := range sets {
|
||||
for endpointID := range set {
|
||||
unionSet[endpointID] = true
|
||||
}
|
||||
}
|
||||
|
||||
return unionSet
|
||||
}
|
||||
83
api/http/handler/edgegroups/edgegroup_create.go
Normal file
83
api/http/handler/edgegroups/edgegroup_create.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type edgeGroupCreatePayload struct {
|
||||
Name string
|
||||
Dynamic bool
|
||||
TagIDs []portainer.TagID
|
||||
Endpoints []portainer.EndpointID
|
||||
PartialMatch bool
|
||||
}
|
||||
|
||||
func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid Edge group name")
|
||||
}
|
||||
if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) {
|
||||
return portainer.Error("TagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
|
||||
return portainer.Error("Endpoints is mandatory for a static Edge group")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload edgeGroupCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
|
||||
}
|
||||
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
if edgeGroup.Name == payload.Name {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")}
|
||||
}
|
||||
}
|
||||
|
||||
edgeGroup := &portainer.EdgeGroup{
|
||||
Name: payload.Name,
|
||||
Dynamic: payload.Dynamic,
|
||||
TagIDs: []portainer.TagID{},
|
||||
Endpoints: []portainer.EndpointID{},
|
||||
PartialMatch: payload.PartialMatch,
|
||||
}
|
||||
|
||||
if edgeGroup.Dynamic {
|
||||
edgeGroup.TagIDs = payload.TagIDs
|
||||
} else {
|
||||
endpointIDs := []portainer.EndpointID{}
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
endpointIDs = append(endpointIDs, endpoint.ID)
|
||||
}
|
||||
}
|
||||
edgeGroup.Endpoints = endpointIDs
|
||||
}
|
||||
|
||||
err = handler.EdgeGroupService.CreateEdgeGroup(edgeGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the Edge group inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
}
|
||||
45
api/http/handler/edgegroups/edgegroup_delete.go
Normal file
45
api/http/handler/edgegroups/edgegroup_delete.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (handler *Handler) edgeGroupDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
|
||||
}
|
||||
|
||||
_, err = handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err}
|
||||
}
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for _, groupID := range edgeStack.EdgeGroups {
|
||||
if groupID == portainer.EdgeGroupID(edgeGroupID) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Edge group is used by an Edge stack", portainer.Error("Edge group is used by an Edge stack")}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.EdgeGroupService.DeleteEdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the Edge group from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
|
||||
}
|
||||
35
api/http/handler/edgegroups/edgegroup_inspect.go
Normal file
35
api/http/handler/edgegroups/edgegroup_inspect.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if edgeGroup.Dynamic {
|
||||
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err}
|
||||
}
|
||||
|
||||
edgeGroup.Endpoints = endpoints
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
}
|
||||
55
api/http/handler/edgegroups/edgegroup_list.go
Normal file
55
api/http/handler/edgegroups/edgegroup_list.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type decoratedEdgeGroup struct {
|
||||
portainer.EdgeGroup
|
||||
HasEdgeStack bool `json:"HasEdgeStack"`
|
||||
}
|
||||
|
||||
func (handler *Handler) edgeGroupList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge stacks from the database", err}
|
||||
}
|
||||
|
||||
usedEdgeGroups := make(map[portainer.EdgeGroupID]bool)
|
||||
|
||||
for _, stack := range edgeStacks {
|
||||
for _, groupID := range stack.EdgeGroups {
|
||||
usedEdgeGroups[groupID] = true
|
||||
}
|
||||
}
|
||||
|
||||
decoratedEdgeGroups := []decoratedEdgeGroup{}
|
||||
for _, orgEdgeGroup := range edgeGroups {
|
||||
edgeGroup := decoratedEdgeGroup{
|
||||
EdgeGroup: orgEdgeGroup,
|
||||
}
|
||||
if edgeGroup.Dynamic {
|
||||
endpoints, err := handler.getEndpointsByTags(edgeGroup.TagIDs, edgeGroup.PartialMatch)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints and endpoint groups for Edge group", err}
|
||||
}
|
||||
|
||||
edgeGroup.Endpoints = endpoints
|
||||
}
|
||||
|
||||
edgeGroup.HasEdgeStack = usedEdgeGroups[edgeGroup.ID]
|
||||
|
||||
decoratedEdgeGroups = append(decoratedEdgeGroups, edgeGroup)
|
||||
}
|
||||
|
||||
return response.JSON(w, decoratedEdgeGroups)
|
||||
}
|
||||
154
api/http/handler/edgegroups/edgegroup_update.go
Normal file
154
api/http/handler/edgegroups/edgegroup_update.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type edgeGroupUpdatePayload struct {
|
||||
Name string
|
||||
Dynamic bool
|
||||
TagIDs []portainer.TagID
|
||||
Endpoints []portainer.EndpointID
|
||||
PartialMatch *bool
|
||||
}
|
||||
|
||||
func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid Edge group name")
|
||||
}
|
||||
if payload.Dynamic && (payload.TagIDs == nil || len(payload.TagIDs) == 0) {
|
||||
return portainer.Error("TagIDs is mandatory for a dynamic Edge group")
|
||||
}
|
||||
if !payload.Dynamic && (payload.Endpoints == nil || len(payload.Endpoints) == 0) {
|
||||
return portainer.Error("Endpoints is mandatory for a static Edge group")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Edge group identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload edgeGroupUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
edgeGroup, err := handler.EdgeGroupService.EdgeGroup(portainer.EdgeGroupID(edgeGroupID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an Edge group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge groups from the database", err}
|
||||
}
|
||||
for _, edgeGroup := range edgeGroups {
|
||||
if edgeGroup.Name == payload.Name && edgeGroup.ID != portainer.EdgeGroupID(edgeGroupID) {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Edge group name must be unique", portainer.Error("Edge group name must be unique")}
|
||||
}
|
||||
}
|
||||
|
||||
edgeGroup.Name = payload.Name
|
||||
}
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
|
||||
}
|
||||
|
||||
oldRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
|
||||
edgeGroup.Dynamic = payload.Dynamic
|
||||
if edgeGroup.Dynamic {
|
||||
edgeGroup.TagIDs = payload.TagIDs
|
||||
} else {
|
||||
endpointIDs := []portainer.EndpointID{}
|
||||
for _, endpointID := range payload.Endpoints {
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint from the database", err}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
endpointIDs = append(endpointIDs, endpoint.ID)
|
||||
}
|
||||
}
|
||||
edgeGroup.Endpoints = endpointIDs
|
||||
}
|
||||
|
||||
if payload.PartialMatch != nil {
|
||||
edgeGroup.PartialMatch = *payload.PartialMatch
|
||||
}
|
||||
|
||||
err = handler.EdgeGroupService.UpdateEdgeGroup(edgeGroup.ID, edgeGroup)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Edge group changes inside the database", err}
|
||||
}
|
||||
|
||||
newRelatedEndpoints := portainer.EdgeGroupRelatedEndpoints(edgeGroup, endpoints, endpointGroups)
|
||||
endpointsToUpdate := append(newRelatedEndpoints, oldRelatedEndpoints...)
|
||||
|
||||
for _, endpointID := range endpointsToUpdate {
|
||||
err = handler.updateEndpoint(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Endpoint relation changes inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeGroup)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpoint(endpointID portainer.EndpointID) error {
|
||||
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeStackSet := map[portainer.EdgeStackID]bool{}
|
||||
|
||||
endpointEdgeStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
for _, edgeStackID := range endpointEdgeStacks {
|
||||
edgeStackSet[edgeStackID] = true
|
||||
}
|
||||
|
||||
relation.EdgeStacks = edgeStackSet
|
||||
|
||||
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, relation)
|
||||
}
|
||||
39
api/http/handler/edgegroups/handler.go
Normal file
39
api/http/handler/edgegroups/handler.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package edgegroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
EdgeGroupService portainer.EdgeGroupService
|
||||
EdgeStackService portainer.EdgeStackService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
EndpointRelationService portainer.EndpointRelationService
|
||||
TagService portainer.TagService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/edge_groups",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_groups",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupList))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/edge_groups/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeGroupDelete))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
289
api/http/handler/edgestacks/edgestack_create.go
Normal file
289
api/http/handler/edgestacks/edgestack_create.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
// POST request on /api/endpoint_groups
|
||||
func (handler *Handler) edgeStackCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
|
||||
}
|
||||
|
||||
edgeStack, err := handler.createSwarmStack(method, r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Edge stack", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
|
||||
}
|
||||
|
||||
relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
|
||||
|
||||
for _, endpointID := range relatedEndpoints {
|
||||
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
|
||||
}
|
||||
|
||||
relation.EdgeStacks[edgeStack.ID] = true
|
||||
|
||||
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeStack)
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStack(method string, r *http.Request) (*portainer.EdgeStack, error) {
|
||||
switch method {
|
||||
case "string":
|
||||
return handler.createSwarmStackFromFileContent(r)
|
||||
case "repository":
|
||||
return handler.createSwarmStackFromGitRepository(r)
|
||||
case "file":
|
||||
return handler.createSwarmStackFromFileUpload(r)
|
||||
}
|
||||
return nil, errors.New("Invalid value for query parameter: method. Value must be one of: string, repository or file")
|
||||
}
|
||||
|
||||
type swarmStackFromFileContentPayload struct {
|
||||
Name string
|
||||
StackFileContent string
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return portainer.Error("Invalid stack file content")
|
||||
}
|
||||
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
|
||||
return portainer.Error("Edge Groups are mandatory for an Edge stack")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromFileContent(r *http.Request) (*portainer.EdgeStack, error) {
|
||||
var payload swarmStackFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = handler.validateUniqueName(payload.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stackID := handler.EdgeStackService.GetNextIdentifier()
|
||||
stack := &portainer.EdgeStack{
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.EdgeStackService.CreateEdgeStack(stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
type swarmStackFromGitRepositoryPayload struct {
|
||||
Name string
|
||||
RepositoryURL string
|
||||
RepositoryReferenceName string
|
||||
RepositoryAuthentication bool
|
||||
RepositoryUsername string
|
||||
RepositoryPassword string
|
||||
ComposeFilePathInRepository string
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
if payload.RepositoryAuthentication && (govalidator.IsNull(payload.RepositoryUsername) || govalidator.IsNull(payload.RepositoryPassword)) {
|
||||
return portainer.Error("Invalid repository credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if govalidator.IsNull(payload.ComposeFilePathInRepository) {
|
||||
payload.ComposeFilePathInRepository = filesystem.ComposeFileDefaultName
|
||||
}
|
||||
if payload.EdgeGroups == nil || len(payload.EdgeGroups) == 0 {
|
||||
return portainer.Error("Edge Groups are mandatory for an Edge stack")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromGitRepository(r *http.Request) (*portainer.EdgeStack, error) {
|
||||
var payload swarmStackFromGitRepositoryPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = handler.validateUniqueName(payload.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stackID := handler.EdgeStackService.GetNextIdentifier()
|
||||
stack := &portainer.EdgeStack{
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetEdgeStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
gitCloneParams := &cloneRepositoryParameters{
|
||||
url: payload.RepositoryURL,
|
||||
referenceName: payload.RepositoryReferenceName,
|
||||
path: projectPath,
|
||||
authentication: payload.RepositoryAuthentication,
|
||||
username: payload.RepositoryUsername,
|
||||
password: payload.RepositoryPassword,
|
||||
}
|
||||
|
||||
err = handler.cloneGitRepository(gitCloneParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = handler.EdgeStackService.CreateEdgeStack(stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
type swarmStackFromFileUploadPayload struct {
|
||||
Name string
|
||||
StackFileContent []byte
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
}
|
||||
|
||||
func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Compose file. Ensure that the Compose file is uploaded correctly")
|
||||
}
|
||||
payload.StackFileContent = composeFileContent
|
||||
|
||||
var edgeGroups []portainer.EdgeGroupID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "EdgeGroups", &edgeGroups, false)
|
||||
if err != nil || len(edgeGroups) == 0 {
|
||||
return portainer.Error("Edge Groups are mandatory for an Edge stack")
|
||||
}
|
||||
payload.EdgeGroups = edgeGroups
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromFileUpload(r *http.Request) (*portainer.EdgeStack, error) {
|
||||
payload := &swarmStackFromFileUploadPayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = handler.validateUniqueName(payload.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stackID := handler.EdgeStackService.GetNextIdentifier()
|
||||
stack := &portainer.EdgeStack{
|
||||
ID: portainer.EdgeStackID(stackID),
|
||||
Name: payload.Name,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
CreationDate: time.Now().Unix(),
|
||||
EdgeGroups: payload.EdgeGroups,
|
||||
Status: make(map[portainer.EndpointID]portainer.EdgeStackStatus),
|
||||
Version: 1,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.EdgeStackService.CreateEdgeStack(stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) validateUniqueName(name string) error {
|
||||
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, stack := range edgeStacks {
|
||||
if strings.EqualFold(stack.Name, name) {
|
||||
return portainer.Error("Edge stack name must be unique")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
62
api/http/handler/edgestacks/edgestack_delete.go
Normal file
62
api/http/handler/edgestacks/edgestack_delete.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (handler *Handler) edgeStackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.EdgeStackService.DeleteEdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the edge stack from the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
|
||||
}
|
||||
|
||||
relatedEndpoints, err := portainer.EdgeStackRelatedEndpoints(edgeStack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
|
||||
|
||||
for _, endpointID := range relatedEndpoints {
|
||||
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
|
||||
}
|
||||
|
||||
delete(relation.EdgeStacks, edgeStack.ID)
|
||||
|
||||
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
37
api/http/handler/edgestacks/edgestack_file.go
Normal file
37
api/http/handler/edgestacks/edgestack_file.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type stackFileResponse struct {
|
||||
StackFileContent string `json:"StackFileContent"`
|
||||
}
|
||||
|
||||
// GET request on /api/edge_stacks/:id/file
|
||||
func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
|
||||
}
|
||||
|
||||
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &stackFileResponse{StackFileContent: string(stackFileContent)})
|
||||
}
|
||||
26
api/http/handler/edgestacks/edgestack_inspect.go
Normal file
26
api/http/handler/edgestacks/edgestack_inspect.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (handler *Handler) edgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeStack)
|
||||
}
|
||||
17
api/http/handler/edgestacks/edgestack_list.go
Normal file
17
api/http/handler/edgestacks/edgestack_list.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
func (handler *Handler) edgeStackList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stacks from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, edgeStacks)
|
||||
}
|
||||
76
api/http/handler/edgestacks/edgestack_status_update.go
Normal file
76
api/http/handler/edgestacks/edgestack_status_update.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type updateStatusPayload struct {
|
||||
Error string
|
||||
Status *portainer.EdgeStackStatusType
|
||||
EndpointID *portainer.EndpointID
|
||||
}
|
||||
|
||||
func (payload *updateStatusPayload) Validate(r *http.Request) error {
|
||||
if payload.Status == nil {
|
||||
return portainer.Error("Invalid status")
|
||||
}
|
||||
if payload.EndpointID == nil {
|
||||
return portainer.Error("Invalid EndpointID")
|
||||
}
|
||||
if *payload.Status == portainer.StatusError && govalidator.IsNull(payload.Error) {
|
||||
return portainer.Error("Error message is mandatory when status is error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var payload updateStatusPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(*payload.EndpointID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
stack.Status[*payload.EndpointID] = portainer.EdgeStackStatus{
|
||||
Type: *payload.Status,
|
||||
Error: payload.Error,
|
||||
EndpointID: *payload.EndpointID,
|
||||
}
|
||||
|
||||
err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
|
||||
}
|
||||
156
api/http/handler/edgestacks/edgestack_update.go
Normal file
156
api/http/handler/edgestacks/edgestack_update.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type updateEdgeStackPayload struct {
|
||||
StackFileContent string
|
||||
Version *int
|
||||
Prune *bool
|
||||
EdgeGroups []portainer.EdgeGroupID
|
||||
}
|
||||
|
||||
func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return portainer.Error("Invalid stack file content")
|
||||
}
|
||||
if payload.EdgeGroups != nil && len(payload.EdgeGroups) == 0 {
|
||||
return portainer.Error("Edge Groups are mandatory for an Edge stack")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) edgeStackUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
stack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(stackID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var payload updateEdgeStackPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
if payload.EdgeGroups != nil {
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from database", err}
|
||||
}
|
||||
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from database", err}
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge groups from database", err}
|
||||
}
|
||||
|
||||
oldRelated, err := portainer.EdgeStackRelatedEndpoints(stack.EdgeGroups, endpoints, endpointGroups, edgeGroups)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
|
||||
}
|
||||
|
||||
newRelated, err := portainer.EdgeStackRelatedEndpoints(payload.EdgeGroups, endpoints, endpointGroups, edgeGroups)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve edge stack related endpoints from database", err}
|
||||
}
|
||||
|
||||
oldRelatedSet := EndpointSet(oldRelated)
|
||||
newRelatedSet := EndpointSet(newRelated)
|
||||
|
||||
endpointsToRemove := map[portainer.EndpointID]bool{}
|
||||
for endpointID := range oldRelatedSet {
|
||||
if !newRelatedSet[endpointID] {
|
||||
endpointsToRemove[endpointID] = true
|
||||
}
|
||||
}
|
||||
|
||||
for endpointID := range endpointsToRemove {
|
||||
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
|
||||
}
|
||||
|
||||
delete(relation.EdgeStacks, stack.ID)
|
||||
|
||||
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
|
||||
}
|
||||
}
|
||||
|
||||
endpointsToAdd := map[portainer.EndpointID]bool{}
|
||||
for endpointID := range newRelatedSet {
|
||||
if !oldRelatedSet[endpointID] {
|
||||
endpointsToAdd[endpointID] = true
|
||||
}
|
||||
}
|
||||
|
||||
for endpointID := range endpointsToAdd {
|
||||
relation, err := handler.EndpointRelationService.EndpointRelation(endpointID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find endpoint relation in database", err}
|
||||
}
|
||||
|
||||
relation.EdgeStacks[stack.ID] = true
|
||||
|
||||
err = handler.EndpointRelationService.UpdateEndpointRelation(endpointID, relation)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint relation in database", err}
|
||||
}
|
||||
}
|
||||
|
||||
stack.EdgeGroups = payload.EdgeGroups
|
||||
|
||||
}
|
||||
|
||||
if payload.Prune != nil {
|
||||
stack.Prune = *payload.Prune
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
_, err = handler.FileService.StoreEdgeStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist updated Compose file on disk", err}
|
||||
}
|
||||
|
||||
if payload.Version != nil && *payload.Version != stack.Version {
|
||||
stack.Version = *payload.Version
|
||||
stack.Status = map[portainer.EndpointID]portainer.EdgeStackStatus{}
|
||||
}
|
||||
|
||||
err = handler.EdgeStackService.UpdateEdgeStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func EndpointSet(endpointIDs []portainer.EndpointID) map[portainer.EndpointID]bool {
|
||||
set := map[portainer.EndpointID]bool{}
|
||||
|
||||
for _, endpointID := range endpointIDs {
|
||||
set[endpointID] = true
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
17
api/http/handler/edgestacks/git.go
Normal file
17
api/http/handler/edgestacks/git.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package edgestacks
|
||||
|
||||
type cloneRepositoryParameters struct {
|
||||
url string
|
||||
referenceName string
|
||||
path string
|
||||
authentication bool
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func (handler *Handler) cloneGitRepository(parameters *cloneRepositoryParameters) error {
|
||||
if parameters.authentication {
|
||||
return handler.GitService.ClonePrivateRepositoryWithBasicAuth(parameters.url, parameters.referenceName, parameters.path, parameters.username, parameters.password)
|
||||
}
|
||||
return handler.GitService.ClonePublicRepository(parameters.url, parameters.referenceName, parameters.path)
|
||||
}
|
||||
46
api/http/handler/edgestacks/handler.go
Normal file
46
api/http/handler/edgestacks/handler.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package edgestacks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
EdgeGroupService portainer.EdgeGroupService
|
||||
EdgeStackService portainer.EdgeStackService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
EndpointRelationService portainer.EndpointRelationService
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/edge_stacks",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackList))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/edge_stacks/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/edge_stacks/{id}/file",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeStackFile))).Methods(http.MethodGet)
|
||||
h.Handle("/edge_stacks/{id}/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.edgeStackStatusUpdate))).Methods(http.MethodPut)
|
||||
return h
|
||||
}
|
||||
49
api/http/handler/edgetemplates/edgetemplate_list.go
Normal file
49
api/http/handler/edgetemplates/edgetemplate_list.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package edgetemplates
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
)
|
||||
|
||||
// GET request on /api/edgetemplates
|
||||
func (handler *Handler) edgeTemplateList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
url := portainer.EdgeTemplatesURL
|
||||
if settings.TemplatesURL != "" {
|
||||
url = settings.TemplatesURL
|
||||
}
|
||||
|
||||
var templateData []byte
|
||||
templateData, err = client.Get(url, 0)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve external templates", err}
|
||||
}
|
||||
|
||||
var templates []portainer.Template
|
||||
|
||||
err = json.Unmarshal(templateData, &templates)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] [http,edge,templates] [failed parsing edge templates] [body: %s]", templateData)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external templates", err}
|
||||
}
|
||||
|
||||
filteredTemplates := []portainer.Template{}
|
||||
|
||||
for _, template := range templates {
|
||||
if template.Type == portainer.EdgeStackTemplate {
|
||||
filteredTemplates = append(filteredTemplates, template)
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, filteredTemplates)
|
||||
}
|
||||
31
api/http/handler/edgetemplates/handler.go
Normal file
31
api/http/handler/edgetemplates/handler.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package edgetemplates
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle edge endpoint operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
|
||||
h.Handle("/edge_templates",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.edgeTemplateList))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
60
api/http/handler/endpointedge/endpoint_edgestack_inspect.go
Normal file
60
api/http/handler/endpointedge/endpoint_edgestack_inspect.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package endpointedge
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type configResponse struct {
|
||||
Prune bool
|
||||
StackFileContent string
|
||||
Name string
|
||||
}
|
||||
|
||||
// GET request on api/endpoints/:id/edge/stacks/:stackId
|
||||
func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "stackId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid edge stack identifier route variable", err}
|
||||
}
|
||||
|
||||
edgeStack, err := handler.EdgeStackService.EdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an edge stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, configResponse{
|
||||
Prune: edgeStack.Prune,
|
||||
StackFileContent: string(stackFileContent),
|
||||
Name: edgeStack.Name,
|
||||
})
|
||||
}
|
||||
33
api/http/handler/endpointedge/handler.go
Normal file
33
api/http/handler/endpointedge/handler.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package endpointedge
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle edge endpoint operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
EndpointService portainer.EndpointService
|
||||
EdgeStackService portainer.EdgeStackService
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
|
||||
h.Handle("/{id}/edge/stacks/{stackId}",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -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)
|
||||
@@ -53,14 +53,39 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, payload.AssociatedEndpoints)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
for _, id := range payload.AssociatedEndpoints {
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.ID == id {
|
||||
endpoint.GroupID = endpointGroup.ID
|
||||
|
||||
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
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 {
|
||||
@@ -37,13 +37,41 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
|
||||
updateAuthorizations = true
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateAuthorizations {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// PUT request on /api/endpoint_groups/:id/endpoints/:endpointId
|
||||
func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
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 {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
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}
|
||||
}
|
||||
|
||||
endpoint.GroupID = endpointGroup.ID
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// DELETE request on /api/endpoint_groups/:id/endpoints/:endpointId
|
||||
func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
_, 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 {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
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}
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -10,12 +11,11 @@ import (
|
||||
)
|
||||
|
||||
type endpointGroupUpdatePayload struct {
|
||||
Name string
|
||||
Description string
|
||||
AssociatedEndpoints []portainer.EndpointID
|
||||
Tags []string
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
Name string
|
||||
Description string
|
||||
TagIDs []portainer.TagID
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
}
|
||||
|
||||
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -50,16 +50,55 @@ 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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if payload.UserAccessPolicies != nil {
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) {
|
||||
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.TeamAccessPolicies != nil {
|
||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
|
||||
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
@@ -67,16 +106,26 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||
}
|
||||
|
||||
if payload.AssociatedEndpoints != nil {
|
||||
if updateAuthorizations {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
api/http/handler/endpointgroups/endpoints.go
Normal file
42
api/http/handler/endpointgroups/endpoints.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package endpointgroups
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (handler *Handler) updateEndpointRelations(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) error {
|
||||
if endpoint.Type != portainer.EdgeAgentEnvironment {
|
||||
return nil
|
||||
}
|
||||
|
||||
if endpointGroup == nil {
|
||||
unassignedGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointGroup = unassignedGroup
|
||||
}
|
||||
|
||||
endpointRelation, err := handler.EndpointRelationService.EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeGroups, err := handler.EdgeGroupService.EdgeGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeStacks, err := handler.EdgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpointStacks := portainer.EndpointRelatedEdgeStacks(endpoint, endpointGroup, edgeGroups, edgeStacks)
|
||||
stacksSet := map[portainer.EdgeStackID]bool{}
|
||||
for _, edgeStackID := range endpointStacks {
|
||||
stacksSet[edgeStackID] = true
|
||||
}
|
||||
endpointRelation.EdgeStacks = stacksSet
|
||||
|
||||
return handler.EndpointRelationService.UpdateEndpointRelation(endpoint.ID, endpointRelation)
|
||||
}
|
||||
@@ -12,8 +12,13 @@ import (
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
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.
|
||||
@@ -22,46 +27,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/endpoint_groups",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoint_groups",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
||||
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupAddEndpoint))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
|
||||
for _, id := range associatedEndpoints {
|
||||
if id == endpoint.ID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
}
|
||||
|
||||
func (handler *Handler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||
for _, id := range associatedEndpoints {
|
||||
|
||||
if id == endpoint.ID {
|
||||
endpoint.GroupID = groupID
|
||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||
if endpoint.GroupID == groupID {
|
||||
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
|
||||
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
// Handler is the HTTP handler used to proxy requests to external APIs.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
EndpointService portainer.EndpointService
|
||||
ProxyManager *proxy.Manager
|
||||
requestBouncer *security.RequestBouncer
|
||||
EndpointService portainer.EndpointService
|
||||
SettingsService portainer.SettingsService
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to proxy requests to external APIs.
|
||||
@@ -23,10 +25,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
h.PathPrefix("/{id}/azure").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
||||
h.PathPrefix("/{id}/docker").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||
h.PathPrefix("/{id}/storidge").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package endpointproxy
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -24,19 +25,39 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if endpoint.Status == portainer.EndpointStatusDown {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
if endpoint.EdgeID == "" {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")}
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||
time.Sleep(waitForAgentToConnect * 2)
|
||||
}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"log"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -29,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 {
|
||||
@@ -41,7 +44,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
|
||||
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
|
||||
if err != nil || endpointType == 0 {
|
||||
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 3 (Azure environment)")
|
||||
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)")
|
||||
}
|
||||
payload.EndpointType = endpointType
|
||||
|
||||
@@ -51,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)
|
||||
@@ -143,12 +146,46 @@ 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)
|
||||
}
|
||||
|
||||
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
|
||||
return handler.createAzureEndpoint(payload)
|
||||
} else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment {
|
||||
return handler.createEdgeAgentEndpoint(payload)
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
@@ -182,14 +219,60 @@ 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{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
endpointType := portainer.EdgeAgentEnvironment
|
||||
endpointID := handler.EndpointService.GetNextIdentifier()
|
||||
|
||||
portainerURL, err := url.Parse(payload.URL)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", err}
|
||||
}
|
||||
|
||||
portainerHost, _, err := net.SplitHostPort(portainerURL.Host)
|
||||
if err != nil {
|
||||
portainerHost = portainerURL.Host
|
||||
}
|
||||
|
||||
if portainerHost == "localhost" {
|
||||
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", errors.New("cannot use localhost as endpoint URL")}
|
||||
}
|
||||
|
||||
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: payload.Name,
|
||||
URL: portainerHost,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
Extensions: []portainer.EndpointExtension{},
|
||||
TagIDs: payload.TagIDs,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.Snapshot{},
|
||||
EdgeKey: edgeKey,
|
||||
}
|
||||
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
@@ -227,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{},
|
||||
}
|
||||
@@ -271,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{},
|
||||
}
|
||||
@@ -293,17 +376,51 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint)
|
||||
snapshot, err := handler.Snapshotter.CreateSnapshot(endpoint)
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
if err != nil {
|
||||
log.Printf("http error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
if strings.Contains(err.Error(), "Invalid request signature") {
|
||||
err = errors.New("agent already paired with another Portainer instance")
|
||||
}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to initiate communications with endpoint", err}
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured while trying to create the endpoint", err}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error {
|
||||
err := handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group, err := handler.EndpointGroupService.EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(group.UserAccessPolicies) > 0 || len(group.TeamAccessPolicies) > 0 {
|
||||
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
|
||||
|
||||
@@ -21,6 +21,10 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
if endpointID == 1 {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "This feature is not available in the demo version of Portainer", portainer.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
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}
|
||||
@@ -41,7 +45,84 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
@@ -2,24 +2,52 @@ package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/endpoints
|
||||
// GET request on /api/endpoints?(start=<start>)&(limit=<limit>)&(search=<search>)&(groupId=<groupId)
|
||||
func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
start, _ := request.RetrieveNumericQueryParameter(r, "start", true)
|
||||
if start != 0 {
|
||||
start--
|
||||
}
|
||||
|
||||
search, _ := request.RetrieveQueryParameter(r, "search", true)
|
||||
if search != "" {
|
||||
search = strings.ToLower(search)
|
||||
}
|
||||
|
||||
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 {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
@@ -27,9 +55,235 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
|
||||
for idx := range filteredEndpoints {
|
||||
hideFields(&filteredEndpoints[idx])
|
||||
if endpointIDs != nil {
|
||||
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
|
||||
}
|
||||
|
||||
return response.JSON(w, filteredEndpoints)
|
||||
if groupID != 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
|
||||
}
|
||||
|
||||
if 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)
|
||||
|
||||
paginatedEndpoints := paginateEndpoints(filteredEndpoints, start, limit)
|
||||
|
||||
for idx := range paginatedEndpoints {
|
||||
hideFields(&paginatedEndpoints[idx])
|
||||
}
|
||||
|
||||
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
||||
return response.JSON(w, paginatedEndpoints)
|
||||
}
|
||||
|
||||
func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []portainer.Endpoint {
|
||||
if limit == 0 {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
endpointCount := len(endpoints)
|
||||
|
||||
if start > endpointCount {
|
||||
start = endpointCount
|
||||
}
|
||||
|
||||
end := start + limit
|
||||
if end > endpointCount {
|
||||
end = endpointCount
|
||||
}
|
||||
|
||||
return endpoints[start:end]
|
||||
}
|
||||
|
||||
func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID portainer.EndpointGroupID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == endpointGroupID {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
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 {
|
||||
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
|
||||
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
|
||||
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
|
||||
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
|
||||
return true
|
||||
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
|
||||
return true
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
98
api/http/handler/endpoints/endpoint_status_inspect.go
Normal file
98
api/http/handler/endpoints/endpoint_status_inspect.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/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
|
||||
func (handler *Handler) endpointStatusInspect(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}
|
||||
}
|
||||
|
||||
if endpoint.EdgeID == "" {
|
||||
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||
|
||||
endpoint.EdgeID = edgeIdentifier
|
||||
|
||||
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
statusResponse := endpointStatusInspectResponse{
|
||||
Status: tunnel.Status,
|
||||
Port: tunnel.Port,
|
||||
Schedules: tunnel.Schedules,
|
||||
CheckinInterval: settings.EdgeAgentCheckinInterval,
|
||||
Credentials: tunnel.Credentials,
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired {
|
||||
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)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -23,7 +24,7 @@ type endpointUpdatePayload struct {
|
||||
AzureApplicationID *string
|
||||
AzureTenantID *string
|
||||
AzureAuthenticationKey *string
|
||||
Tags []string
|
||||
TagIDs []portainer.TagID
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
}
|
||||
@@ -68,20 +69,63 @@ 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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if payload.UserAccessPolicies != nil {
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
|
||||
endpoint.UserAccessPolicies = payload.UserAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.TeamAccessPolicies != nil {
|
||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) {
|
||||
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.Status != nil {
|
||||
@@ -162,7 +206,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||
}
|
||||
@@ -173,5 +217,48 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
if updateAuthorizations {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ const (
|
||||
|
||||
func hideFields(endpoint *portainer.Endpoint) {
|
||||
endpoint.AzureCredentials = portainer.AzureCredentials{}
|
||||
if len(endpoint.Snapshots) > 0 {
|
||||
endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint operations.
|
||||
@@ -26,12 +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
|
||||
Snapshotter portainer.Snapshotter
|
||||
TagService portainer.TagService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
@@ -43,24 +53,26 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
||||
}
|
||||
|
||||
h.Handle("/endpoints",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/snapshot",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/extensions",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
h.Handle("/endpoints/{id}/job",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/snapshot",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
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
|
||||
}
|
||||
|
||||
117
api/http/handler/extensions/data.go
Normal file
117
api/http/handler/extensions/data.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 4
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func updateTeamAccessPolicyToReadOnlyRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 4
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func (handler *Handler) upgradeRBACData() error {
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for key := range endpointGroup.UserAccessPolicies {
|
||||
updateUserAccessPolicyToReadOnlyRole(endpointGroup.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpointGroup.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToReadOnlyRole(endpointGroup.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for key := range endpoint.UserAccessPolicies {
|
||||
updateUserAccessPolicyToReadOnlyRole(endpoint.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpoint.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToReadOnlyRole(endpoint.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
func updateUserAccessPolicyToNoRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 0
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func updateTeamAccessPolicyToNoRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 0
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func (handler *Handler) downgradeRBACData() error {
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for key := range endpointGroup.UserAccessPolicies {
|
||||
updateUserAccessPolicyToNoRole(endpointGroup.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpointGroup.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToNoRole(endpointGroup.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for key := range endpoint.UserAccessPolicies {
|
||||
updateUserAccessPolicyToNoRole(endpoint.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpoint.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToNoRole(endpoint.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
@@ -41,16 +41,21 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err}
|
||||
}
|
||||
|
||||
for _, existingExtension := range extensions {
|
||||
if existingExtension.ID == extensionID && existingExtension.Enabled {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
|
||||
}
|
||||
}
|
||||
|
||||
extension := &portainer.Extension{
|
||||
ID: extensionID,
|
||||
}
|
||||
|
||||
for _, existingExtension := range extensions {
|
||||
if existingExtension.ID == extensionID && (existingExtension.Enabled || !existingExtension.License.Valid) {
|
||||
if existingExtension.License.LicenseKey == payload.License {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
|
||||
}
|
||||
|
||||
_ = handler.ExtensionManager.DisableExtension(&existingExtension)
|
||||
extension.Enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
extensionDefinitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
||||
@@ -68,15 +73,14 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to enable extension", err}
|
||||
}
|
||||
|
||||
extension.Enabled = true
|
||||
|
||||
if extension.ID == portainer.RBACExtension {
|
||||
if extension.ID == portainer.RBACExtension && !extension.Enabled {
|
||||
err = handler.upgradeRBACData()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||
}
|
||||
}
|
||||
|
||||
extension.Enabled = true
|
||||
err = handler.ExtensionService.Persist(extension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user