Compare commits
341 Commits
feat1752-s
...
refactor-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb44a0cc8 | ||
|
|
b3bf6b6115 | ||
|
|
aaf533df8e | ||
|
|
f5edc1f54e | ||
|
|
76ff555c1e | ||
|
|
0c7d69eb17 | ||
|
|
efd421bff8 | ||
|
|
9e9843db9b | ||
|
|
18feb4ee95 | ||
|
|
26a6f3c237 | ||
|
|
3b8f982dbd | ||
|
|
dbab524e5d | ||
|
|
1618388e39 | ||
|
|
0037a6699a | ||
|
|
83d32cdc0f | ||
|
|
cdf880a397 | ||
|
|
ac4af41317 | ||
|
|
ce6cb837f9 | ||
|
|
9967ae5994 | ||
|
|
a171e540c5 | ||
|
|
cb858f0412 | ||
|
|
82078a8d8f | ||
|
|
2b31f489d9 | ||
|
|
e2a17480af | ||
|
|
0670079566 | ||
|
|
5ca9501540 | ||
|
|
415c1759d1 | ||
|
|
db0091b46d | ||
|
|
42529cc5ea | ||
|
|
60fbfeba23 | ||
|
|
f5091ce5fb | ||
|
|
58962de20e | ||
|
|
1eb7e6bacc | ||
|
|
130baddea0 | ||
|
|
9cbf1f34a7 | ||
|
|
c152d3f62e | ||
|
|
da44f14e07 | ||
|
|
49516e2c3f | ||
|
|
9c4c782a90 | ||
|
|
7aa6a30614 | ||
|
|
99e50370bd | ||
|
|
dc2a8cf1f4 | ||
|
|
b9ac3d4286 | ||
|
|
6711e6c969 | ||
|
|
4a5fa211a7 | ||
|
|
d510d23408 | ||
|
|
ce9e009e22 | ||
|
|
9918c1260b | ||
|
|
e325ad10dd | ||
|
|
73f20b5157 | ||
|
|
b6f04c5e0d | ||
|
|
2ef8c0b33e | ||
|
|
7643f8d08c | ||
|
|
086bad2956 | ||
|
|
d5dfc889bb | ||
|
|
ef926dce33 | ||
|
|
d768e72a21 | ||
|
|
78e2aaf7d4 | ||
|
|
17cf374c30 | ||
|
|
165096bef0 | ||
|
|
de76ba4e67 | ||
|
|
b1e048e218 | ||
|
|
8f32d58fae | ||
|
|
16226b1202 | ||
|
|
8f568c8699 | ||
|
|
af34b99cd4 | ||
|
|
2755527d28 | ||
|
|
4d8133f696 | ||
|
|
fdc11dbe3a | ||
|
|
d1f7e17ee6 | ||
|
|
508352f4ea | ||
|
|
9b6b6e09ae | ||
|
|
899cd5f279 | ||
|
|
bb8857bddc | ||
|
|
ce13167c98 | ||
|
|
2eec8b75d0 | ||
|
|
048c74a0dc | ||
|
|
6b1c476b63 | ||
|
|
c5b5f80bea | ||
|
|
cea2c60b55 | ||
|
|
576f369152 | ||
|
|
fca4f619b5 | ||
|
|
90281fd7f0 | ||
|
|
c1939f6070 | ||
|
|
50c604ee4c | ||
|
|
41ded64037 | ||
|
|
801336336f | ||
|
|
90a0998502 | ||
|
|
1a4dff536d | ||
|
|
f772cd31cb | ||
|
|
8160fe4717 | ||
|
|
86c60807cd | ||
|
|
c1f2d90997 | ||
|
|
3699b794eb | ||
|
|
69252a8377 | ||
|
|
193e7eb3f8 | ||
|
|
de5f6086d0 | ||
|
|
46e8f10aea | ||
|
|
60040e90d0 | ||
|
|
c5c06b307a | ||
|
|
c28274667d | ||
|
|
54163e3b92 | ||
|
|
62eb47b3cb | ||
|
|
808eb7d341 | ||
|
|
a33eca4bbb | ||
|
|
50e77d2bf1 | ||
|
|
50a3b08209 | ||
|
|
0a439b3893 | ||
|
|
0d4e1d00f0 | ||
|
|
b09f491f62 | ||
|
|
dc067b3308 | ||
|
|
b121f975fa | ||
|
|
3f44925d7e | ||
|
|
80d570861d | ||
|
|
317bd53e43 | ||
|
|
24f066716b | ||
|
|
4cbde7bb0d | ||
|
|
f6bdc5c2b3 | ||
|
|
c650fe56c2 | ||
|
|
fc8938e871 | ||
|
|
44b7e0fdca | ||
|
|
fe63b4a156 | ||
|
|
42365a52b1 | ||
|
|
d6aafceba8 | ||
|
|
c7983d8993 | ||
|
|
34667bd3b3 | ||
|
|
3a3577754e | ||
|
|
bed49c37e4 | ||
|
|
dedc02cc8d | ||
|
|
4cecde7f96 | ||
|
|
17ac3e5ed1 | ||
|
|
25620c5008 | ||
|
|
9bebe9dee7 | ||
|
|
81e3ace232 | ||
|
|
15b6941872 | ||
|
|
7aaa9e58e9 | ||
|
|
515daf6dba | ||
|
|
0a1643bbcf | ||
|
|
38f24683a6 | ||
|
|
7494101a4d | ||
|
|
996319d299 | ||
|
|
2ee6f2780b | ||
|
|
241a701eca | ||
|
|
463b379876 | ||
|
|
f2cd33e831 | ||
|
|
a8e90e881d | ||
|
|
cb42525c18 | ||
|
|
3dfac3599a | ||
|
|
5f4c42393d | ||
|
|
4148e7140d | ||
|
|
6b05a35881 | ||
|
|
6648c0bbe7 | ||
|
|
dbda568481 | ||
|
|
189d131105 | ||
|
|
1384359baf | ||
|
|
6c26cf1f39 | ||
|
|
8780b0a901 | ||
|
|
f5ada3085e | ||
|
|
acc5218c16 | ||
|
|
8a186b4024 | ||
|
|
5c2e714e69 | ||
|
|
f222b3cb1a | ||
|
|
e440ba53cb | ||
|
|
035d71edb2 | ||
|
|
17d85fdc15 | ||
|
|
df30246595 | ||
|
|
eed11e33f3 | ||
|
|
42a357f863 | ||
|
|
6fd5ddc802 | ||
|
|
f5dc663879 | ||
|
|
79c24ced96 | ||
|
|
65979709e9 | ||
|
|
2541f4daea | ||
|
|
1a94158f77 | ||
|
|
9e1800e2ec | ||
|
|
a9b107dbb5 | ||
|
|
101bb41587 | ||
|
|
acce5e0023 | ||
|
|
816a963308 | ||
|
|
119391bb52 | ||
|
|
bd0fd358ce | ||
|
|
5fa4403d20 | ||
|
|
dc9a878f4b | ||
|
|
692ab5e983 | ||
|
|
ea496c1acb | ||
|
|
b44b1202b0 | ||
|
|
aa14a8ba35 | ||
|
|
2fce3c110b | ||
|
|
f538e4e626 | ||
|
|
4accf4a4c7 | ||
|
|
8fd6891cd6 | ||
|
|
969f70edeb | ||
|
|
c778e79004 | ||
|
|
34b886d690 | ||
|
|
9d8bf5df63 | ||
|
|
3c66fa8084 | ||
|
|
515e2ec337 | ||
|
|
27c179df93 | ||
|
|
b809177147 | ||
|
|
52788029ed | ||
|
|
d510bbbcfd | ||
|
|
17d63ae3ca | ||
|
|
5e49f934b9 | ||
|
|
d03fd5805a | ||
|
|
fe8dfee69a | ||
|
|
488dc5f9db | ||
|
|
0ef25a4cbd | ||
|
|
94d3d7bde2 | ||
|
|
40e0c3879c | ||
|
|
d455ab3fc7 | ||
|
|
0825d05546 | ||
|
|
cf370f6a4c | ||
|
|
381ab81fdd | ||
|
|
64c29f7402 | ||
|
|
558036ee53 | ||
|
|
1b57b657bd | ||
|
|
a2d9f591a7 | ||
|
|
a9e00b89fa | ||
|
|
dd571a792f | ||
|
|
e7ab057c81 | ||
|
|
309620545c | ||
|
|
55b50c2a49 | ||
|
|
807c830db0 | ||
|
|
695c28d4f8 | ||
|
|
4740375ba5 | ||
|
|
7d32a6619d | ||
|
|
a5dd408048 | ||
|
|
04a7944b32 | ||
|
|
1267bbcd5e | ||
|
|
59c2353c50 | ||
|
|
f6e1782632 | ||
|
|
ae7fff3fe9 | ||
|
|
110fcc46a6 | ||
|
|
dbbea0a20f | ||
|
|
f60fc1cd69 | ||
|
|
ac090129aa | ||
|
|
7661b55920 | ||
|
|
c52eb28b79 | ||
|
|
e94d6ad6b2 | ||
|
|
bedeb3e354 | ||
|
|
78bf374548 | ||
|
|
62b14bf9aa | ||
|
|
652b73390a | ||
|
|
5001de43fc | ||
|
|
661e207a4b | ||
|
|
8df64031e8 | ||
|
|
a61654a35d | ||
|
|
2ce830eb16 | ||
|
|
86bb816cf1 | ||
|
|
61d33383b3 | ||
|
|
bc45470eeb | ||
|
|
fbf24c88af | ||
|
|
71447e2cc0 | ||
|
|
354fda31f1 | ||
|
|
6ab510e5cb | ||
|
|
7e6c647e93 | ||
|
|
07c1e1bc3e | ||
|
|
fe6ca042f3 | ||
|
|
9813099aa4 | ||
|
|
cca378b2e8 | ||
|
|
b5dfaff292 | ||
|
|
4f9a8180f9 | ||
|
|
14d2bf4ebb | ||
|
|
65291c68e9 | ||
|
|
719299d75b | ||
|
|
6e85d1d9b8 | ||
|
|
ea9d4d6818 | ||
|
|
0370a3b7f6 | ||
|
|
34bc20dfed | ||
|
|
40fe596e11 | ||
|
|
6a13b8a677 | ||
|
|
dde82f3d96 | ||
|
|
2565d4ed62 | ||
|
|
6a119254ad | ||
|
|
49eb5b54ed | ||
|
|
3cb053250b | ||
|
|
f5ccfabb2b | ||
|
|
fd3e96735c | ||
|
|
fb6752c143 | ||
|
|
c50c5bed81 | ||
|
|
e6d4e802dc | ||
|
|
e171624fbd | ||
|
|
c856e73397 | ||
|
|
9215d0f3f0 | ||
|
|
c4d647887f | ||
|
|
0abfca7554 | ||
|
|
d6e9e5c31d | ||
|
|
dc424f61a7 | ||
|
|
3629749653 | ||
|
|
9b02ceaab7 | ||
|
|
b564395e50 | ||
|
|
a515acb6f5 | ||
|
|
dc38715cf8 | ||
|
|
0b8665f148 | ||
|
|
5956c4db40 | ||
|
|
70f025c50e | ||
|
|
e9496affa2 | ||
|
|
86591cf1a6 | ||
|
|
e684cdad8c | ||
|
|
fabaae66c2 | ||
|
|
c780d52bcf | ||
|
|
fe73fe616d | ||
|
|
db0fc1382d | ||
|
|
e09f653155 | ||
|
|
21fece80da | ||
|
|
50e6991745 | ||
|
|
dc1ab3ffc3 | ||
|
|
6751fab987 | ||
|
|
aa3f7397d8 | ||
|
|
e791e0a9c8 | ||
|
|
4b4ba3f942 | ||
|
|
e75be92c37 | ||
|
|
b03a3cb36b | ||
|
|
10b904a13b | ||
|
|
42497ab60c | ||
|
|
ce4a4f0d4f | ||
|
|
6a9e389b7c | ||
|
|
cf6be6f596 | ||
|
|
7436ac508b | ||
|
|
50ef742c63 | ||
|
|
79878cfb85 | ||
|
|
2f4b68e043 | ||
|
|
67c020db9b | ||
|
|
c349aac7d2 | ||
|
|
28deb2237f | ||
|
|
08afe4d084 | ||
|
|
d8d4b38384 | ||
|
|
6cbcb9f358 | ||
|
|
871547b118 | ||
|
|
c15ee9af7e | ||
|
|
1f16eb446b | ||
|
|
d612ec9cee | ||
|
|
65f542f722 | ||
|
|
47f49b1e5d | ||
|
|
f1f798b7eb | ||
|
|
483c8641d8 | ||
|
|
96b7169125 | ||
|
|
2113da56fa | ||
|
|
0b26165a68 | ||
|
|
a7fab5809b | ||
|
|
25c5d1ab07 |
12
.babelrc
Normal file
12
.babelrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"plugins": ["lodash", "angularjs-annotate"],
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -53,6 +53,7 @@ plugins:
|
||||
mass_threshold: 80
|
||||
eslint:
|
||||
enabled: true
|
||||
channel: "eslint-5"
|
||||
config:
|
||||
config: .eslintrc.yml
|
||||
fixme:
|
||||
|
||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
test/
|
||||
546
.eslintrc.yml
546
.eslintrc.yml
@@ -1,287 +1,291 @@
|
||||
env:
|
||||
browser: true
|
||||
jquery: true
|
||||
node: true
|
||||
es6: true
|
||||
|
||||
globals:
|
||||
angular: true
|
||||
|
||||
# globals:
|
||||
# angular: true
|
||||
# $: true
|
||||
# _: true
|
||||
# moment: true
|
||||
# filesize: true
|
||||
# splitargs: true
|
||||
extends:
|
||||
- 'eslint:recommended'
|
||||
|
||||
# http://eslint.org/docs/rules/
|
||||
parserOptions:
|
||||
ecmaVersion: 2018
|
||||
sourceType: module
|
||||
ecmaFeatures:
|
||||
modules: true
|
||||
|
||||
# # http://eslint.org/docs/rules/
|
||||
rules:
|
||||
# Possible Errors
|
||||
no-await-in-loop: off
|
||||
no-cond-assign: error
|
||||
# # 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-empty: error
|
||||
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
|
||||
# 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-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: off
|
||||
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
|
||||
# # 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
|
||||
# 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
|
||||
# # 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
|
||||
# # 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
|
||||
# # 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
|
||||
# # 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
|
||||
# # 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
|
||||
|
||||
16
README.md
16
README.md
@@ -1,27 +1,20 @@
|
||||
|
||||
<p align="center">
|
||||
<img title="portainer" src='https://portainer.io/images/logo_alt.png' />
|
||||
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/assets/images/logo_alt.png?raw=true' />
|
||||
</p>
|
||||
|
||||
[](https://hub.docker.com/r/portainer/portainer/)
|
||||
[](http://microbadger.com/images/portainer/portainer "Image size")
|
||||
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
||||
[](https://semaphoreci.com/portainer/portainer)
|
||||
[](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
|
||||
[](https://codeclimate.com/github/portainer/portainer)
|
||||
[](https://portainer.io/slack/)
|
||||
[](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](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).
|
||||
|
||||
**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*.
|
||||
**_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*.
|
||||
|
||||
## Demo
|
||||
|
||||
<img src="https://portainer.io/images/screenshots/portainer.gif" width="77%"/>
|
||||
|
||||
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
|
||||
|
||||
Please note that the public demo cluster is **reset every 15min**.
|
||||
@@ -44,7 +37,6 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
|
||||
* Issues: https://github.com/portainer/portainer/issues
|
||||
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
|
||||
* Slack (chat): https://portainer.io/slack/
|
||||
* Gitter (chat): https://gitter.im/portainer/Lobby
|
||||
|
||||
## Reporting bugs and contributing
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
|
||||
// TarFileInBuffer will create a tar archive containing a single file named via fileName and using the content
|
||||
// specified in fileContent. Returns the archive as a byte array.
|
||||
func TarFileInBuffer(fileContent []byte, fileName string) ([]byte, error) {
|
||||
func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&buffer)
|
||||
|
||||
header := &tar.Header{
|
||||
Name: fileName,
|
||||
Mode: 0600,
|
||||
Mode: mode,
|
||||
Size: int64(len(fileContent)),
|
||||
}
|
||||
|
||||
|
||||
48
api/archive/zip.go
Normal file
48
api/archive/zip.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
|
||||
func UnzipArchive(archiveData []byte, dest string) error {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(archiveData), int64(len(archiveData)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, zipFile := range zipReader.File {
|
||||
|
||||
f, err := zipFile.Open()
|
||||
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
|
||||
}
|
||||
@@ -7,13 +7,14 @@ import (
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/deploykey"
|
||||
"github.com/portainer/portainer/bolt/dockerhub"
|
||||
"github.com/portainer/portainer/bolt/endpoint"
|
||||
"github.com/portainer/portainer/bolt/endpointgroup"
|
||||
"github.com/portainer/portainer/bolt/extension"
|
||||
"github.com/portainer/portainer/bolt/migrator"
|
||||
"github.com/portainer/portainer/bolt/registry"
|
||||
"github.com/portainer/portainer/bolt/resourcecontrol"
|
||||
"github.com/portainer/portainer/bolt/schedule"
|
||||
"github.com/portainer/portainer/bolt/settings"
|
||||
"github.com/portainer/portainer/bolt/stack"
|
||||
"github.com/portainer/portainer/bolt/tag"
|
||||
@@ -39,6 +40,7 @@ type Store struct {
|
||||
DockerHubService *dockerhub.Service
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
ExtensionService *extension.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
SettingsService *settings.Service
|
||||
@@ -50,7 +52,7 @@ type Store struct {
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
DeploykeyService *deploykey.Service
|
||||
ScheduleService *schedule.Service
|
||||
}
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
@@ -137,9 +139,11 @@ func (store *Store) MigrateData() error {
|
||||
DatabaseVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
StackService: store.StackService,
|
||||
TemplateService: store.TemplateService,
|
||||
UserService: store.UserService,
|
||||
VersionService: store.VersionService,
|
||||
FileService: store.fileService,
|
||||
@@ -176,6 +180,12 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.EndpointService = endpointService
|
||||
|
||||
extensionService, err := extension.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.ExtensionService = extensionService
|
||||
|
||||
registryService, err := registry.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -206,12 +216,6 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.TagService = tagService
|
||||
|
||||
deploykeyService, err := deploykey.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.DeploykeyService = deploykeyService
|
||||
|
||||
teammembershipService, err := teammembership.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -248,5 +252,11 @@ func (store *Store) initServices() error {
|
||||
}
|
||||
store.WebhookService = webhookService
|
||||
|
||||
scheduleService, err := schedule.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.ScheduleService = scheduleService
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package deploykey
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "deploykeys"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Keys return an array containing all the keys.
|
||||
func (service *Service) Deploykeys() ([]portainer.Deploykey, error) {
|
||||
var deploykeys = make([]portainer.Deploykey, 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 deploykey portainer.Deploykey
|
||||
err := internal.UnmarshalObject(v, &deploykey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
deploykeys = append(deploykeys, deploykey)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return deploykeys, err
|
||||
}
|
||||
|
||||
// CreateKey creates a new key.
|
||||
func (service *Service) CreateDeploykey(deploykey *portainer.Deploykey) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
deploykey.ID = portainer.DeploykeyID(id)
|
||||
|
||||
data, err := internal.MarshalObject(deploykey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(deploykey.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteDeploykey deletes a key.
|
||||
func (service *Service) DeleteDeploykey(ID portainer.DeploykeyID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
86
api/bolt/extension/extension.go
Normal file
86
api/bolt/extension/extension.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "extension"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Extension returns a extension by ID
|
||||
func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extension, error) {
|
||||
var extension portainer.Extension
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &extension, nil
|
||||
}
|
||||
|
||||
// Extensions return an array containing all the extensions.
|
||||
func (service *Service) Extensions() ([]portainer.Extension, error) {
|
||||
var extensions = make([]portainer.Extension, 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 extension portainer.Extension
|
||||
err := internal.UnmarshalObject(v, &extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
extensions = append(extensions, extension)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return extensions, err
|
||||
}
|
||||
|
||||
// Persist persists a extension inside the database.
|
||||
func (service *Service) Persist(extension *portainer.Extension) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
data, err := internal.MarshalObject(extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(extension.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteExtension deletes a Extension.
|
||||
func (service *Service) DeleteExtension(ID portainer.ExtensionID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
35
api/bolt/migrator/migrate_dbversion14.go
Normal file
35
api/bolt/migrator/migrate_dbversion14.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion15() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.EnableHostManagementFeatures = false
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func (m *Migrator) updateTemplatesToVersion15() error {
|
||||
legacyTemplates, err := m.templateService.Templates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, template := range legacyTemplates {
|
||||
template.Logo = strings.Replace(template.Logo, "https://portainer.io/images", portainer.AssetsServerURL, -1)
|
||||
|
||||
err = m.templateService.UpdateTemplate(template.ID, &template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
14
api/bolt/migrator/migrate_dbversion15.go
Normal file
14
api/bolt/migrator/migrate_dbversion15.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion16() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if legacySettings.SnapshotInterval == "" {
|
||||
legacySettings.SnapshotInterval = "5m"
|
||||
}
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
19
api/bolt/migrator/migrate_dbversion16.go
Normal file
19
api/bolt/migrator/migrate_dbversion16.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package migrator
|
||||
|
||||
func (m *Migrator) updateExtensionsToDBVersion17() error {
|
||||
legacyExtensions, err := m.extensionService.Extensions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, extension := range legacyExtensions {
|
||||
extension.License.Valid = true
|
||||
|
||||
err = m.extensionService.Persist(&extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -5,9 +5,11 @@ import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/endpoint"
|
||||
"github.com/portainer/portainer/bolt/endpointgroup"
|
||||
"github.com/portainer/portainer/bolt/extension"
|
||||
"github.com/portainer/portainer/bolt/resourcecontrol"
|
||||
"github.com/portainer/portainer/bolt/settings"
|
||||
"github.com/portainer/portainer/bolt/stack"
|
||||
"github.com/portainer/portainer/bolt/template"
|
||||
"github.com/portainer/portainer/bolt/user"
|
||||
"github.com/portainer/portainer/bolt/version"
|
||||
)
|
||||
@@ -19,9 +21,11 @@ type (
|
||||
db *bolt.DB
|
||||
endpointGroupService *endpointgroup.Service
|
||||
endpointService *endpoint.Service
|
||||
extensionService *extension.Service
|
||||
resourceControlService *resourcecontrol.Service
|
||||
settingsService *settings.Service
|
||||
stackService *stack.Service
|
||||
templateService *template.Service
|
||||
userService *user.Service
|
||||
versionService *version.Service
|
||||
fileService portainer.FileService
|
||||
@@ -33,9 +37,11 @@ type (
|
||||
DatabaseVersion int
|
||||
EndpointGroupService *endpointgroup.Service
|
||||
EndpointService *endpoint.Service
|
||||
ExtensionService *extension.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
SettingsService *settings.Service
|
||||
StackService *stack.Service
|
||||
TemplateService *template.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
FileService portainer.FileService
|
||||
@@ -49,8 +55,10 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||
currentDBVersion: parameters.DatabaseVersion,
|
||||
endpointGroupService: parameters.EndpointGroupService,
|
||||
endpointService: parameters.EndpointService,
|
||||
extensionService: parameters.ExtensionService,
|
||||
resourceControlService: parameters.ResourceControlService,
|
||||
settingsService: parameters.SettingsService,
|
||||
templateService: parameters.TemplateService,
|
||||
stackService: parameters.StackService,
|
||||
userService: parameters.UserService,
|
||||
versionService: parameters.VersionService,
|
||||
@@ -186,5 +194,33 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.0
|
||||
if m.currentDBVersion < 15 {
|
||||
err := m.updateSettingsToDBVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateTemplatesToVersion15()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentDBVersion < 16 {
|
||||
err := m.updateSettingsToDBVersion16()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.20.1
|
||||
if m.currentDBVersion < 17 {
|
||||
err := m.updateExtensionsToDBVersion17()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
129
api/bolt/schedule/schedule.go
Normal file
129
api/bolt/schedule/schedule.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "schedules"
|
||||
)
|
||||
|
||||
// Service represents a service for managing schedule 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
|
||||
}
|
||||
|
||||
// Schedule returns a schedule by ID.
|
||||
func (service *Service) Schedule(ID portainer.ScheduleID) (*portainer.Schedule, error) {
|
||||
var schedule portainer.Schedule
|
||||
identifier := internal.Itob(int(ID))
|
||||
|
||||
err := internal.GetObject(service.db, BucketName, identifier, &schedule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &schedule, nil
|
||||
}
|
||||
|
||||
// UpdateSchedule updates a schedule.
|
||||
func (service *Service) UpdateSchedule(ID portainer.ScheduleID, schedule *portainer.Schedule) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, schedule)
|
||||
}
|
||||
|
||||
// DeleteSchedule deletes a schedule.
|
||||
func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
return internal.DeleteObject(service.db, BucketName, identifier)
|
||||
}
|
||||
|
||||
// Schedules return a array containing all the schedules.
|
||||
func (service *Service) Schedules() ([]portainer.Schedule, error) {
|
||||
var schedules = make([]portainer.Schedule, 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 schedule portainer.Schedule
|
||||
err := internal.UnmarshalObject(v, &schedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
schedules = append(schedules, schedule)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return schedules, err
|
||||
}
|
||||
|
||||
// SchedulesByJobType return a array containing all the schedules
|
||||
// with the specified JobType.
|
||||
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
|
||||
var schedules = make([]portainer.Schedule, 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 schedule portainer.Schedule
|
||||
err := internal.UnmarshalObject(v, &schedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if schedule.JobType == jobType {
|
||||
schedules = append(schedules, schedule)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return schedules, err
|
||||
}
|
||||
|
||||
// CreateSchedule assign an ID to a new schedule and saves it.
|
||||
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
// We manually manage sequences for schedules
|
||||
err := bucket.SetSequence(uint64(schedule.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := internal.MarshalObject(schedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(internal.Itob(int(schedule.ID)), data)
|
||||
})
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a schedule.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return internal.GetNextIdentifier(service.db, BucketName)
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package main // import "github.com/portainer/portainer"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt"
|
||||
@@ -87,18 +89,13 @@ func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||
}
|
||||
|
||||
func initDigitalSignatureService() portainer.DigitalSignatureService {
|
||||
return &crypto.ECDSAService{}
|
||||
return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
|
||||
}
|
||||
|
||||
func initCryptoService() portainer.CryptoService {
|
||||
return &crypto.Service{}
|
||||
}
|
||||
|
||||
func initDigitalDeploykeyService() portainer.DigitalDeploykeyService {
|
||||
return &crypto.ECDSAService{}
|
||||
}
|
||||
|
||||
|
||||
func initLDAPService() portainer.LDAPService {
|
||||
return &ldap.Service{}
|
||||
}
|
||||
@@ -115,25 +112,110 @@ func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter
|
||||
return docker.NewSnapshotter(clientFactory)
|
||||
}
|
||||
|
||||
func initJobScheduler(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter, flags *portainer.CLIFlags) (portainer.JobScheduler, error) {
|
||||
jobScheduler := cron.NewJobScheduler(endpointService, snapshotter)
|
||||
func initJobScheduler() portainer.JobScheduler {
|
||||
return cron.NewJobScheduler()
|
||||
}
|
||||
|
||||
if *flags.ExternalEndpoints != "" {
|
||||
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
|
||||
err := jobScheduler.ScheduleEndpointSyncJob(*flags.ExternalEndpoints, *flags.SyncInterval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter portainer.Snapshotter, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, settingsService portainer.SettingsService) error {
|
||||
settings, err := settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schedules, err := scheduleService.SchedulesByJobType(portainer.SnapshotJobType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var snapshotSchedule *portainer.Schedule
|
||||
if len(schedules) == 0 {
|
||||
snapshotJob := &portainer.SnapshotJob{}
|
||||
snapshotSchedule = &portainer.Schedule{
|
||||
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
|
||||
Name: "system_snapshot",
|
||||
CronExpression: "@every " + settings.SnapshotInterval,
|
||||
Recurring: true,
|
||||
JobType: portainer.SnapshotJobType,
|
||||
SnapshotJob: snapshotJob,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
} else {
|
||||
snapshotSchedule = &schedules[0]
|
||||
}
|
||||
|
||||
snapshotJobContext := cron.NewSnapshotJobContext(endpointService, snapshotter)
|
||||
snapshotJobRunner := cron.NewSnapshotJobRunner(snapshotSchedule, snapshotJobContext)
|
||||
|
||||
err = jobScheduler.ScheduleJob(snapshotJobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(schedules) == 0 {
|
||||
return scheduleService.CreateSchedule(snapshotSchedule)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, flags *portainer.CLIFlags) error {
|
||||
if *flags.ExternalEndpoints == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
|
||||
|
||||
schedules, err := scheduleService.SchedulesByJobType(portainer.EndpointSyncJobType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(schedules) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
endpointSyncJob := &portainer.EndpointSyncJob{}
|
||||
|
||||
endpointSyncSchedule := &portainer.Schedule{
|
||||
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
|
||||
Name: "system_endpointsync",
|
||||
CronExpression: "@every " + *flags.SyncInterval,
|
||||
Recurring: true,
|
||||
JobType: portainer.EndpointSyncJobType,
|
||||
EndpointSyncJob: endpointSyncJob,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
|
||||
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endpointSyncSchedule, endpointSyncJobContext)
|
||||
|
||||
err = jobScheduler.ScheduleJob(endpointSyncJobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scheduleService.CreateSchedule(endpointSyncSchedule)
|
||||
}
|
||||
|
||||
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error {
|
||||
schedules, err := scheduleService.Schedules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, schedule := range schedules {
|
||||
|
||||
if schedule.JobType == portainer.ScriptExecutionJobType {
|
||||
jobContext := cron.NewScriptExecutionJobContext(jobService, endpointService, fileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(&schedule, jobContext)
|
||||
|
||||
err = jobScheduler.ScheduleJob(jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *flags.Snapshot {
|
||||
err := jobScheduler.ScheduleSnapshotJob(*flags.SnapshotInterval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return jobScheduler, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *portainer.Status {
|
||||
@@ -178,8 +260,10 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
EnableHostManagementFeatures: false,
|
||||
SnapshotInterval: *flags.SnapshotInterval,
|
||||
}
|
||||
|
||||
@@ -388,6 +472,46 @@ func initEndpoint(flags *portainer.CLIFlags, endpointService portainer.EndpointS
|
||||
return createUnsecuredEndpoint(*flags.EndpointURL, endpointService, snapshotter)
|
||||
}
|
||||
|
||||
func initJobService(dockerClientFactory *docker.ClientFactory) portainer.JobService {
|
||||
return docker.NewJobService(dockerClientFactory)
|
||||
}
|
||||
|
||||
func initExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) (portainer.ExtensionManager, error) {
|
||||
extensionManager := exec.NewExtensionManager(fileService, extensionService)
|
||||
|
||||
extensions, err := extensionService.Extensions()
|
||||
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
|
||||
}
|
||||
|
||||
func terminateIfNoAdminCreated(userService portainer.UserService) {
|
||||
timer1 := time.NewTimer(5 * time.Minute)
|
||||
<-timer1.C
|
||||
|
||||
users, err := userService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
log.Fatal("No administrator account was created after 5 min. Shutting down the Portainer instance for security reasons.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flags := initCLI()
|
||||
|
||||
@@ -406,24 +530,22 @@ func main() {
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
digitalDeploykeyService := initDigitalDeploykeyService()
|
||||
|
||||
err := initKeyPair(fileService, digitalSignatureService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
extensionManager, err := initExtensionManager(fileService, store.ExtensionService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
clientFactory := initClientFactory(digitalSignatureService)
|
||||
|
||||
jobService := initJobService(clientFactory)
|
||||
|
||||
snapshotter := initSnapshotter(clientFactory)
|
||||
|
||||
jobScheduler, err := initJobScheduler(store.EndpointService, snapshotter, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
jobScheduler.Start()
|
||||
|
||||
endpointManagement := true
|
||||
if *flags.ExternalEndpoints != "" {
|
||||
endpointManagement = false
|
||||
@@ -446,6 +568,27 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
jobScheduler := initJobScheduler()
|
||||
|
||||
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = loadEndpointSyncSystemSchedule(jobScheduler, store.ScheduleService, store.EndpointService, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if *flags.Snapshot {
|
||||
err = loadSnapshotSystemSchedule(jobScheduler, snapshotter, store.ScheduleService, store.EndpointService, store.SettingsService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
jobScheduler.Start()
|
||||
|
||||
err = initDockerHub(store.DockerHubService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -494,6 +637,10 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if !*flags.NoAuth {
|
||||
go terminateIfNoAdminCreated(store.UserService)
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
@@ -505,18 +652,19 @@ func main() {
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
DeploykeyService: store.DeploykeyService,
|
||||
DigitalDeploykeyService: digitalDeploykeyService,
|
||||
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,
|
||||
@@ -529,6 +677,7 @@ func main() {
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
DockerClientFactory: clientFactory,
|
||||
JobService: jobService,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type (
|
||||
endpointSnapshotJob struct {
|
||||
endpointService portainer.EndpointService
|
||||
snapshotter portainer.Snapshotter
|
||||
}
|
||||
)
|
||||
|
||||
func newEndpointSnapshotJob(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) endpointSnapshotJob {
|
||||
return endpointSnapshotJob{
|
||||
endpointService: endpointService,
|
||||
snapshotter: snapshotter,
|
||||
}
|
||||
}
|
||||
|
||||
func (job endpointSnapshotJob) Snapshot() error {
|
||||
|
||||
endpoints, err := job.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
snapshot, err := job.snapshotter.CreateSnapshot(&endpoint)
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
if err != nil {
|
||||
log.Printf("cron error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = job.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (job endpointSnapshotJob) Run() {
|
||||
err := job.Snapshot()
|
||||
if err != nil {
|
||||
log.Printf("cron error: snapshot job error (err=%s)\n", err)
|
||||
}
|
||||
}
|
||||
@@ -9,44 +9,93 @@ import (
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type (
|
||||
endpointSyncJob struct {
|
||||
endpointService portainer.EndpointService
|
||||
endpointFilePath string
|
||||
}
|
||||
// EndpointSyncJobRunner is used to run a EndpointSyncJob
|
||||
type EndpointSyncJobRunner struct {
|
||||
schedule *portainer.Schedule
|
||||
context *EndpointSyncJobContext
|
||||
}
|
||||
|
||||
synchronization struct {
|
||||
endpointsToCreate []*portainer.Endpoint
|
||||
endpointsToUpdate []*portainer.Endpoint
|
||||
endpointsToDelete []*portainer.Endpoint
|
||||
}
|
||||
// EndpointSyncJobContext represents the context of execution of a EndpointSyncJob
|
||||
type EndpointSyncJobContext struct {
|
||||
endpointService portainer.EndpointService
|
||||
endpointFilePath string
|
||||
}
|
||||
|
||||
fileEndpoint struct {
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
|
||||
TLSCACert string `json:"TLSCACert,omitempty"`
|
||||
TLSCert string `json:"TLSCert,omitempty"`
|
||||
TLSKey string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrEmptyEndpointArray is an error raised when the external endpoint source array is empty.
|
||||
ErrEmptyEndpointArray = portainer.Error("External endpoint source is empty")
|
||||
)
|
||||
|
||||
func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob {
|
||||
return endpointSyncJob{
|
||||
// NewEndpointSyncJobContext returns a new context that can be used to execute a EndpointSyncJob
|
||||
func NewEndpointSyncJobContext(endpointService portainer.EndpointService, endpointFilePath string) *EndpointSyncJobContext {
|
||||
return &EndpointSyncJobContext{
|
||||
endpointService: endpointService,
|
||||
endpointFilePath: endpointFilePath,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEndpointSyncJobRunner returns a new runner that can be scheduled
|
||||
func NewEndpointSyncJobRunner(schedule *portainer.Schedule, context *EndpointSyncJobContext) *EndpointSyncJobRunner {
|
||||
return &EndpointSyncJobRunner{
|
||||
schedule: schedule,
|
||||
context: context,
|
||||
}
|
||||
}
|
||||
|
||||
type synchronization struct {
|
||||
endpointsToCreate []*portainer.Endpoint
|
||||
endpointsToUpdate []*portainer.Endpoint
|
||||
endpointsToDelete []*portainer.Endpoint
|
||||
}
|
||||
|
||||
type fileEndpoint struct {
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
|
||||
TLSCACert string `json:"TLSCACert,omitempty"`
|
||||
TLSCert string `json:"TLSCert,omitempty"`
|
||||
TLSKey string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
|
||||
// GetSchedule returns the schedule associated to the runner
|
||||
func (runner *EndpointSyncJobRunner) GetSchedule() *portainer.Schedule {
|
||||
return runner.schedule
|
||||
}
|
||||
|
||||
// Run triggers the execution of the endpoint synchronization process.
|
||||
func (runner *EndpointSyncJobRunner) Run() {
|
||||
data, err := ioutil.ReadFile(runner.context.endpointFilePath)
|
||||
if endpointSyncError(err) {
|
||||
return
|
||||
}
|
||||
|
||||
var fileEndpoints []fileEndpoint
|
||||
err = json.Unmarshal(data, &fileEndpoints)
|
||||
if endpointSyncError(err) {
|
||||
return
|
||||
}
|
||||
|
||||
if len(fileEndpoints) == 0 {
|
||||
log.Println("background job error (endpoint synchronization). External endpoint source is empty")
|
||||
return
|
||||
}
|
||||
|
||||
storedEndpoints, err := runner.context.endpointService.Endpoints()
|
||||
if endpointSyncError(err) {
|
||||
return
|
||||
}
|
||||
|
||||
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
|
||||
|
||||
sync := prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
||||
if sync.requireSync() {
|
||||
err = runner.context.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
||||
if endpointSyncError(err) {
|
||||
return
|
||||
}
|
||||
log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
|
||||
}
|
||||
}
|
||||
|
||||
func endpointSyncError(err error) bool {
|
||||
if err != nil {
|
||||
log.Printf("cron error: synchronization job error (err=%s)\n", err)
|
||||
log.Printf("background job error (endpoint synchronization). Unable to synchronize endpoints (err=%s)\n", err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -126,8 +175,7 @@ func (sync synchronization) requireSync() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// TMP: endpointSyncJob method to access logger, should be generic
|
||||
func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
|
||||
func prepareSyncData(storedEndpoints, fileEndpoints []portainer.Endpoint) *synchronization {
|
||||
endpointsToCreate := make([]*portainer.Endpoint, 0)
|
||||
endpointsToUpdate := make([]*portainer.Endpoint, 0)
|
||||
endpointsToDelete := make([]*portainer.Endpoint, 0)
|
||||
@@ -164,43 +212,3 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
|
||||
endpointsToDelete: endpointsToDelete,
|
||||
}
|
||||
}
|
||||
|
||||
func (job endpointSyncJob) Sync() error {
|
||||
data, err := ioutil.ReadFile(job.endpointFilePath)
|
||||
if endpointSyncError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileEndpoints []fileEndpoint
|
||||
err = json.Unmarshal(data, &fileEndpoints)
|
||||
if endpointSyncError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(fileEndpoints) == 0 {
|
||||
return ErrEmptyEndpointArray
|
||||
}
|
||||
|
||||
storedEndpoints, err := job.endpointService.Endpoints()
|
||||
if endpointSyncError(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
|
||||
|
||||
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
||||
if sync.requireSync() {
|
||||
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
||||
if endpointSyncError(err) {
|
||||
return err
|
||||
}
|
||||
log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (job endpointSyncJob) Run() {
|
||||
log.Println("cron: synchronization job started")
|
||||
err := job.Sync()
|
||||
endpointSyncError(err)
|
||||
}
|
||||
|
||||
96
api/cron/job_script_execution.go
Normal file
96
api/cron/job_script_execution.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// ScriptExecutionJobRunner is used to run a ScriptExecutionJob
|
||||
type ScriptExecutionJobRunner struct {
|
||||
schedule *portainer.Schedule
|
||||
context *ScriptExecutionJobContext
|
||||
executedOnce bool
|
||||
}
|
||||
|
||||
// ScriptExecutionJobContext represents the context of execution of a ScriptExecutionJob
|
||||
type ScriptExecutionJobContext struct {
|
||||
jobService portainer.JobService
|
||||
endpointService portainer.EndpointService
|
||||
fileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewScriptExecutionJobContext returns a new context that can be used to execute a ScriptExecutionJob
|
||||
func NewScriptExecutionJobContext(jobService portainer.JobService, endpointService portainer.EndpointService, fileService portainer.FileService) *ScriptExecutionJobContext {
|
||||
return &ScriptExecutionJobContext{
|
||||
jobService: jobService,
|
||||
endpointService: endpointService,
|
||||
fileService: fileService,
|
||||
}
|
||||
}
|
||||
|
||||
// NewScriptExecutionJobRunner returns a new runner that can be scheduled
|
||||
func NewScriptExecutionJobRunner(schedule *portainer.Schedule, context *ScriptExecutionJobContext) *ScriptExecutionJobRunner {
|
||||
return &ScriptExecutionJobRunner{
|
||||
schedule: schedule,
|
||||
context: context,
|
||||
executedOnce: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Run triggers the execution of the job.
|
||||
// It will iterate through all the endpoints specified in the context to
|
||||
// execute the script associated to the job.
|
||||
func (runner *ScriptExecutionJobRunner) Run() {
|
||||
if !runner.schedule.Recurring && runner.executedOnce {
|
||||
return
|
||||
}
|
||||
runner.executedOnce = true
|
||||
|
||||
scriptFile, err := runner.context.fileService.GetFileContent(runner.schedule.ScriptExecutionJob.ScriptPath)
|
||||
if err != nil {
|
||||
log.Printf("scheduled job error (script execution). Unable to retrieve script file (err=%s)\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
targets := make([]*portainer.Endpoint, 0)
|
||||
for _, endpointID := range runner.schedule.ScriptExecutionJob.Endpoints {
|
||||
endpoint, err := runner.context.endpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
log.Printf("scheduled job error (script execution). Unable to retrieve information about endpoint (id=%d) (err=%s)\n", endpointID, err)
|
||||
return
|
||||
}
|
||||
|
||||
targets = append(targets, endpoint)
|
||||
}
|
||||
|
||||
runner.executeAndRetry(targets, scriptFile, 0)
|
||||
}
|
||||
|
||||
func (runner *ScriptExecutionJobRunner) executeAndRetry(endpoints []*portainer.Endpoint, script []byte, retryCount int) {
|
||||
retryTargets := make([]*portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err := runner.context.jobService.ExecuteScript(endpoint, "", runner.schedule.ScriptExecutionJob.Image, script, runner.schedule)
|
||||
if err == portainer.ErrUnableToPingEndpoint {
|
||||
retryTargets = append(retryTargets, endpoint)
|
||||
} else if err != nil {
|
||||
log.Printf("scheduled job error (script execution). Unable to execute script (endpoint=%s) (err=%s)\n", endpoint.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
retryCount++
|
||||
if retryCount >= runner.schedule.ScriptExecutionJob.RetryCount {
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(runner.schedule.ScriptExecutionJob.RetryInterval) * time.Second)
|
||||
|
||||
runner.executeAndRetry(retryTargets, script, retryCount)
|
||||
}
|
||||
|
||||
// GetSchedule returns the schedule associated to the runner
|
||||
func (runner *ScriptExecutionJobRunner) GetSchedule() *portainer.Schedule {
|
||||
return runner.schedule
|
||||
}
|
||||
85
api/cron/job_snapshot.go
Normal file
85
api/cron/job_snapshot.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// SnapshotJobRunner is used to run a SnapshotJob
|
||||
type SnapshotJobRunner struct {
|
||||
schedule *portainer.Schedule
|
||||
context *SnapshotJobContext
|
||||
}
|
||||
|
||||
// SnapshotJobContext represents the context of execution of a SnapshotJob
|
||||
type SnapshotJobContext struct {
|
||||
endpointService portainer.EndpointService
|
||||
snapshotter portainer.Snapshotter
|
||||
}
|
||||
|
||||
// NewSnapshotJobContext returns a new context that can be used to execute a SnapshotJob
|
||||
func NewSnapshotJobContext(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) *SnapshotJobContext {
|
||||
return &SnapshotJobContext{
|
||||
endpointService: endpointService,
|
||||
snapshotter: snapshotter,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSnapshotJobRunner returns a new runner that can be scheduled
|
||||
func NewSnapshotJobRunner(schedule *portainer.Schedule, context *SnapshotJobContext) *SnapshotJobRunner {
|
||||
return &SnapshotJobRunner{
|
||||
schedule: schedule,
|
||||
context: context,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSchedule returns the schedule associated to the runner
|
||||
func (runner *SnapshotJobRunner) GetSchedule() *portainer.Schedule {
|
||||
return runner.schedule
|
||||
}
|
||||
|
||||
// Run triggers the execution of the schedule.
|
||||
// It will iterate through all the endpoints available in the database to
|
||||
// create a snapshot of each one of them.
|
||||
// As a snapshot can be a long process, to avoid any concurrency issue we
|
||||
// retrieve the latest version of the endpoint right after a snapshot.
|
||||
func (runner *SnapshotJobRunner) Run() {
|
||||
go func() {
|
||||
endpoints, err := runner.context.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to retrieve endpoint list (err=%s)\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
snapshot, snapshotError := runner.context.snapshotter.CreateSnapshot(&endpoint)
|
||||
|
||||
latestEndpointReference, err := runner.context.endpointService.Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
latestEndpointReference.Status = portainer.EndpointStatusUp
|
||||
if snapshotError != nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError)
|
||||
latestEndpointReference.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = runner.context.endpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to update endpoint (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,76 +1,109 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
// JobScheduler represents a service for managing crons.
|
||||
// JobScheduler represents a service for managing crons
|
||||
type JobScheduler struct {
|
||||
cron *cron.Cron
|
||||
endpointService portainer.EndpointService
|
||||
snapshotter portainer.Snapshotter
|
||||
|
||||
endpointFilePath string
|
||||
endpointSyncInterval string
|
||||
cron *cron.Cron
|
||||
}
|
||||
|
||||
// NewJobScheduler initializes a new service.
|
||||
func NewJobScheduler(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) *JobScheduler {
|
||||
// NewJobScheduler initializes a new service
|
||||
func NewJobScheduler() *JobScheduler {
|
||||
return &JobScheduler{
|
||||
cron: cron.New(),
|
||||
endpointService: endpointService,
|
||||
snapshotter: snapshotter,
|
||||
cron: cron.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleEndpointSyncJob schedules a cron job to synchronize the endpoints from a file
|
||||
func (scheduler *JobScheduler) ScheduleEndpointSyncJob(endpointFilePath string, interval string) error {
|
||||
|
||||
scheduler.endpointFilePath = endpointFilePath
|
||||
scheduler.endpointSyncInterval = interval
|
||||
|
||||
job := newEndpointSyncJob(endpointFilePath, scheduler.endpointService)
|
||||
|
||||
err := job.Sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scheduler.cron.AddJob("@every "+interval, job)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ScheduleSnapshotJob schedules a cron job to create endpoint snapshots
|
||||
func (scheduler *JobScheduler) ScheduleSnapshotJob(interval string) error {
|
||||
job := newEndpointSnapshotJob(scheduler.endpointService, scheduler.snapshotter)
|
||||
go job.Snapshot()
|
||||
// UpdateSystemJobSchedule updates the first occurence of the specified
|
||||
// scheduled job based on the specified job type.
|
||||
// It does so by re-creating a new cron
|
||||
// and adding all the existing jobs. It will then re-schedule the new job
|
||||
// with the update cron expression passed in parameter.
|
||||
// NOTE: the cron library do not support updating schedules directly
|
||||
// hence the work-around
|
||||
func (scheduler *JobScheduler) UpdateSystemJobSchedule(jobType portainer.JobType, newCronExpression string) error {
|
||||
cronEntries := scheduler.cron.Entries()
|
||||
newCron := cron.New()
|
||||
|
||||
return scheduler.cron.AddJob("@every "+interval, job)
|
||||
}
|
||||
|
||||
// UpdateSnapshotJob will update the schedules to match the new snapshot interval
|
||||
func (scheduler *JobScheduler) UpdateSnapshotJob(interval string) {
|
||||
// TODO: the cron library do not support removing/updating schedules.
|
||||
// As a work-around we need to re-create the cron and reschedule the jobs.
|
||||
// We should update the library.
|
||||
jobs := scheduler.cron.Entries()
|
||||
scheduler.cron.Stop()
|
||||
|
||||
scheduler.cron = cron.New()
|
||||
|
||||
for _, job := range jobs {
|
||||
switch job.Job.(type) {
|
||||
case endpointSnapshotJob:
|
||||
scheduler.ScheduleSnapshotJob(interval)
|
||||
case endpointSyncJob:
|
||||
scheduler.ScheduleEndpointSyncJob(scheduler.endpointFilePath, scheduler.endpointSyncInterval)
|
||||
default:
|
||||
log.Println("Unsupported job")
|
||||
for _, entry := range cronEntries {
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == jobType {
|
||||
err := newCron.AddJob(newCronExpression, entry.Job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
newCron.Schedule(entry.Schedule, entry.Job)
|
||||
}
|
||||
|
||||
scheduler.cron.Stop()
|
||||
scheduler.cron = newCron
|
||||
scheduler.cron.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateJobSchedule updates a specific scheduled job by re-creating a new cron
|
||||
// and adding all the existing jobs. It will then re-schedule the new job
|
||||
// via the specified JobRunner parameter.
|
||||
// NOTE: the cron library do not support updating schedules directly
|
||||
// hence the work-around
|
||||
func (scheduler *JobScheduler) UpdateJobSchedule(runner portainer.JobRunner) error {
|
||||
cronEntries := scheduler.cron.Entries()
|
||||
newCron := cron.New()
|
||||
|
||||
for _, entry := range cronEntries {
|
||||
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().ID == runner.GetSchedule().ID {
|
||||
|
||||
var jobRunner cron.Job = runner
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().JobType == portainer.SnapshotJobType {
|
||||
jobRunner = entry.Job
|
||||
}
|
||||
|
||||
err := newCron.AddJob(runner.GetSchedule().CronExpression, jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
newCron.Schedule(entry.Schedule, entry.Job)
|
||||
}
|
||||
|
||||
scheduler.cron.Stop()
|
||||
scheduler.cron = newCron
|
||||
scheduler.cron.Start()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnscheduleJob remove a scheduled job by re-creating a new cron
|
||||
// and adding all the existing jobs except for the one specified via scheduleID.
|
||||
// NOTE: the cron library do not support removing schedules directly
|
||||
// hence the work-around
|
||||
func (scheduler *JobScheduler) UnscheduleJob(scheduleID portainer.ScheduleID) {
|
||||
cronEntries := scheduler.cron.Entries()
|
||||
newCron := cron.New()
|
||||
|
||||
for _, entry := range cronEntries {
|
||||
|
||||
if entry.Job.(portainer.JobRunner).GetSchedule().ID == scheduleID {
|
||||
continue
|
||||
}
|
||||
|
||||
newCron.Schedule(entry.Schedule, entry.Job)
|
||||
}
|
||||
|
||||
scheduler.cron.Stop()
|
||||
scheduler.cron = newCron
|
||||
scheduler.cron.Start()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// These constants from [PROTOCOL.certkeys] represent the algorithm names
|
||||
// for certificate types supported by this package.
|
||||
const (
|
||||
CertAlgoRSAv01 = "ssh-rsa-cert-v01@openssh.com"
|
||||
CertAlgoDSAv01 = "ssh-dss-cert-v01@openssh.com"
|
||||
CertAlgoECDSA256v01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
|
||||
CertAlgoECDSA384v01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
|
||||
CertAlgoECDSA521v01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
|
||||
)
|
||||
|
||||
// Certificate types are used to specify whether a certificate is for identification
|
||||
// of a user or a host. Current identities are defined in [PROTOCOL.certkeys].
|
||||
const (
|
||||
UserCert = 1
|
||||
HostCert = 2
|
||||
)
|
||||
|
||||
type signature struct {
|
||||
Format string
|
||||
Blob []byte
|
||||
}
|
||||
|
||||
type tuple struct {
|
||||
Name string
|
||||
Data string
|
||||
}
|
||||
|
||||
const (
|
||||
maxUint64 = 1<<64 - 1
|
||||
maxInt64 = 1<<63 - 1
|
||||
)
|
||||
|
||||
// CertTime represents an unsigned 64-bit time value in seconds starting from
|
||||
// UNIX epoch. We use CertTime instead of time.Time in order to properly handle
|
||||
// the "infinite" time value ^0, which would become negative when expressed as
|
||||
// an int64.
|
||||
type CertTime uint64
|
||||
|
||||
func (ct CertTime) Time() time.Time {
|
||||
if ct > maxInt64 {
|
||||
return time.Unix(maxInt64, 0)
|
||||
}
|
||||
return time.Unix(int64(ct), 0)
|
||||
}
|
||||
|
||||
func (ct CertTime) IsInfinite() bool {
|
||||
return ct == maxUint64
|
||||
}
|
||||
|
||||
// An OpenSSHCertV01 represents an OpenSSH certificate as defined in
|
||||
// [PROTOCOL.certkeys]?rev=1.8.
|
||||
type OpenSSHCertV01 struct {
|
||||
Nonce []byte
|
||||
Key PublicKey
|
||||
Serial uint64
|
||||
Type uint32
|
||||
KeyId string
|
||||
ValidPrincipals []string
|
||||
ValidAfter, ValidBefore CertTime
|
||||
CriticalOptions []tuple
|
||||
Extensions []tuple
|
||||
Reserved []byte
|
||||
SignatureKey PublicKey
|
||||
Signature *signature
|
||||
}
|
||||
|
||||
// validateOpenSSHCertV01Signature uses the cert's SignatureKey to verify that
|
||||
// the cert's Signature.Blob is the result of signing the cert bytes starting
|
||||
// from the algorithm string and going up to and including the SignatureKey.
|
||||
func validateOpenSSHCertV01Signature(cert *OpenSSHCertV01) bool {
|
||||
return cert.SignatureKey.Verify(cert.BytesForSigning(), cert.Signature.Blob)
|
||||
}
|
||||
|
||||
var certAlgoNames = map[string]string{
|
||||
KeyAlgoRSA: CertAlgoRSAv01,
|
||||
KeyAlgoDSA: CertAlgoDSAv01,
|
||||
KeyAlgoECDSA256: CertAlgoECDSA256v01,
|
||||
KeyAlgoECDSA384: CertAlgoECDSA384v01,
|
||||
KeyAlgoECDSA521: CertAlgoECDSA521v01,
|
||||
}
|
||||
|
||||
// certToPrivAlgo returns the underlying algorithm for a certificate algorithm.
|
||||
// Panics if a non-certificate algorithm is passed.
|
||||
func certToPrivAlgo(algo string) string {
|
||||
for privAlgo, pubAlgo := range certAlgoNames {
|
||||
if pubAlgo == algo {
|
||||
return privAlgo
|
||||
}
|
||||
}
|
||||
panic("unknown cert algorithm")
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) marshal(includeAlgo, includeSig bool) []byte {
|
||||
algoName := cert.PublicKeyAlgo()
|
||||
pubKey := cert.Key.Marshal()
|
||||
sigKey := MarshalPublicKey(cert.SignatureKey)
|
||||
|
||||
var length int
|
||||
if includeAlgo {
|
||||
length += stringLength(len(algoName))
|
||||
}
|
||||
length += stringLength(len(cert.Nonce))
|
||||
length += len(pubKey)
|
||||
length += 8 // Length of Serial
|
||||
length += 4 // Length of Type
|
||||
length += stringLength(len(cert.KeyId))
|
||||
length += lengthPrefixedNameListLength(cert.ValidPrincipals)
|
||||
length += 8 // Length of ValidAfter
|
||||
length += 8 // Length of ValidBefore
|
||||
length += tupleListLength(cert.CriticalOptions)
|
||||
length += tupleListLength(cert.Extensions)
|
||||
length += stringLength(len(cert.Reserved))
|
||||
length += stringLength(len(sigKey))
|
||||
if includeSig {
|
||||
length += signatureLength(cert.Signature)
|
||||
}
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := ret
|
||||
if includeAlgo {
|
||||
r = marshalString(r, []byte(algoName))
|
||||
}
|
||||
r = marshalString(r, cert.Nonce)
|
||||
copy(r, pubKey)
|
||||
r = r[len(pubKey):]
|
||||
r = marshalUint64(r, cert.Serial)
|
||||
r = marshalUint32(r, cert.Type)
|
||||
r = marshalString(r, []byte(cert.KeyId))
|
||||
r = marshalLengthPrefixedNameList(r, cert.ValidPrincipals)
|
||||
r = marshalUint64(r, uint64(cert.ValidAfter))
|
||||
r = marshalUint64(r, uint64(cert.ValidBefore))
|
||||
r = marshalTupleList(r, cert.CriticalOptions)
|
||||
r = marshalTupleList(r, cert.Extensions)
|
||||
r = marshalString(r, cert.Reserved)
|
||||
r = marshalString(r, sigKey)
|
||||
if includeSig {
|
||||
r = marshalSignature(r, cert.Signature)
|
||||
}
|
||||
if len(r) > 0 {
|
||||
panic("ssh: internal error, marshaling certificate did not fill the entire buffer")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) BytesForSigning() []byte {
|
||||
return cert.marshal(true, false)
|
||||
}
|
||||
|
||||
func (cert *OpenSSHCertV01) Marshal() []byte {
|
||||
return cert.marshal(false, true)
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) PublicKeyAlgo() string {
|
||||
algo, ok := certAlgoNames[c.Key.PublicKeyAlgo()]
|
||||
if !ok {
|
||||
panic("unknown cert key type")
|
||||
}
|
||||
return algo
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) PrivateKeyAlgo() string {
|
||||
return c.Key.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
func (c *OpenSSHCertV01) Verify(data []byte, sig []byte) bool {
|
||||
return c.Key.Verify(data, sig)
|
||||
}
|
||||
|
||||
func parseOpenSSHCertV01(in []byte, algo string) (out *OpenSSHCertV01, rest []byte, ok bool) {
|
||||
cert := new(OpenSSHCertV01)
|
||||
|
||||
if cert.Nonce, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
privAlgo := certToPrivAlgo(algo)
|
||||
cert.Key, in, ok = parsePubKey(in, privAlgo)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// We test PublicKeyAlgo to make sure we don't use some weird sub-cert.
|
||||
if cert.Key.PublicKeyAlgo() != privAlgo {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Serial, in, ok = parseUint64(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Type, in, ok = parseUint32(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
keyId, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.KeyId = string(keyId)
|
||||
|
||||
if cert.ValidPrincipals, in, ok = parseLengthPrefixedNameList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
va, in, ok := parseUint64(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.ValidAfter = CertTime(va)
|
||||
|
||||
vb, in, ok := parseUint64(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cert.ValidBefore = CertTime(vb)
|
||||
|
||||
if cert.CriticalOptions, in, ok = parseTupleList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Extensions, in, ok = parseTupleList(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Reserved, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
sigKey, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if cert.SignatureKey, _, ok = ParsePublicKey(sigKey); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if cert.Signature, in, ok = parseSignature(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return cert, in, ok
|
||||
}
|
||||
|
||||
func lengthPrefixedNameListLength(namelist []string) int {
|
||||
length := 4 // length prefix for list
|
||||
for _, name := range namelist {
|
||||
length += 4 // length prefix for name
|
||||
length += len(name)
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalLengthPrefixedNameList(to []byte, namelist []string) []byte {
|
||||
length := uint32(lengthPrefixedNameListLength(namelist) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
for _, name := range namelist {
|
||||
to = marshalString(to, []byte(name))
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
func parseLengthPrefixedNameList(in []byte) (out []string, rest []byte, ok bool) {
|
||||
list, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for len(list) > 0 {
|
||||
var next []byte
|
||||
if next, list, ok = parseString(list); !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
out = append(out, string(next))
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func tupleListLength(tupleList []tuple) int {
|
||||
length := 4 // length prefix for list
|
||||
for _, t := range tupleList {
|
||||
length += 4 // length prefix for t.Name
|
||||
length += len(t.Name)
|
||||
length += 4 // length prefix for t.Data
|
||||
length += len(t.Data)
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalTupleList(to []byte, tuplelist []tuple) []byte {
|
||||
length := uint32(tupleListLength(tuplelist) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
for _, t := range tuplelist {
|
||||
to = marshalString(to, []byte(t.Name))
|
||||
to = marshalString(to, []byte(t.Data))
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
func parseTupleList(in []byte) (out []tuple, rest []byte, ok bool) {
|
||||
list, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for len(list) > 0 {
|
||||
var name, data []byte
|
||||
var ok bool
|
||||
name, list, ok = parseString(list)
|
||||
if !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
data, list, ok = parseString(list)
|
||||
if !ok {
|
||||
return nil, nil, false
|
||||
}
|
||||
out = append(out, tuple{string(name), string(data)})
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func signatureLength(sig *signature) int {
|
||||
length := 4 // length prefix for signature
|
||||
length += stringLength(len(sig.Format))
|
||||
length += stringLength(len(sig.Blob))
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalSignature(to []byte, sig *signature) []byte {
|
||||
length := uint32(signatureLength(sig) - 4)
|
||||
to = marshalUint32(to, length)
|
||||
to = marshalString(to, []byte(sig.Format))
|
||||
to = marshalString(to, sig.Blob)
|
||||
return to
|
||||
}
|
||||
|
||||
func parseSignatureBody(in []byte) (out *signature, rest []byte, ok bool) {
|
||||
var format []byte
|
||||
if format, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
out = &signature{
|
||||
Format: string(format),
|
||||
}
|
||||
|
||||
if out.Blob, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
return out, in, ok
|
||||
}
|
||||
|
||||
func parseSignature(in []byte) (out *signature, rest []byte, ok bool) {
|
||||
var sigBytes []byte
|
||||
if sigBytes, rest, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
out, sigBytes, ok = parseSignatureBody(sigBytes)
|
||||
if !ok || len(sigBytes) > 0 {
|
||||
return nil, nil, false
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package crypto
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,6 +26,15 @@ type ECDSAService struct {
|
||||
privateKey *ecdsa.PrivateKey
|
||||
publicKey *ecdsa.PublicKey
|
||||
encodedPubKey string
|
||||
secret string
|
||||
}
|
||||
|
||||
// NewECDSAService returns a pointer to a ECDSAService.
|
||||
// An optional secret can be specified
|
||||
func NewECDSAService(secret string) *ECDSAService {
|
||||
return &ECDSAService{
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
// EncodedPublicKey returns the encoded version of the public that can be used
|
||||
@@ -93,34 +100,17 @@ func (service *ECDSAService) GenerateKeyPair() ([]byte, []byte, error) {
|
||||
return private, public, nil
|
||||
}
|
||||
|
||||
// GenerateKeyPair will create a new key pair using ECDSA.
|
||||
func (service *ECDSAService) GenerateSshKey() ([]byte, error) {
|
||||
//savePublicFileTo := "./id_ecdsa_test.pub"
|
||||
|
||||
pubkeyCurve := elliptic.P256()
|
||||
|
||||
byteKeys, err := ecdsa.GenerateKey(pubkeyCurve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
|
||||
publicKeyBytes, err := generateECDSAPublicKey(&byteKeys.PublicKey)
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
log.Println(publicKeyBytes)
|
||||
err = writeKeyToFile(publicKeyBytes,"./testFile" )
|
||||
if err != nil {
|
||||
return nil,err
|
||||
}
|
||||
return publicKeyBytes, nil
|
||||
}
|
||||
|
||||
// Sign creates a signature from a message.
|
||||
// It automatically hash the message using MD5 and creates a signature from
|
||||
// CreateSignature creates a digital signature.
|
||||
// It automatically hash a specific message using MD5 and creates a signature from
|
||||
// that hash.
|
||||
// If a secret is associated to the service, it will be used instead of the specified
|
||||
// message.
|
||||
// It then encodes the generated signature in base64.
|
||||
func (service *ECDSAService) Sign(message string) (string, error) {
|
||||
func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||
if service.secret != "" {
|
||||
message = service.secret
|
||||
}
|
||||
|
||||
hash := HashFromBytes([]byte(message))
|
||||
|
||||
r := big.NewInt(0)
|
||||
@@ -145,22 +135,3 @@ func (service *ECDSAService) Sign(message string) (string, error) {
|
||||
|
||||
return base64.RawStdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
func generateECDSAPublicKey(privatekey *ecdsa.PublicKey) ([]byte, error) {
|
||||
publicRsaKey, err := NewPublicKey(privatekey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pubKeyBytes := MarshalAuthorizedKey(publicRsaKey)
|
||||
return pubKeyBytes, nil
|
||||
}
|
||||
// writePemToFile writes keys to a file
|
||||
func writeKeyToFile(keyBytes []byte, saveFileTo string) error {
|
||||
log.Println("hey")
|
||||
err := ioutil.WriteFile(saveFileTo, keyBytes, 0777)
|
||||
log.Println("here")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This package is a duplicate of 32844aa1ae54: https://code.google.com/p/go/source/browse/ssh/keys.go?repo=crypto
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/dsa"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// These constants represent the algorithm names for key types supported by this
|
||||
// package.
|
||||
const (
|
||||
KeyAlgoRSA = "ssh-rsa"
|
||||
KeyAlgoDSA = "ssh-dss"
|
||||
KeyAlgoECDSA256 = "ecdsa-sha2-nistp256"
|
||||
KeyAlgoECDSA384 = "ecdsa-sha2-nistp384"
|
||||
KeyAlgoECDSA521 = "ecdsa-sha2-nistp521"
|
||||
)
|
||||
|
||||
// parsePubKey parses a public key of the given algorithm.
|
||||
// Use ParsePublicKey for keys with prepended algorithm.
|
||||
func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, ok bool) {
|
||||
|
||||
switch algo {
|
||||
case KeyAlgoRSA:
|
||||
return parseRSA(in)
|
||||
case KeyAlgoDSA:
|
||||
return parseDSA(in)
|
||||
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
|
||||
return parseECDSA(in)
|
||||
case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01:
|
||||
return parseOpenSSHCertV01(in, algo)
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
|
||||
|
||||
// parseAuthorizedKey parses a public key in OpenSSH authorized_keys format
|
||||
// (see sshd(8) manual page) once the options and key type fields have been
|
||||
// removed.
|
||||
func parseAuthorizedKey(in []byte) (out PublicKey, comment string, ok bool) {
|
||||
in = bytes.TrimSpace(in)
|
||||
|
||||
i := bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
i = len(in)
|
||||
}
|
||||
base64Key := in[:i]
|
||||
|
||||
key := make([]byte, base64.StdEncoding.DecodedLen(len(base64Key)))
|
||||
n, err := base64.StdEncoding.Decode(key, base64Key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
key = key[:n]
|
||||
out, _, ok = ParsePublicKey(key)
|
||||
if !ok {
|
||||
return nil, "", false
|
||||
}
|
||||
comment = string(bytes.TrimSpace(in[i:]))
|
||||
return
|
||||
}
|
||||
|
||||
// ParseAuthorizedKeys parses a public key from an authorized_keys
|
||||
// file used in OpenSSH according to the sshd(8) manual page.
|
||||
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, ok bool) {
|
||||
for len(in) > 0 {
|
||||
end := bytes.IndexByte(in, '\n')
|
||||
if end != -1 {
|
||||
rest = in[end+1:]
|
||||
in = in[:end]
|
||||
} else {
|
||||
rest = nil
|
||||
}
|
||||
|
||||
end = bytes.IndexByte(in, '\r')
|
||||
if end != -1 {
|
||||
in = in[:end]
|
||||
}
|
||||
|
||||
in = bytes.TrimSpace(in)
|
||||
if len(in) == 0 || in[0] == '#' {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
i := bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
if out, comment, ok = parseAuthorizedKey(in[i:]); ok {
|
||||
return
|
||||
}
|
||||
|
||||
// No key type recognised. Maybe there's an options field at
|
||||
// the beginning.
|
||||
var b byte
|
||||
inQuote := false
|
||||
var candidateOptions []string
|
||||
optionStart := 0
|
||||
for i, b = range in {
|
||||
isEnd := !inQuote && (b == ' ' || b == '\t')
|
||||
if (b == ',' && !inQuote) || isEnd {
|
||||
if i-optionStart > 0 {
|
||||
candidateOptions = append(candidateOptions, string(in[optionStart:i]))
|
||||
}
|
||||
optionStart = i + 1
|
||||
}
|
||||
if isEnd {
|
||||
break
|
||||
}
|
||||
if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) {
|
||||
inQuote = !inQuote
|
||||
}
|
||||
}
|
||||
for i < len(in) && (in[i] == ' ' || in[i] == '\t') {
|
||||
i++
|
||||
}
|
||||
if i == len(in) {
|
||||
// Invalid line: unmatched quote
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
in = in[i:]
|
||||
i = bytes.IndexAny(in, " \t")
|
||||
if i == -1 {
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
if out, comment, ok = parseAuthorizedKey(in[i:]); ok {
|
||||
options = candidateOptions
|
||||
return
|
||||
}
|
||||
|
||||
in = rest
|
||||
continue
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ParsePublicKey parses an SSH public key formatted for use in
|
||||
// the SSH wire protocol according to RFC 4253, section 6.6.
|
||||
func ParsePublicKey(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
algo, in, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
return parsePubKey(in, string(algo))
|
||||
}
|
||||
|
||||
// MarshalAuthorizedKey returns a byte stream suitable for inclusion
|
||||
// in an OpenSSH authorized_keys file following the format specified
|
||||
// in the sshd(8) manual page.
|
||||
func MarshalAuthorizedKey(key PublicKey) []byte {
|
||||
b := &bytes.Buffer{}
|
||||
b.WriteString(key.PublicKeyAlgo())
|
||||
b.WriteByte(' ')
|
||||
e := base64.NewEncoder(base64.StdEncoding, b)
|
||||
e.Write(MarshalPublicKey(key))
|
||||
e.Close()
|
||||
b.WriteByte('\n')
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// PublicKey is an abstraction of different types of public keys.
|
||||
type PublicKey interface {
|
||||
// PrivateKeyAlgo returns the name of the encryption system.
|
||||
PrivateKeyAlgo() string
|
||||
|
||||
// PublicKeyAlgo returns the algorithm for the public key,
|
||||
// which may be different from PrivateKeyAlgo for certificates.
|
||||
PublicKeyAlgo() string
|
||||
|
||||
// Marshal returns the serialized key data in SSH wire format,
|
||||
// without the name prefix. Callers should typically use
|
||||
// MarshalPublicKey().
|
||||
Marshal() []byte
|
||||
|
||||
// Verify that sig is a signature on the given data using this
|
||||
// key. This function will hash the data appropriately first.
|
||||
Verify(data []byte, sigBlob []byte) bool
|
||||
}
|
||||
|
||||
// A Signer is can create signatures that verify against a public key.
|
||||
type Signer interface {
|
||||
// PublicKey returns an associated PublicKey instance.
|
||||
PublicKey() PublicKey
|
||||
|
||||
// Sign returns raw signature for the given data. This method
|
||||
// will apply the hash specified for the keytype to the data.
|
||||
Sign(rand io.Reader, data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type rsaPublicKey rsa.PublicKey
|
||||
|
||||
func (r *rsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ssh-rsa"
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) PublicKeyAlgo() string {
|
||||
return r.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseRSA parses an RSA key according to RFC 4253, section 6.6.
|
||||
func parseRSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
key := new(rsa.PublicKey)
|
||||
|
||||
bigE, in, ok := parseInt(in)
|
||||
if !ok || bigE.BitLen() > 24 {
|
||||
return
|
||||
}
|
||||
e := bigE.Int64()
|
||||
if e < 3 || e&1 == 0 {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
key.E = int(e)
|
||||
|
||||
if key.N, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return (*rsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) Marshal() []byte {
|
||||
// See RFC 4253, section 6.6.
|
||||
e := new(big.Int).SetInt64(int64(r.E))
|
||||
length := intLength(e)
|
||||
length += intLength(r.N)
|
||||
|
||||
ret := make([]byte, length)
|
||||
rest := marshalInt(ret, e)
|
||||
marshalInt(rest, r.N)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *rsaPublicKey) Verify(data []byte, sig []byte) bool {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
return rsa.VerifyPKCS1v15((*rsa.PublicKey)(r), crypto.SHA1, digest, sig) == nil
|
||||
}
|
||||
|
||||
type rsaPrivateKey struct {
|
||||
*rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (r *rsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*rsaPublicKey)(&r.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (r *rsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
return rsa.SignPKCS1v15(rand, r.PrivateKey, crypto.SHA1, digest)
|
||||
}
|
||||
|
||||
type dsaPublicKey dsa.PublicKey
|
||||
|
||||
func (r *dsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ssh-dss"
|
||||
}
|
||||
|
||||
func (r *dsaPublicKey) PublicKeyAlgo() string {
|
||||
return r.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseDSA parses an DSA key according to RFC 4253, section 6.6.
|
||||
func parseDSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
key := new(dsa.PublicKey)
|
||||
|
||||
if key.P, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.Q, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.G, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if key.Y, in, ok = parseInt(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ok = true
|
||||
return (*dsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (r *dsaPublicKey) Marshal() []byte {
|
||||
// See RFC 4253, section 6.6.
|
||||
length := intLength(r.P)
|
||||
length += intLength(r.Q)
|
||||
length += intLength(r.G)
|
||||
length += intLength(r.Y)
|
||||
|
||||
ret := make([]byte, length)
|
||||
rest := marshalInt(ret, r.P)
|
||||
rest = marshalInt(rest, r.Q)
|
||||
rest = marshalInt(rest, r.G)
|
||||
marshalInt(rest, r.Y)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (k *dsaPublicKey) Verify(data []byte, sigBlob []byte) bool {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
|
||||
// Per RFC 4253, section 6.6,
|
||||
// The value for 'dss_signature_blob' is encoded as a string containing
|
||||
// r, followed by s (which are 160-bit integers, without lengths or
|
||||
// padding, unsigned, and in network byte order).
|
||||
// For DSS purposes, sig.Blob should be exactly 40 bytes in length.
|
||||
if len(sigBlob) != 40 {
|
||||
return false
|
||||
}
|
||||
r := new(big.Int).SetBytes(sigBlob[:20])
|
||||
s := new(big.Int).SetBytes(sigBlob[20:])
|
||||
return dsa.Verify((*dsa.PublicKey)(k), digest, r, s)
|
||||
}
|
||||
|
||||
type dsaPrivateKey struct {
|
||||
*dsa.PrivateKey
|
||||
}
|
||||
|
||||
func (k *dsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*dsaPublicKey)(&k.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (k *dsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := crypto.SHA1.New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
r, s, err := dsa.Sign(rand, k.PrivateKey, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := make([]byte, 40)
|
||||
copy(sig[:20], r.Bytes())
|
||||
copy(sig[20:], s.Bytes())
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
type ecdsaPublicKey ecdsa.PublicKey
|
||||
|
||||
func (key *ecdsaPublicKey) PrivateKeyAlgo() string {
|
||||
return "ecdsa-sha2-" + key.nistID()
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) nistID() string {
|
||||
switch key.Params().BitSize {
|
||||
case 256:
|
||||
return "nistp256"
|
||||
case 384:
|
||||
return "nistp384"
|
||||
case 521:
|
||||
return "nistp521"
|
||||
}
|
||||
panic("ssh: unsupported ecdsa key size")
|
||||
}
|
||||
|
||||
func supportedEllipticCurve(curve elliptic.Curve) bool {
|
||||
return (curve == elliptic.P256() || curve == elliptic.P384() || curve == elliptic.P521())
|
||||
}
|
||||
|
||||
// ecHash returns the hash to match the given elliptic curve, see RFC
|
||||
// 5656, section 6.2.1
|
||||
func ecHash(curve elliptic.Curve) crypto.Hash {
|
||||
bitSize := curve.Params().BitSize
|
||||
switch {
|
||||
case bitSize <= 256:
|
||||
return crypto.SHA256
|
||||
case bitSize <= 384:
|
||||
return crypto.SHA384
|
||||
}
|
||||
return crypto.SHA512
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) PublicKeyAlgo() string {
|
||||
return key.PrivateKeyAlgo()
|
||||
}
|
||||
|
||||
// parseECDSA parses an ECDSA key according to RFC 5656, section 3.1.
|
||||
func parseECDSA(in []byte) (out PublicKey, rest []byte, ok bool) {
|
||||
var identifier []byte
|
||||
if identifier, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
key := new(ecdsa.PublicKey)
|
||||
|
||||
switch string(identifier) {
|
||||
case "nistp256":
|
||||
key.Curve = elliptic.P256()
|
||||
case "nistp384":
|
||||
key.Curve = elliptic.P384()
|
||||
case "nistp521":
|
||||
key.Curve = elliptic.P521()
|
||||
default:
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
|
||||
var keyBytes []byte
|
||||
if keyBytes, in, ok = parseString(in); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
key.X, key.Y = elliptic.Unmarshal(key.Curve, keyBytes)
|
||||
if key.X == nil || key.Y == nil {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
return (*ecdsaPublicKey)(key), in, ok
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) Marshal() []byte {
|
||||
// See RFC 5656, section 3.1.
|
||||
keyBytes := elliptic.Marshal(key.Curve, key.X, key.Y)
|
||||
|
||||
ID := key.nistID()
|
||||
length := stringLength(len(ID))
|
||||
length += stringLength(len(keyBytes))
|
||||
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(ID))
|
||||
r = marshalString(r, keyBytes)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (key *ecdsaPublicKey) Verify(data []byte, sigBlob []byte) bool {
|
||||
h := ecHash(key.Curve).New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
|
||||
// Per RFC 5656, section 3.1.2,
|
||||
// The ecdsa_signature_blob value has the following specific encoding:
|
||||
// mpint r
|
||||
// mpint s
|
||||
r, rest, ok := parseInt(sigBlob)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
s, rest, ok := parseInt(rest)
|
||||
if !ok || len(rest) > 0 {
|
||||
return false
|
||||
}
|
||||
return ecdsa.Verify((*ecdsa.PublicKey)(key), digest, r, s)
|
||||
}
|
||||
|
||||
type ecdsaPrivateKey struct {
|
||||
*ecdsa.PrivateKey
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) PublicKey() PublicKey {
|
||||
return (*ecdsaPublicKey)(&k.PrivateKey.PublicKey)
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) Sign(rand io.Reader, data []byte) ([]byte, error) {
|
||||
h := ecHash(k.PrivateKey.PublicKey.Curve).New()
|
||||
h.Write(data)
|
||||
digest := h.Sum(nil)
|
||||
r, s, err := ecdsa.Sign(rand, k.PrivateKey, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig := make([]byte, intLength(r)+intLength(s))
|
||||
rest := marshalInt(sig, r)
|
||||
marshalInt(rest, s)
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// NewPrivateKey takes a pointer to rsa, dsa or ecdsa PrivateKey
|
||||
// returns a corresponding Signer instance. EC keys should use P256,
|
||||
// P384 or P521.
|
||||
func NewSignerFromKey(k interface{}) (Signer, error) {
|
||||
var sshKey Signer
|
||||
switch t := k.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
sshKey = &rsaPrivateKey{t}
|
||||
case *dsa.PrivateKey:
|
||||
sshKey = &dsaPrivateKey{t}
|
||||
case *ecdsa.PrivateKey:
|
||||
if !supportedEllipticCurve(t.Curve) {
|
||||
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
|
||||
}
|
||||
|
||||
sshKey = &ecdsaPrivateKey{t}
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
|
||||
}
|
||||
return sshKey, nil
|
||||
}
|
||||
|
||||
// NewPublicKey takes a pointer to rsa, dsa or ecdsa PublicKey
|
||||
// and returns a corresponding ssh PublicKey instance. EC keys should use P256, P384 or P521.
|
||||
func NewPublicKey(k interface{}) (PublicKey, error) {
|
||||
|
||||
var sshKey PublicKey
|
||||
|
||||
switch t := k.(type) {
|
||||
case *rsa.PublicKey:
|
||||
sshKey = (*rsaPublicKey)(t)
|
||||
case *ecdsa.PublicKey:
|
||||
if !supportedEllipticCurve(t.Curve) {
|
||||
return nil, errors.New("ssh: only P256, P384 and P521 EC keys are supported.")
|
||||
}
|
||||
sshKey = (*ecdsaPublicKey)(t)
|
||||
case *dsa.PublicKey:
|
||||
sshKey = (*dsaPublicKey)(t)
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
|
||||
}
|
||||
return sshKey, nil
|
||||
}
|
||||
|
||||
// ParsePublicKey parses a PEM encoded private key. It supports
|
||||
// PKCS#1, RSA, DSA and ECDSA private keys.
|
||||
func ParsePrivateKey(pemBytes []byte) (Signer, error) {
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
return nil, errors.New("ssh: no key found")
|
||||
}
|
||||
|
||||
var rawkey interface{}
|
||||
switch block.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
rsa, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = rsa
|
||||
case "EC PRIVATE KEY":
|
||||
ec, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = ec
|
||||
case "DSA PRIVATE KEY":
|
||||
ec, err := parseDSAPrivate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawkey = ec
|
||||
default:
|
||||
return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type)
|
||||
}
|
||||
|
||||
return NewSignerFromKey(rawkey)
|
||||
}
|
||||
|
||||
// parseDSAPrivate parses a DSA key in ASN.1 DER encoding, as
|
||||
// documented in the OpenSSL DSA manpage.
|
||||
// TODO(hanwen): move this in to crypto/x509 after the Go 1.2 freeze.
|
||||
func parseDSAPrivate(p []byte) (*dsa.PrivateKey, error) {
|
||||
k := struct {
|
||||
Version int
|
||||
P *big.Int
|
||||
Q *big.Int
|
||||
G *big.Int
|
||||
Priv *big.Int
|
||||
Pub *big.Int
|
||||
}{}
|
||||
rest, err := asn1.Unmarshal(p, &k)
|
||||
if err != nil {
|
||||
return nil, errors.New("ssh: failed to parse DSA key: " + err.Error())
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
return nil, errors.New("ssh: garbage after DSA key")
|
||||
}
|
||||
|
||||
return &dsa.PrivateKey{
|
||||
PublicKey: dsa.PublicKey{
|
||||
Parameters: dsa.Parameters{
|
||||
P: k.P,
|
||||
Q: k.Q,
|
||||
G: k.G,
|
||||
},
|
||||
Y: k.Priv,
|
||||
},
|
||||
X: k.Pub,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
// Copyright 2012 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package crypto
|
||||
|
||||
// Copy only of used functions from message.go, common.go
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
const (
|
||||
kexAlgoDH1SHA1 = "diffie-hellman-group1-sha1"
|
||||
kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
|
||||
kexAlgoECDH256 = "ecdh-sha2-nistp256"
|
||||
kexAlgoECDH384 = "ecdh-sha2-nistp384"
|
||||
kexAlgoECDH521 = "ecdh-sha2-nistp521"
|
||||
)
|
||||
|
||||
var bigOne = big.NewInt(1)
|
||||
|
||||
func stringLength(n int) int {
|
||||
return 4 + n
|
||||
}
|
||||
|
||||
// MarshalPublicKey serializes a supported key or certificate for use
|
||||
// by the SSH wire protocol. It can be used for comparison with the
|
||||
// pubkey argument of ServerConfig's PublicKeyCallback as well as for
|
||||
// generating an authorized_keys or host_keys file.
|
||||
func MarshalPublicKey(key PublicKey) []byte {
|
||||
// See also RFC 4253 6.6.
|
||||
algoname := key.PublicKeyAlgo()
|
||||
blob := key.Marshal()
|
||||
|
||||
length := stringLength(len(algoname))
|
||||
length += len(blob)
|
||||
ret := make([]byte, length)
|
||||
r := marshalString(ret, []byte(algoname))
|
||||
copy(r, blob)
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseInt(in []byte) (out *big.Int, rest []byte, ok bool) {
|
||||
contents, rest, ok := parseString(in)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
out = new(big.Int)
|
||||
|
||||
if len(contents) > 0 && contents[0]&0x80 == 0x80 {
|
||||
// This is a negative number
|
||||
notBytes := make([]byte, len(contents))
|
||||
for i := range notBytes {
|
||||
notBytes[i] = ^contents[i]
|
||||
}
|
||||
out.SetBytes(notBytes)
|
||||
out.Add(out, bigOne)
|
||||
out.Neg(out)
|
||||
} else {
|
||||
// Positive number
|
||||
out.SetBytes(contents)
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func parseUint32(in []byte) (uint32, []byte, bool) {
|
||||
if len(in) < 4 {
|
||||
return 0, nil, false
|
||||
}
|
||||
return binary.BigEndian.Uint32(in), in[4:], true
|
||||
}
|
||||
|
||||
func parseUint64(in []byte) (uint64, []byte, bool) {
|
||||
if len(in) < 8 {
|
||||
return 0, nil, false
|
||||
}
|
||||
return binary.BigEndian.Uint64(in), in[8:], true
|
||||
}
|
||||
|
||||
func parseString(in []byte) (out, rest []byte, ok bool) {
|
||||
if len(in) < 4 {
|
||||
return
|
||||
}
|
||||
length := binary.BigEndian.Uint32(in)
|
||||
if uint32(len(in)) < 4+length {
|
||||
return
|
||||
}
|
||||
out = in[4 : 4+length]
|
||||
rest = in[4+length:]
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func intLength(n *big.Int) int {
|
||||
length := 4 /* length bytes */
|
||||
if n.Sign() < 0 {
|
||||
nMinus1 := new(big.Int).Neg(n)
|
||||
nMinus1.Sub(nMinus1, bigOne)
|
||||
bitLen := nMinus1.BitLen()
|
||||
if bitLen%8 == 0 {
|
||||
// The number will need 0xff padding
|
||||
length++
|
||||
}
|
||||
length += (bitLen + 7) / 8
|
||||
} else if n.Sign() == 0 {
|
||||
// A zero is the zero length string
|
||||
} else {
|
||||
bitLen := n.BitLen()
|
||||
if bitLen%8 == 0 {
|
||||
// The number will need 0x00 padding
|
||||
length++
|
||||
}
|
||||
length += (bitLen + 7) / 8
|
||||
}
|
||||
|
||||
return length
|
||||
}
|
||||
|
||||
func marshalUint32(to []byte, n uint32) []byte {
|
||||
binary.BigEndian.PutUint32(to, n)
|
||||
return to[4:]
|
||||
}
|
||||
|
||||
func marshalUint64(to []byte, n uint64) []byte {
|
||||
binary.BigEndian.PutUint64(to, n)
|
||||
return to[8:]
|
||||
}
|
||||
|
||||
func marshalInt(to []byte, n *big.Int) []byte {
|
||||
lengthBytes := to
|
||||
to = to[4:]
|
||||
length := 0
|
||||
|
||||
if n.Sign() < 0 {
|
||||
// A negative number has to be converted to two's-complement
|
||||
// form. So we'll subtract 1 and invert. If the
|
||||
// most-significant-bit isn't set then we'll need to pad the
|
||||
// beginning with 0xff in order to keep the number negative.
|
||||
nMinus1 := new(big.Int).Neg(n)
|
||||
nMinus1.Sub(nMinus1, bigOne)
|
||||
bytes := nMinus1.Bytes()
|
||||
for i := range bytes {
|
||||
bytes[i] ^= 0xff
|
||||
}
|
||||
if len(bytes) == 0 || bytes[0]&0x80 == 0 {
|
||||
to[0] = 0xff
|
||||
to = to[1:]
|
||||
length++
|
||||
}
|
||||
nBytes := copy(to, bytes)
|
||||
to = to[nBytes:]
|
||||
length += nBytes
|
||||
} else if n.Sign() == 0 {
|
||||
// A zero is the zero length string
|
||||
} else {
|
||||
bytes := n.Bytes()
|
||||
if len(bytes) > 0 && bytes[0]&0x80 != 0 {
|
||||
// We'll have to pad this with a 0x00 in order to
|
||||
// stop it looking like a negative number.
|
||||
to[0] = 0
|
||||
to = to[1:]
|
||||
length++
|
||||
}
|
||||
nBytes := copy(to, bytes)
|
||||
to = to[nBytes:]
|
||||
length += nBytes
|
||||
}
|
||||
|
||||
lengthBytes[0] = byte(length >> 24)
|
||||
lengthBytes[1] = byte(length >> 16)
|
||||
lengthBytes[2] = byte(length >> 8)
|
||||
lengthBytes[3] = byte(length)
|
||||
return to
|
||||
}
|
||||
|
||||
func marshalString(to []byte, s []byte) []byte {
|
||||
to[0] = byte(len(s) >> 24)
|
||||
to[1] = byte(len(s) >> 16)
|
||||
to[2] = byte(len(s) >> 8)
|
||||
to[3] = byte(len(s))
|
||||
to = to[4:]
|
||||
copy(to, s)
|
||||
return to[len(s):]
|
||||
}
|
||||
@@ -27,12 +27,13 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService) *Clien
|
||||
}
|
||||
|
||||
// CreateClient is a generic function to create a Docker client based on
|
||||
// a specific endpoint configuration
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
// a specific endpoint configuration. The nodeName parameter can be used
|
||||
// with an agent enabled endpoint to target a specific node in an agent cluster.
|
||||
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string) (*client.Client, error) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return nil, unsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return createAgentClient(endpoint, factory.signatureService)
|
||||
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||
@@ -61,13 +62,13 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
)
|
||||
}
|
||||
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService) (*client.Client, error) {
|
||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signature, err := signatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
signature, err := signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -77,6 +78,10 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
||||
portainer.PortainerAgentSignatureHeader: signature,
|
||||
}
|
||||
|
||||
if nodeName != "" {
|
||||
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||
}
|
||||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
@@ -97,7 +102,7 @@ func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
Transport: transport,
|
||||
Timeout: 30 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
115
api/docker/job.go
Normal file
115
api/docker/job.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/archive"
|
||||
)
|
||||
|
||||
// JobService represents a service that handles the execution of jobs
|
||||
type JobService struct {
|
||||
dockerClientFactory *ClientFactory
|
||||
}
|
||||
|
||||
// NewJobService returns a pointer to a new job service
|
||||
func NewJobService(dockerClientFactory *ClientFactory) *JobService {
|
||||
return &JobService{
|
||||
dockerClientFactory: dockerClientFactory,
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteScript will leverage a privileged container to execute a script against the specified endpoint/nodename.
|
||||
// It will copy the script content specified as a parameter inside a container based on the specified image and execute it.
|
||||
func (service *JobService) ExecuteScript(endpoint *portainer.Endpoint, nodeName, image string, script []byte, schedule *portainer.Schedule) error {
|
||||
buffer, err := archive.TarFileInBuffer(script, "script.sh", 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli, err := service.dockerClientFactory.CreateClient(endpoint, nodeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
_, err = cli.Ping(context.Background())
|
||||
if err != nil {
|
||||
return portainer.ErrUnableToPingEndpoint
|
||||
}
|
||||
|
||||
err = pullImage(cli, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containerConfig := &container.Config{
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
WorkingDir: "/tmp",
|
||||
Image: image,
|
||||
Labels: map[string]string{
|
||||
"io.portainer.job.endpoint": strconv.Itoa(int(endpoint.ID)),
|
||||
},
|
||||
Cmd: strslice.StrSlice([]string{"sh", "/tmp/script.sh"}),
|
||||
}
|
||||
|
||||
if schedule != nil {
|
||||
containerConfig.Labels["io.portainer.schedule.id"] = strconv.Itoa(int(schedule.ID))
|
||||
}
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
Binds: []string{"/:/host", "/etc:/etc:ro", "/usr:/usr:ro", "/run:/run:ro", "/sbin:/sbin:ro", "/var:/var:ro"},
|
||||
NetworkMode: "host",
|
||||
Privileged: true,
|
||||
}
|
||||
|
||||
networkConfig := &network.NetworkingConfig{}
|
||||
|
||||
body, err := cli.ContainerCreate(context.Background(), containerConfig, hostConfig, networkConfig, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schedule != nil {
|
||||
err = cli.ContainerRename(context.Background(), body.ID, schedule.Name+"_"+body.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
copyOptions := types.CopyToContainerOptions{}
|
||||
err = cli.CopyToContainer(context.Background(), body.ID, "/tmp", bytes.NewReader(buffer), copyOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startOptions := types.ContainerStartOptions{}
|
||||
return cli.ContainerStart(context.Background(), body.ID, startOptions)
|
||||
}
|
||||
|
||||
func pullImage(cli *client.Client, image string) error {
|
||||
imageReadCloser, err := cli.ImagePull(context.Background(), image, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer imageReadCloser.Close()
|
||||
|
||||
_, err = io.Copy(ioutil.Discard, imageReadCloser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -52,6 +52,16 @@ func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = snapshotNetworks(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = snapshotVersion(snapshot, cli)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
snapshot.Time = time.Now().Unix()
|
||||
return snapshot, nil
|
||||
}
|
||||
@@ -66,6 +76,7 @@ func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
snapshot.DockerVersion = info.ServerVersion
|
||||
snapshot.TotalCPU = info.NCPU
|
||||
snapshot.TotalMemory = info.MemTotal
|
||||
snapshot.SnapshotRaw.Info = info
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -132,6 +143,7 @@ func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error
|
||||
snapshot.RunningContainerCount = runningContainers
|
||||
snapshot.StoppedContainerCount = stoppedContainers
|
||||
snapshot.StackCount += len(stacks)
|
||||
snapshot.SnapshotRaw.Containers = containers
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -142,6 +154,7 @@ func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
}
|
||||
|
||||
snapshot.ImageCount = len(images)
|
||||
snapshot.SnapshotRaw.Images = images
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -152,5 +165,24 @@ func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
}
|
||||
|
||||
snapshot.VolumeCount = len(volumes.Volumes)
|
||||
snapshot.SnapshotRaw.Volumes = volumes
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotNetworks(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshot.SnapshotRaw.Networks = networks
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotVersion(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||
version, err := cli.ServerVersion(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshot.SnapshotRaw.Version = version
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
|
||||
|
||||
// CreateSnapshot creates a snapshot of a specific endpoint
|
||||
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint)
|
||||
cli, err := snapshotter.clientFactory.CreateClient(endpoint, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -71,11 +71,6 @@ const (
|
||||
ErrEndpointExtensionAlreadyAssociated = Error("This extension is already associated to the endpoint")
|
||||
)
|
||||
|
||||
// Deploykey errors
|
||||
const (
|
||||
ErrDeploykeyAlreadyExists = Error("A key already exists with this name")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
const (
|
||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||
@@ -93,6 +88,21 @@ const (
|
||||
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
|
||||
)
|
||||
|
||||
// Extension errors.
|
||||
const (
|
||||
ErrExtensionAlreadyEnabled = Error("This extension is already enabled")
|
||||
)
|
||||
|
||||
// Docker errors.
|
||||
const (
|
||||
ErrUnableToPingEndpoint = Error("Unable to communicate with the endpoint")
|
||||
)
|
||||
|
||||
// Schedule errors.
|
||||
const (
|
||||
ErrHostManagementFeaturesDisabled = Error("Host management features are disabled")
|
||||
)
|
||||
|
||||
// Error represents an application error.
|
||||
type Error string
|
||||
|
||||
|
||||
211
api/exec/extension.go
Normal file
211
api/exec/extension.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/orcaman/concurrent-map"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/client"
|
||||
)
|
||||
|
||||
var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/extensions/"
|
||||
|
||||
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
||||
portainer.RegistryManagementExtension: "extension-registry-management",
|
||||
portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
|
||||
}
|
||||
|
||||
// ExtensionManager represents a service used to
|
||||
// manage extension processes.
|
||||
type ExtensionManager struct {
|
||||
processes cmap.ConcurrentMap
|
||||
fileService portainer.FileService
|
||||
extensionService portainer.ExtensionService
|
||||
}
|
||||
|
||||
// NewExtensionManager returns a pointer to an ExtensionManager
|
||||
func NewExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) *ExtensionManager {
|
||||
return &ExtensionManager{
|
||||
processes: cmap.New(),
|
||||
fileService: fileService,
|
||||
extensionService: extensionService,
|
||||
}
|
||||
}
|
||||
|
||||
func processKey(ID portainer.ExtensionID) string {
|
||||
return strconv.Itoa(int(ID))
|
||||
}
|
||||
|
||||
func buildExtensionURL(extension *portainer.Extension) string {
|
||||
extensionURL := extensionDownloadBaseURL
|
||||
extensionURL += extensionBinaryMap[extension.ID]
|
||||
extensionURL += "-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
extensionURL += "-" + extension.Version
|
||||
extensionURL += ".zip"
|
||||
return extensionURL
|
||||
}
|
||||
|
||||
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
|
||||
|
||||
extensionFilename := extensionBinaryMap[extension.ID]
|
||||
extensionFilename += "-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
extensionFilename += "-" + extension.Version
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
extensionFilename += ".exe"
|
||||
}
|
||||
|
||||
extensionPath := path.Join(
|
||||
binaryPath,
|
||||
extensionFilename)
|
||||
|
||||
return extensionPath
|
||||
}
|
||||
|
||||
// FetchExtensionDefinitions will fetch the list of available
|
||||
// extension definitions from the official Portainer assets server
|
||||
func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
|
||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 30)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var extensions []portainer.Extension
|
||||
err = json.Unmarshal(extensionData, &extensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extensions, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
// mode to validate the extension license. If the license is valid, it will then start
|
||||
// the extension process and register it in the processes map.
|
||||
func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension, licenseKey string) error {
|
||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
||||
extensionBinaryExists, err := manager.fileService.FileExists(extensionBinaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !extensionBinaryExists {
|
||||
err := manager.downloadExtension(extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
licenseDetails, err := validateLicense(extensionBinaryPath, licenseKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension.License = portainer.LicenseInformation{
|
||||
LicenseKey: licenseKey,
|
||||
Company: licenseDetails[0],
|
||||
Expiration: licenseDetails[1],
|
||||
Valid: true,
|
||||
}
|
||||
extension.Version = licenseDetails[2]
|
||||
|
||||
return manager.startExtensionProcess(extension, extensionBinaryPath)
|
||||
}
|
||||
|
||||
// DisableExtension will retrieve the process associated to the extension
|
||||
// from the processes map and kill the process. It will then remove the process
|
||||
// from the processes map and remove the binary associated to the extension
|
||||
// from the filesystem
|
||||
func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension) error {
|
||||
process, ok := manager.processes.Get(processKey(extension.ID))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := process.(*exec.Cmd).Process.Kill()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.processes.Remove(processKey(extension.ID))
|
||||
|
||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
||||
return manager.fileService.RemoveDirectory(extensionBinaryPath)
|
||||
}
|
||||
|
||||
// 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
|
||||
func (manager *ExtensionManager) UpdateExtension(extension *portainer.Extension, version string) error {
|
||||
oldVersion := extension.Version
|
||||
|
||||
extension.Version = version
|
||||
err := manager.downloadExtension(extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension.Version = oldVersion
|
||||
err = manager.DisableExtension(extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension.Version = version
|
||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
||||
|
||||
licenseDetails, err := validateLicense(extensionBinaryPath, extension.License.LicenseKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension.Version = licenseDetails[2]
|
||||
|
||||
return manager.startExtensionProcess(extension, extensionBinaryPath)
|
||||
}
|
||||
|
||||
func (manager *ExtensionManager) downloadExtension(extension *portainer.Extension) error {
|
||||
extensionURL := buildExtensionURL(extension)
|
||||
|
||||
data, err := client.Get(extensionURL, 30)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return manager.fileService.ExtractExtensionArchive(data)
|
||||
}
|
||||
|
||||
func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
||||
licenseCheckProcess := exec.Command(binaryPath, "-license", licenseKey, "-check")
|
||||
cmdOutput := &bytes.Buffer{}
|
||||
licenseCheckProcess.Stdout = cmdOutput
|
||||
|
||||
err := licenseCheckProcess.Run()
|
||||
if err != nil {
|
||||
return nil, errors.New("Invalid extension license key")
|
||||
}
|
||||
|
||||
output := string(cmdOutput.Bytes())
|
||||
|
||||
return strings.Split(output, "|"), nil
|
||||
}
|
||||
|
||||
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
|
||||
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
|
||||
err := extensionProcess.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.processes.Set(processKey(extension.ID), extensionProcess)
|
||||
return nil
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(dataPath string)
|
||||
return err
|
||||
}
|
||||
|
||||
signature, err := manager.signatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
signature, err := manager.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/archive"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
@@ -32,6 +33,13 @@ const (
|
||||
PrivateKeyFile = "portainer.key"
|
||||
// PublicKeyFile represents the name on disk of the file containing the public key.
|
||||
PublicKeyFile = "portainer.pub"
|
||||
// BinaryStorePath represents the subfolder where binaries are stored in the file store folder.
|
||||
BinaryStorePath = "bin"
|
||||
// ScheduleStorePath represents the subfolder where schedule files are stored.
|
||||
ScheduleStorePath = "schedules"
|
||||
// ExtensionRegistryManagementStorePath represents the subfolder where files related to the
|
||||
// registry management extension are stored.
|
||||
ExtensionRegistryManagementStorePath = "extensions"
|
||||
)
|
||||
|
||||
// Service represents a service for managing files and directories.
|
||||
@@ -63,9 +71,30 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = service.createDirectoryInStore(BinaryStorePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// GetBinaryFolder returns the full path to the binary store on the filesystem
|
||||
func (service *Service) GetBinaryFolder() string {
|
||||
return path.Join(service.fileStorePath, BinaryStorePath)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// RemoveDirectory removes a directory on the filesystem.
|
||||
func (service *Service) RemoveDirectory(directoryPath string) error {
|
||||
return os.RemoveAll(directoryPath)
|
||||
@@ -97,6 +126,27 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
|
||||
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.
|
||||
func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) {
|
||||
extensionStorePath := path.Join(ExtensionRegistryManagementStorePath, folder)
|
||||
err := service.createDirectoryInStore(extensionStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
file := path.Join(extensionStorePath, fileName)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(file, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, file), nil
|
||||
}
|
||||
|
||||
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
|
||||
// It returns the path to the newly created file.
|
||||
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (string, error) {
|
||||
@@ -318,3 +368,32 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
||||
block, _ := pem.Decode(fileContent)
|
||||
return block.Bytes, nil
|
||||
}
|
||||
|
||||
// GetScheduleFolder returns the absolute path on the filesystem for a schedule based
|
||||
// on its identifier.
|
||||
func (service *Service) GetScheduleFolder(identifier string) string {
|
||||
return path.Join(service.fileStorePath, ScheduleStorePath, identifier)
|
||||
}
|
||||
|
||||
// StoreScheduledJobFileFromBytes creates a subfolder in the ScheduleStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreScheduledJobFileFromBytes(identifier string, data []byte) (string, error) {
|
||||
scheduleStorePath := path.Join(ScheduleStorePath, identifier)
|
||||
err := service.createDirectoryInStore(scheduleStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath := path.Join(scheduleStorePath, createScheduledJobFileName(identifier))
|
||||
r := bytes.NewReader(data)
|
||||
err = service.createFileInStore(filePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, filePath), nil
|
||||
}
|
||||
|
||||
func createScheduledJobFileName(identifier string) string {
|
||||
return "job_" + identifier + ".sh"
|
||||
}
|
||||
|
||||
138
api/http/handler/auth/authenticate_oauth.go
Normal file
138
api/http/handler/auth/authenticate_oauth.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"log"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type oauthPayload struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
func (payload *oauthPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Code) {
|
||||
return portainer.Error("Invalid OAuth authorization code")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateThroughExtension(code, licenseKey string, settings *portainer.OAuthSettings) (string, error) {
|
||||
extensionURL := handler.ProxyManager.GetExtensionURL(portainer.OAuthAuthenticationExtension)
|
||||
|
||||
encodedConfiguration, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", extensionURL+"/validate", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req.Header.Set("X-OAuth-Config", string(encodedConfiguration))
|
||||
req.Header.Set("X-OAuth-Code", code)
|
||||
req.Header.Set("X-PortainerExtension-License", licenseKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
type extensionResponse struct {
|
||||
Username string `json:"Username,omitempty"`
|
||||
Err string `json:"err,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
var extResp extensionResponse
|
||||
err = json.Unmarshal(body, &extResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", portainer.Error(extResp.Err + ":" + extResp.Details)
|
||||
}
|
||||
|
||||
return extResp.Username, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload oauthPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod != 3 {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", portainer.Error("OAuth authentication is not enabled")}
|
||||
}
|
||||
|
||||
extension, err := handler.ExtensionService.Extension(portainer.OAuthAuthenticationExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", portainer.ErrUnauthorized}
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername(username)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
|
||||
}
|
||||
|
||||
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", portainer.ErrUnauthorized}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
user = &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
|
||||
}
|
||||
|
||||
if settings.OAuthSettings.DefaultTeamID != 0 {
|
||||
membership := &portainer.TeamMembership{
|
||||
UserID: user.ID,
|
||||
TeamID: settings.OAuthSettings.DefaultTeamID,
|
||||
Role: portainer.TeamMember,
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.CreateTeamMembership(membership)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,8 @@ type Handler struct {
|
||||
SettingsService portainer.SettingsService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ExtensionService portainer.ExtensionService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
@@ -36,6 +39,9 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
|
||||
Router: mux.NewRouter(),
|
||||
authDisabled: authDisabled,
|
||||
}
|
||||
|
||||
h.Handle("/auth/oauth/validate",
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.validateOAuth)))).Methods(http.MethodPost)
|
||||
h.Handle("/auth",
|
||||
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"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"
|
||||
)
|
||||
|
||||
type deploykeyCreatePayload struct {
|
||||
Name string
|
||||
Privatekeypath string
|
||||
Publickeypath string
|
||||
UserID int
|
||||
LastUsage string
|
||||
}
|
||||
|
||||
func (payload *deploykeyCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid deploykey Name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/deploykeys
|
||||
func (handler *Handler) deploykeyCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload deploykeyCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
deploykeys, err := handler.DeploykeyService.Deploykeys()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve deploykeys from the database", err}
|
||||
}
|
||||
|
||||
for _, deploykey := range deploykeys {
|
||||
if deploykey.Name == payload.Name {
|
||||
return &httperror.HandlerError{http.StatusConflict, "This name is already associated to a deploykey", portainer.ErrDeploykeyAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
pubkeypath, errrrr := handler.DigitalDeploykeyService.GenerateSshKey()
|
||||
|
||||
if errrrr != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid key payload", errrrr}
|
||||
}
|
||||
|
||||
//encodedStr := hex.EncodeToString(privatepath)
|
||||
encodedStr1 := hex.EncodeToString(pubkeypath)
|
||||
dateTime := time.Now().Local().Format("2006-01-02 15:04:05")
|
||||
|
||||
deploykey := &portainer.Deploykey{
|
||||
Name: payload.Name,
|
||||
Privatekeypath: "abc",
|
||||
Publickeypath: encodedStr1,
|
||||
UserID: payload.UserID,
|
||||
LastUsage: dateTime,
|
||||
}
|
||||
|
||||
err = handler.DeploykeyService.CreateDeploykey(deploykey)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the deploykey inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, deploykey)
|
||||
}
|
||||
func BytesToString(data []byte) string {
|
||||
return string(data[:])
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// DELETE request on /api/deploykeys/:id
|
||||
func (handler *Handler) deploykeyDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid deploykey identifier route variable", err}
|
||||
}
|
||||
|
||||
err = handler.DeploykeyService.DeleteDeploykey(portainer.DeploykeyID(id))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the deploykey from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// GET request on /api/deploykeys
|
||||
func (handler *Handler) deploykeyList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
deploykeys, err := handler.DeploykeyService.Deploykeys()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve deploykeys from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, deploykeys)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package deploykeys
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle deploykey operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DeploykeyService portainer.DeploykeyService
|
||||
CryptoService portainer.CryptoService
|
||||
DigitalDeploykeyService portainer.DigitalDeploykeyService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage deploykey operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/deploykeys",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/deploykeys",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyList))).Methods(http.MethodGet)
|
||||
h.Handle("/deploykeys/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploykeyDelete))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package endpointproxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -23,6 +24,10 @@ 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.EndpointAccess(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package endpointproxy
|
||||
|
||||
// TODO: legacy extension management
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
@@ -42,9 +44,9 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt
|
||||
proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension)
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetExtensionProxy(proxyExtensionKey)
|
||||
proxy = handler.ProxyManager.GetLegacyExtensionProxy(proxyExtensionKey)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
|
||||
proxy, err = handler.ProxyManager.CreateLegacyExtensionProxy(proxyExtensionKey, storidgeExtension.URL)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy", err}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
||||
handler.ProxyManager.DeleteExtensionProxies(string(endpointID))
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package endpoints
|
||||
|
||||
// TODO: legacy extension management
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package endpoints
|
||||
|
||||
// TODO: legacy extension management
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
|
||||
116
api/http/handler/endpoints/endpoint_job.go
Normal file
116
api/http/handler/endpoints/endpoint_job.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
type endpointJobFromFilePayload struct {
|
||||
Image string
|
||||
File []byte
|
||||
}
|
||||
|
||||
type endpointJobFromFileContentPayload struct {
|
||||
Image string
|
||||
FileContent string
|
||||
}
|
||||
|
||||
func (payload *endpointJobFromFilePayload) Validate(r *http.Request) error {
|
||||
file, _, err := request.RetrieveMultiPartFormFile(r, "File")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Script file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.File = file
|
||||
|
||||
image, err := request.RetrieveMultiPartFormValue(r, "Image", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid image name")
|
||||
}
|
||||
payload.Image = image
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *endpointJobFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
return portainer.Error("Invalid script file content")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.Image) {
|
||||
return portainer.Error("Invalid image name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/endpoints/:id/job?method&nodeName
|
||||
func (handler *Handler) endpointJob(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}
|
||||
}
|
||||
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method", err}
|
||||
}
|
||||
|
||||
nodeName, _ := request.RetrieveQueryParameter(r, "nodeName", true)
|
||||
|
||||
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.EndpointAccess(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "file":
|
||||
return handler.executeJobFromFile(w, r, endpoint, nodeName)
|
||||
case "string":
|
||||
return handler.executeJobFromFileContent(w, r, endpoint, nodeName)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) executeJobFromFile(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, nodeName string) *httperror.HandlerError {
|
||||
payload := &endpointJobFromFilePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.ExecuteScript(endpoint, nodeName, payload.Image, payload.File, nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) executeJobFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, nodeName string) *httperror.HandlerError {
|
||||
var payload endpointJobFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
err = handler.JobService.ExecuteScript(endpoint, nodeName, payload.Image, []byte(payload.FileContent), nil)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed executing job", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
@@ -1,41 +1,51 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// POST request on /api/endpoints/snapshot
|
||||
// POST request on /api/endpoints/:id/snapshot
|
||||
func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
continue
|
||||
}
|
||||
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}
|
||||
}
|
||||
|
||||
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 endpoint.Type == portainer.AzureEnvironment {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for Azure endpoints", err}
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint)
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
latestEndpointReference.Status = portainer.EndpointStatusUp
|
||||
if snapshotError != nil {
|
||||
latestEndpointReference.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
|
||||
49
api/http/handler/endpoints/endpoint_snapshots.go
Normal file
49
api/http/handler/endpoints/endpoint_snapshots.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// POST request on /api/endpoints/snapshot
|
||||
func (handler *Handler) endpointSnapshots(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}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint)
|
||||
|
||||
latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID)
|
||||
if latestEndpointReference == nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
latestEndpointReference.Status = portainer.EndpointStatusUp
|
||||
if snapshotError != nil {
|
||||
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError)
|
||||
latestEndpointReference.Status = portainer.EndpointStatusDown
|
||||
}
|
||||
|
||||
if snapshot != nil {
|
||||
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
@@ -12,16 +12,17 @@ import (
|
||||
)
|
||||
|
||||
type endpointUpdatePayload struct {
|
||||
Name string
|
||||
URL string
|
||||
PublicURL string
|
||||
GroupID int
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
TLSSkipClientVerify bool
|
||||
AzureApplicationID string
|
||||
AzureTenantID string
|
||||
AzureAuthenticationKey string
|
||||
Name *string
|
||||
URL *string
|
||||
PublicURL *string
|
||||
GroupID *int
|
||||
TLS *bool
|
||||
TLSSkipVerify *bool
|
||||
TLSSkipClientVerify *bool
|
||||
Status *int
|
||||
AzureApplicationID *string
|
||||
AzureTenantID *string
|
||||
AzureAuthenticationKey *string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
@@ -53,36 +54,49 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if payload.Name != "" {
|
||||
endpoint.Name = payload.Name
|
||||
if payload.Name != nil {
|
||||
endpoint.Name = *payload.Name
|
||||
}
|
||||
|
||||
if payload.URL != "" {
|
||||
endpoint.URL = payload.URL
|
||||
if payload.URL != nil {
|
||||
endpoint.URL = *payload.URL
|
||||
}
|
||||
|
||||
if payload.PublicURL != "" {
|
||||
endpoint.PublicURL = payload.PublicURL
|
||||
if payload.PublicURL != nil {
|
||||
endpoint.PublicURL = *payload.PublicURL
|
||||
}
|
||||
|
||||
if payload.GroupID != 0 {
|
||||
endpoint.GroupID = portainer.EndpointGroupID(payload.GroupID)
|
||||
if payload.GroupID != nil {
|
||||
endpoint.GroupID = portainer.EndpointGroupID(*payload.GroupID)
|
||||
}
|
||||
|
||||
if payload.Tags != nil {
|
||||
endpoint.Tags = payload.Tags
|
||||
}
|
||||
|
||||
if payload.Status != nil {
|
||||
switch *payload.Status {
|
||||
case 1:
|
||||
endpoint.Status = portainer.EndpointStatusUp
|
||||
break
|
||||
case 2:
|
||||
endpoint.Status = portainer.EndpointStatusDown
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
credentials := endpoint.AzureCredentials
|
||||
if payload.AzureApplicationID != "" {
|
||||
credentials.ApplicationID = payload.AzureApplicationID
|
||||
if payload.AzureApplicationID != nil {
|
||||
credentials.ApplicationID = *payload.AzureApplicationID
|
||||
}
|
||||
if payload.AzureTenantID != "" {
|
||||
credentials.TenantID = payload.AzureTenantID
|
||||
if payload.AzureTenantID != nil {
|
||||
credentials.TenantID = *payload.AzureTenantID
|
||||
}
|
||||
if payload.AzureAuthenticationKey != "" {
|
||||
credentials.AuthenticationKey = payload.AzureAuthenticationKey
|
||||
if payload.AzureAuthenticationKey != nil {
|
||||
credentials.AuthenticationKey = *payload.AzureAuthenticationKey
|
||||
}
|
||||
|
||||
httpClient := client.NewHTTPClient()
|
||||
@@ -93,44 +107,55 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
endpoint.AzureCredentials = credentials
|
||||
}
|
||||
|
||||
folder := strconv.Itoa(endpointID)
|
||||
if payload.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = payload.TLSSkipVerify
|
||||
if !payload.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
|
||||
}
|
||||
if payload.TLS != nil {
|
||||
folder := strconv.Itoa(endpointID)
|
||||
|
||||
if *payload.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
if payload.TLSSkipVerify != nil {
|
||||
endpoint.TLSConfig.TLSSkipVerify = *payload.TLSSkipVerify
|
||||
|
||||
if !*payload.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
|
||||
}
|
||||
}
|
||||
|
||||
if payload.TLSSkipClientVerify != nil {
|
||||
if !*payload.TLSSkipClientVerify {
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
|
||||
}
|
||||
}
|
||||
|
||||
if !payload.TLSSkipClientVerify {
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
|
||||
}
|
||||
} else {
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(folder)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
|
||||
err = handler.FileService.DeleteTLSFiles(folder)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove TLS files from disk", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
|
||||
@@ -31,6 +31,7 @@ type Handler struct {
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
Snapshotter portainer.Snapshotter
|
||||
JobService portainer.JobService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
@@ -44,7 +45,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
||||
h.Handle("/endpoints",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/snapshot",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
@@ -59,6 +60,9 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||
|
||||
h.Handle("/endpoints/{id}/job",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/snapshot",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
79
api/http/handler/extensions/extension_create.go
Normal file
79
api/http/handler/extensions/extension_create.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package extensions
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type extensionCreatePayload struct {
|
||||
License string
|
||||
}
|
||||
|
||||
func (payload *extensionCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.License) {
|
||||
return portainer.Error("Invalid license")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload extensionCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
extensionIdentifier, err := strconv.Atoi(string(payload.License[0]))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid license format", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
extensions, err := handler.ExtensionService.Extensions()
|
||||
if err != nil {
|
||||
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,
|
||||
}
|
||||
|
||||
extensionDefinitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
||||
}
|
||||
|
||||
for _, def := range extensionDefinitions {
|
||||
if def.ID == extension.ID {
|
||||
extension.Version = def.Version
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.ExtensionManager.EnableExtension(extension, payload.License)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to enable extension", 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}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
38
api/http/handler/extensions/extension_delete.go
Normal file
38
api/http/handler/extensions/extension_delete.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// DELETE request on /api/extensions/:id
|
||||
func (handler *Handler) extensionDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
extension, err := handler.ExtensionService.Extension(extensionID)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.ExtensionManager.DisableExtension(extension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete extension", err}
|
||||
}
|
||||
|
||||
err = handler.ExtensionService.DeleteExtension(extensionID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the extension from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
63
api/http/handler/extensions/extension_inspect.go
Normal file
63
api/http/handler/extensions/extension_inspect.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/client"
|
||||
)
|
||||
|
||||
// GET request on /api/extensions/:id
|
||||
func (handler *Handler) extensionInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 30)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
||||
}
|
||||
|
||||
var extensions []portainer.Extension
|
||||
err = json.Unmarshal(extensionData, &extensions)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external extension definitions", err}
|
||||
}
|
||||
|
||||
var extension portainer.Extension
|
||||
for _, p := range extensions {
|
||||
if p.ID == extensionID {
|
||||
extension = p
|
||||
if extension.DescriptionURL != "" {
|
||||
description, _ := client.Get(extension.DescriptionURL, 10)
|
||||
extension.Description = string(description)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
storedExtension, err := handler.ExtensionService.Extension(extensionID)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return response.JSON(w, extension)
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
extension.Enabled = storedExtension.Enabled
|
||||
|
||||
extensionVer := semver.New(extension.Version)
|
||||
pVer := semver.New(storedExtension.Version)
|
||||
|
||||
if pVer.LessThan(*extensionVer) {
|
||||
extension.UpdateAvailable = true
|
||||
}
|
||||
|
||||
return response.JSON(w, extension)
|
||||
}
|
||||
56
api/http/handler/extensions/extension_list.go
Normal file
56
api/http/handler/extensions/extension_list.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// GET request on /api/extensions?store=<store>
|
||||
func (handler *Handler) extensionList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
storeDetails, _ := request.RetrieveBooleanQueryParameter(r, "store", true)
|
||||
|
||||
extensions, err := handler.ExtensionService.Extensions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions from the database", err}
|
||||
}
|
||||
|
||||
if storeDetails {
|
||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions", err}
|
||||
}
|
||||
|
||||
for idx := range definitions {
|
||||
associateExtensionData(&definitions[idx], extensions)
|
||||
}
|
||||
|
||||
extensions = definitions
|
||||
}
|
||||
|
||||
return response.JSON(w, extensions)
|
||||
}
|
||||
|
||||
func associateExtensionData(definition *portainer.Extension, extensions []portainer.Extension) {
|
||||
for _, extension := range extensions {
|
||||
if extension.ID == definition.ID {
|
||||
|
||||
definition.Enabled = extension.Enabled
|
||||
definition.License.Company = extension.License.Company
|
||||
definition.License.Expiration = extension.License.Expiration
|
||||
definition.License.Valid = extension.License.Valid
|
||||
|
||||
definitionVersion := semver.New(definition.Version)
|
||||
extensionVersion := semver.New(extension.Version)
|
||||
if extensionVersion.LessThan(*definitionVersion) {
|
||||
definition.UpdateAvailable = true
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
56
api/http/handler/extensions/extension_update.go
Normal file
56
api/http/handler/extensions/extension_update.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package extensions
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type extensionUpdatePayload struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
func (payload *extensionUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Version) {
|
||||
return portainer.Error("Invalid extension version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) extensionUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
var payload extensionUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
extension, err := handler.ExtensionService.Extension(extensionID)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.ExtensionManager.UpdateExtension(extension, payload.Version)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update extension", err}
|
||||
}
|
||||
|
||||
err = handler.ExtensionService.Persist(extension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
37
api/http/handler/extensions/handler.go
Normal file
37
api/http/handler/extensions/handler.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle extension operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
ExtensionService portainer.ExtensionService
|
||||
ExtensionManager portainer.ExtensionManager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage extension operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
h.Handle("/extensions",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||
h.Handle("/extensions",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/extensions/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/extensions/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/extensions/{id}/update",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
@@ -5,15 +5,16 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/http/handler/auth"
|
||||
"github.com/portainer/portainer/http/handler/deploykeys"
|
||||
"github.com/portainer/portainer/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/http/handler/extensions"
|
||||
"github.com/portainer/portainer/http/handler/file"
|
||||
"github.com/portainer/portainer/http/handler/motd"
|
||||
"github.com/portainer/portainer/http/handler/registries"
|
||||
"github.com/portainer/portainer/http/handler/resourcecontrols"
|
||||
"github.com/portainer/portainer/http/handler/schedules"
|
||||
"github.com/portainer/portainer/http/handler/settings"
|
||||
"github.com/portainer/portainer/http/handler/stacks"
|
||||
"github.com/portainer/portainer/http/handler/status"
|
||||
@@ -37,6 +38,7 @@ type Handler struct {
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
FileHandler *file.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
ExtensionHandler *extensions.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
SettingsHandler *settings.Handler
|
||||
@@ -50,7 +52,7 @@ type Handler struct {
|
||||
UserHandler *users.Handler
|
||||
WebSocketHandler *websocket.Handler
|
||||
WebhookHandler *webhooks.Handler
|
||||
DeploykeyHandler *deploykeys.Handler
|
||||
SchedulesHanlder *schedules.Handler
|
||||
}
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
@@ -75,6 +77,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
||||
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/extensions"):
|
||||
http.StripPrefix("/api", h.ExtensionHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/resource_controls"):
|
||||
@@ -89,8 +93,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/deploykeys"):
|
||||
http.StripPrefix("/api", h.DeploykeyHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
||||
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/users"):
|
||||
@@ -103,6 +105,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/webhooks"):
|
||||
http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/schedules"):
|
||||
http.StripPrefix("/api", h.SchedulesHanlder).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/"):
|
||||
h.FileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type motdResponse struct {
|
||||
Title string `json:"Title"`
|
||||
Message string `json:"Message"`
|
||||
Hash []byte `json:"Hash"`
|
||||
}
|
||||
@@ -18,10 +19,16 @@ func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
motd, err := client.Get(portainer.MessageOfTheDayURL, 0)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
response.JSON(w, &motdResponse{Message: ""})
|
||||
return
|
||||
}
|
||||
|
||||
title, err := client.Get(portainer.MessageOfTheDayTitleURL, 0)
|
||||
if err != nil {
|
||||
response.JSON(w, &motdResponse{Message: ""})
|
||||
return
|
||||
}
|
||||
|
||||
hash := crypto.HashFromBytes(motd)
|
||||
response.JSON(w, &motdResponse{Message: string(motd), Hash: hash})
|
||||
response.JSON(w, &motdResponse{Title: string(title), Message: string(motd), Hash: hash})
|
||||
}
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
func hideFields(registry *portainer.Registry) {
|
||||
registry.Password = ""
|
||||
registry.ManagementConfiguration = nil
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle registry operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
RegistryService portainer.RegistryService
|
||||
requestBouncer *security.RequestBouncer
|
||||
RegistryService portainer.RegistryService
|
||||
ExtensionService portainer.ExtensionService
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage registry operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
}
|
||||
|
||||
h.Handle("/registries",
|
||||
@@ -31,13 +37,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h.Handle("/registries",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/registries/{id}/access",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdateAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/registries/{id}/configure",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||
h.PathPrefix("/registries/{id}/v2").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
83
api/http/handler/registries/proxy.go
Normal file
83
api/http/handler/registries/proxy.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// request on /api/registries/:id/v2
|
||||
func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
|
||||
}
|
||||
|
||||
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register registry proxy", err}
|
||||
}
|
||||
}
|
||||
|
||||
managementConfiguration := registry.ManagementConfiguration
|
||||
if managementConfiguration == nil {
|
||||
managementConfiguration = createDefaultManagementConfiguration(registry)
|
||||
}
|
||||
|
||||
encodedConfiguration, err := json.Marshal(managementConfiguration)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err}
|
||||
}
|
||||
|
||||
id := strconv.Itoa(int(registryID))
|
||||
r.Header.Set("X-RegistryManagement-Key", id)
|
||||
r.Header.Set("X-RegistryManagement-URI", registry.URL)
|
||||
r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration))
|
||||
r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey)
|
||||
|
||||
http.StripPrefix("/registries/"+id, proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDefaultManagementConfiguration(registry *portainer.Registry) *portainer.RegistryManagementConfiguration {
|
||||
config := &portainer.RegistryManagementConfiguration{
|
||||
Type: registry.Type,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
}
|
||||
|
||||
if registry.Authentication {
|
||||
config.Authentication = true
|
||||
config.Username = registry.Username
|
||||
config.Password = registry.Password
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
137
api/http/handler/registries/registry_configure.go
Normal file
137
api/http/handler/registries/registry_configure.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type registryConfigurePayload struct {
|
||||
Authentication bool
|
||||
Username string
|
||||
Password string
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
TLSCertFile []byte
|
||||
TLSKeyFile []byte
|
||||
TLSCACertFile []byte
|
||||
}
|
||||
|
||||
func (payload *registryConfigurePayload) Validate(r *http.Request) error {
|
||||
useAuthentication, _ := request.RetrieveBooleanMultiPartFormValue(r, "Authentication", true)
|
||||
payload.Authentication = useAuthentication
|
||||
|
||||
if useAuthentication {
|
||||
username, err := request.RetrieveMultiPartFormValue(r, "Username", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid username")
|
||||
}
|
||||
payload.Username = username
|
||||
|
||||
password, _ := request.RetrieveMultiPartFormValue(r, "Password", true)
|
||||
payload.Password = password
|
||||
}
|
||||
|
||||
useTLS, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLS", true)
|
||||
payload.TLS = useTLS
|
||||
|
||||
skipTLSVerify, _ := request.RetrieveBooleanMultiPartFormValue(r, "TLSSkipVerify", true)
|
||||
payload.TLSSkipVerify = skipTLSVerify
|
||||
|
||||
if useTLS && !skipTLSVerify {
|
||||
cert, _, err := request.RetrieveMultiPartFormFile(r, "TLSCertFile")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid certificate file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSCertFile = cert
|
||||
|
||||
key, _, err := request.RetrieveMultiPartFormFile(r, "TLSKeyFile")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid key file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSKeyFile = key
|
||||
|
||||
ca, _, err := request.RetrieveMultiPartFormFile(r, "TLSCACertFile")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid CA certificate file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.TLSCACertFile = ca
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST request on /api/registries/:id/configure
|
||||
func (handler *Handler) registryConfigure(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
}
|
||||
|
||||
payload := ®istryConfigurePayload{}
|
||||
err = payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
registry.ManagementConfiguration = &portainer.RegistryManagementConfiguration{
|
||||
Type: registry.Type,
|
||||
}
|
||||
|
||||
if payload.Authentication {
|
||||
registry.ManagementConfiguration.Authentication = true
|
||||
registry.ManagementConfiguration.Username = payload.Username
|
||||
if payload.Username == registry.Username && payload.Password == "" {
|
||||
registry.ManagementConfiguration.Password = registry.Password
|
||||
} else {
|
||||
registry.ManagementConfiguration.Password = payload.Password
|
||||
}
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
registry.ManagementConfiguration.TLSConfig = portainer.TLSConfiguration{
|
||||
TLS: true,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
}
|
||||
|
||||
if !payload.TLSSkipVerify {
|
||||
folder := strconv.Itoa(int(registry.ID))
|
||||
|
||||
certPath, err := handler.FileService.StoreRegistryManagementFileFromBytes(folder, "cert.pem", payload.TLSCertFile)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS certificate file on disk", err}
|
||||
}
|
||||
registry.ManagementConfiguration.TLSConfig.TLSCertPath = certPath
|
||||
|
||||
keyPath, err := handler.FileService.StoreRegistryManagementFileFromBytes(folder, "key.pem", payload.TLSKeyFile)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS key file on disk", err}
|
||||
}
|
||||
registry.ManagementConfiguration.TLSConfig.TLSKeyPath = keyPath
|
||||
|
||||
cacertPath, err := handler.FileService.StoreRegistryManagementFileFromBytes(folder, "ca.pem", payload.TLSCACertFile)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist TLS CA certificate file on disk", err}
|
||||
}
|
||||
registry.ManagementConfiguration.TLSConfig.TLSCACertPath = cacertPath
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
type registryCreatePayload struct {
|
||||
Name string
|
||||
Type int
|
||||
URL string
|
||||
Authentication bool
|
||||
Username string
|
||||
@@ -28,6 +29,9 @@ func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
||||
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if payload.Type != 1 && payload.Type != 2 && payload.Type != 3 {
|
||||
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,6 +53,7 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.RegistryType(payload.Type),
|
||||
Name: payload.Name,
|
||||
URL: payload.URL,
|
||||
Authentication: payload.Authentication,
|
||||
|
||||
@@ -23,6 +23,11 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
|
||||
}
|
||||
|
||||
hideFields(registry)
|
||||
return response.JSON(w, registry)
|
||||
}
|
||||
|
||||
44
api/http/handler/schedules/handler.go
Normal file
44
api/http/handler/schedules/handler.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle schedule operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
ScheduleService portainer.ScheduleService
|
||||
EndpointService portainer.EndpointService
|
||||
SettingsService portainer.SettingsService
|
||||
FileService portainer.FileService
|
||||
JobService portainer.JobService
|
||||
JobScheduler portainer.JobScheduler
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage schedule operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
h.Handle("/schedules",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet)
|
||||
h.Handle("/schedules",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/schedules/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/schedules/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/schedules/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/schedules/{id}/file",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet)
|
||||
h.Handle("/schedules/{id}/tasks",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
238
api/http/handler/schedules/schedule_create.go
Normal file
238
api/http/handler/schedules/schedule_create.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"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"
|
||||
"github.com/portainer/portainer/cron"
|
||||
)
|
||||
|
||||
type scheduleCreateFromFilePayload struct {
|
||||
Name string
|
||||
Image string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Endpoints []portainer.EndpointID
|
||||
File []byte
|
||||
RetryCount int
|
||||
RetryInterval int
|
||||
}
|
||||
|
||||
type scheduleCreateFromFileContentPayload struct {
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Image string
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent string
|
||||
RetryCount int
|
||||
RetryInterval int
|
||||
}
|
||||
|
||||
func (payload *scheduleCreateFromFilePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid schedule name")
|
||||
}
|
||||
|
||||
if !govalidator.Matches(name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("Invalid schedule name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
image, err := request.RetrieveMultiPartFormValue(r, "Image", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid schedule image")
|
||||
}
|
||||
payload.Image = image
|
||||
|
||||
cronExpression, err := request.RetrieveMultiPartFormValue(r, "CronExpression", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid cron expression")
|
||||
}
|
||||
payload.CronExpression = cronExpression
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid endpoints")
|
||||
}
|
||||
payload.Endpoints = endpoints
|
||||
|
||||
file, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid script file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.File = file
|
||||
|
||||
retryCount, _ := request.RetrieveNumericMultiPartFormValue(r, "RetryCount", true)
|
||||
payload.RetryCount = retryCount
|
||||
|
||||
retryInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "RetryInterval", true)
|
||||
payload.RetryInterval = retryInterval
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid schedule name")
|
||||
}
|
||||
|
||||
if !govalidator.Matches(payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("Invalid schedule name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.Image) {
|
||||
return portainer.Error("Invalid schedule image")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.CronExpression) {
|
||||
return portainer.Error("Invalid cron expression")
|
||||
}
|
||||
|
||||
if payload.Endpoints == nil || len(payload.Endpoints) == 0 {
|
||||
return portainer.Error("Invalid endpoints payload")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.FileContent) {
|
||||
return portainer.Error("Invalid script file content")
|
||||
}
|
||||
|
||||
if payload.RetryCount != 0 && payload.RetryInterval == 0 {
|
||||
return portainer.Error("RetryInterval must be set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// POST /api/schedules?method=file/string
|
||||
func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
if !settings.EnableHostManagementFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method. Valid values are: file or string", err}
|
||||
}
|
||||
|
||||
switch method {
|
||||
case "string":
|
||||
return handler.createScheduleFromFileContent(w, r)
|
||||
case "file":
|
||||
return handler.createScheduleFromFile(w, r)
|
||||
default:
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: method. Valid values are: file or string", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload scheduleCreateFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
schedule := handler.createScheduleObjectFromFileContentPayload(&payload)
|
||||
|
||||
err = handler.addAndPersistSchedule(schedule, []byte(payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule script job", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleFromFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
payload := &scheduleCreateFromFilePayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
schedule := handler.createScheduleObjectFromFilePayload(payload)
|
||||
|
||||
err = handler.addAndPersistSchedule(schedule, payload.File)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to schedule script job", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCreateFromFilePayload) *portainer.Schedule {
|
||||
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
||||
|
||||
job := &portainer.ScriptExecutionJob{
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval,
|
||||
}
|
||||
|
||||
schedule := &portainer.Schedule{
|
||||
ID: scheduleIdentifier,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Recurring: payload.Recurring,
|
||||
JobType: portainer.ScriptExecutionJobType,
|
||||
ScriptExecutionJob: job,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return schedule
|
||||
}
|
||||
|
||||
func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *scheduleCreateFromFileContentPayload) *portainer.Schedule {
|
||||
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
||||
|
||||
job := &portainer.ScriptExecutionJob{
|
||||
Endpoints: payload.Endpoints,
|
||||
Image: payload.Image,
|
||||
RetryCount: payload.RetryCount,
|
||||
RetryInterval: payload.RetryInterval,
|
||||
}
|
||||
|
||||
schedule := &portainer.Schedule{
|
||||
ID: scheduleIdentifier,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Recurring: payload.Recurring,
|
||||
JobType: portainer.ScriptExecutionJobType,
|
||||
ScriptExecutionJob: job,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return schedule
|
||||
}
|
||||
|
||||
func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error {
|
||||
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schedule.ScriptExecutionJob.ScriptPath = scriptPath
|
||||
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
||||
|
||||
err = handler.JobScheduler.ScheduleJob(jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handler.ScheduleService.CreateSchedule(schedule)
|
||||
}
|
||||
53
api/http/handler/schedules/schedule_delete.go
Normal file
53
api/http/handler/schedules/schedule_delete.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
if !settings.EnableHostManagementFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if schedule.JobType == portainer.SnapshotJobType || schedule.JobType == portainer.EndpointSyncJobType {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Cannot remove system schedules", errors.New("Cannot remove system schedule")}
|
||||
}
|
||||
|
||||
scheduleFolder := handler.FileService.GetScheduleFolder(strconv.Itoa(scheduleID))
|
||||
err = handler.FileService.RemoveDirectory(scheduleFolder)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err}
|
||||
}
|
||||
|
||||
handler.JobScheduler.UnscheduleJob(schedule.ID)
|
||||
|
||||
err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the schedule from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
49
api/http/handler/schedules/schedule_file.go
Normal file
49
api/http/handler/schedules/schedule_file.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type scheduleFileResponse struct {
|
||||
ScheduleFileContent string `json:"ScheduleFileContent"`
|
||||
}
|
||||
|
||||
// GET request on /api/schedules/:id/file
|
||||
func (handler *Handler) scheduleFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
if !settings.EnableHostManagementFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if schedule.JobType != portainer.ScriptExecutionJobType {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Unable to retrieve script file", errors.New("This type of schedule do not have any associated script file")}
|
||||
}
|
||||
|
||||
scheduleFileContent, err := handler.FileService.GetFileContent(schedule.ScriptExecutionJob.ScriptPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve schedule script file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &scheduleFileResponse{ScheduleFileContent: string(scheduleFileContent)})
|
||||
}
|
||||
35
api/http/handler/schedules/schedule_inspect.go
Normal file
35
api/http/handler/schedules/schedule_inspect.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
)
|
||||
|
||||
func (handler *Handler) scheduleInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
if !settings.EnableHostManagementFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
27
api/http/handler/schedules/schedule_list.go
Normal file
27
api/http/handler/schedules/schedule_list.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// GET request on /api/schedules
|
||||
func (handler *Handler) scheduleList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
if !settings.EnableHostManagementFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
schedules, err := handler.ScheduleService.Schedules()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve schedules from the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedules)
|
||||
}
|
||||
95
api/http/handler/schedules/schedule_tasks.go
Normal file
95
api/http/handler/schedules/schedule_tasks.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type taskContainer struct {
|
||||
ID string `json:"Id"`
|
||||
EndpointID portainer.EndpointID `json:"EndpointId"`
|
||||
Status string `json:"Status"`
|
||||
Created float64 `json:"Created"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
}
|
||||
|
||||
// GET request on /api/schedules/:id/tasks
|
||||
func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
if !settings.EnableHostManagementFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if schedule.JobType != portainer.ScriptExecutionJobType {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Unable to retrieve schedule tasks", errors.New("This type of schedule do not have any associated tasks")}
|
||||
}
|
||||
|
||||
tasks := make([]taskContainer, 0)
|
||||
|
||||
for _, endpointID := range schedule.ScriptExecutionJob.Endpoints {
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpointTasks, err := extractTasksFromContainerSnasphot(endpoint, schedule.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find extract schedule tasks from endpoint snapshot", err}
|
||||
}
|
||||
|
||||
tasks = append(tasks, endpointTasks...)
|
||||
}
|
||||
|
||||
return response.JSON(w, tasks)
|
||||
}
|
||||
|
||||
func extractTasksFromContainerSnasphot(endpoint *portainer.Endpoint, scheduleID portainer.ScheduleID) ([]taskContainer, error) {
|
||||
endpointTasks := make([]taskContainer, 0)
|
||||
if len(endpoint.Snapshots) == 0 {
|
||||
return endpointTasks, nil
|
||||
}
|
||||
|
||||
b, err := json.Marshal(endpoint.Snapshots[0].SnapshotRaw.Containers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers []taskContainer
|
||||
err = json.Unmarshal(b, &containers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
if container.Labels["io.portainer.schedule.id"] == strconv.Itoa(int(scheduleID)) {
|
||||
container.EndpointID = endpoint.ID
|
||||
endpointTasks = append(endpointTasks, container)
|
||||
}
|
||||
}
|
||||
|
||||
return endpointTasks, nil
|
||||
}
|
||||
126
api/http/handler/schedules/schedule_update.go
Normal file
126
api/http/handler/schedules/schedule_update.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package schedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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"
|
||||
"github.com/portainer/portainer/cron"
|
||||
)
|
||||
|
||||
type scheduleUpdatePayload struct {
|
||||
Name *string
|
||||
Image *string
|
||||
CronExpression *string
|
||||
Recurring *bool
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent *string
|
||||
RetryCount *int
|
||||
RetryInterval *int
|
||||
}
|
||||
|
||||
func (payload *scheduleUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.Name != nil && !govalidator.Matches(*payload.Name, `^[a-zA-Z0-9][a-zA-Z0-9_.-]+$`) {
|
||||
return errors.New("Invalid schedule name format. Allowed characters are: [a-zA-Z0-9_.-]")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to retrieve settings", err}
|
||||
}
|
||||
if !settings.EnableHostManagementFeatures {
|
||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Host management features are disabled", portainer.ErrHostManagementFeaturesDisabled}
|
||||
}
|
||||
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload scheduleUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
updateJobSchedule := updateSchedule(schedule, &payload)
|
||||
|
||||
if payload.FileContent != nil {
|
||||
_, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(scheduleID), []byte(*payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist script file changes on the filesystem", err}
|
||||
}
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if updateJobSchedule {
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule, jobContext)
|
||||
err := handler.JobScheduler.UpdateJobSchedule(jobRunner)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update job scheduler", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.ScheduleService.UpdateSchedule(portainer.ScheduleID(scheduleID), schedule)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist schedule changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, schedule)
|
||||
}
|
||||
|
||||
func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) bool {
|
||||
updateJobSchedule := false
|
||||
|
||||
if payload.Name != nil {
|
||||
schedule.Name = *payload.Name
|
||||
}
|
||||
|
||||
if payload.Endpoints != nil {
|
||||
schedule.ScriptExecutionJob.Endpoints = payload.Endpoints
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.CronExpression != nil {
|
||||
schedule.CronExpression = *payload.CronExpression
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.Recurring != nil {
|
||||
schedule.Recurring = *payload.Recurring
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.Image != nil {
|
||||
schedule.ScriptExecutionJob.Image = *payload.Image
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.RetryCount != nil {
|
||||
schedule.ScriptExecutionJob.RetryCount = *payload.RetryCount
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if payload.RetryInterval != nil {
|
||||
schedule.ScriptExecutionJob.RetryInterval = *payload.RetryInterval
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
return updateJobSchedule
|
||||
}
|
||||
@@ -9,6 +9,11 @@ import (
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
func hideFields(settings *portainer.Settings) {
|
||||
settings.LDAPSettings.Password = ""
|
||||
settings.OAuthSettings.ClientSecret = ""
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle settings operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
@@ -16,6 +21,7 @@ type Handler struct {
|
||||
LDAPService portainer.LDAPService
|
||||
FileService portainer.FileService
|
||||
JobScheduler portainer.JobScheduler
|
||||
ScheduleService portainer.ScheduleService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
|
||||
@@ -14,5 +14,6 @@ func (handler *Handler) settingsInspect(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
hideFields(settings)
|
||||
return response.JSON(w, settings)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
@@ -13,7 +14,9 @@ type publicSettingsResponse struct {
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
ExternalTemplates bool `json:"ExternalTemplates"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
}
|
||||
|
||||
// GET request on /api/settings/public
|
||||
@@ -28,7 +31,13 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||
ExternalTemplates: false,
|
||||
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||
settings.OAuthSettings.AuthorizationURI,
|
||||
settings.OAuthSettings.ClientID,
|
||||
settings.OAuthSettings.RedirectURI,
|
||||
settings.OAuthSettings.Scopes),
|
||||
}
|
||||
|
||||
if settings.TemplatesURL != "" {
|
||||
|
||||
@@ -16,15 +16,17 @@ type settingsUpdatePayload struct {
|
||||
BlackListedLabels []portainer.Pair
|
||||
AuthenticationMethod *int
|
||||
LDAPSettings *portainer.LDAPSettings
|
||||
OAuthSettings *portainer.OAuthSettings
|
||||
AllowBindMountsForRegularUsers *bool
|
||||
AllowPrivilegedModeForRegularUsers *bool
|
||||
EnableHostManagementFeatures *bool
|
||||
SnapshotInterval *string
|
||||
TemplatesURL *string
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 {
|
||||
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal) or 2 (LDAP/AD)")
|
||||
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 {
|
||||
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)")
|
||||
}
|
||||
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
|
||||
return portainer.Error("Invalid logo URL. Must correspond to a valid URL format")
|
||||
@@ -65,7 +67,21 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
if payload.LDAPSettings != nil {
|
||||
ldapPassword := settings.LDAPSettings.Password
|
||||
if payload.LDAPSettings.Password != "" {
|
||||
ldapPassword = payload.LDAPSettings.Password
|
||||
}
|
||||
settings.LDAPSettings = *payload.LDAPSettings
|
||||
settings.LDAPSettings.Password = ldapPassword
|
||||
}
|
||||
|
||||
if payload.OAuthSettings != nil {
|
||||
clientSecret := payload.OAuthSettings.ClientSecret
|
||||
if clientSecret == "" {
|
||||
clientSecret = settings.OAuthSettings.ClientSecret
|
||||
}
|
||||
settings.OAuthSettings = *payload.OAuthSettings
|
||||
settings.OAuthSettings.ClientSecret = clientSecret
|
||||
}
|
||||
|
||||
if payload.AllowBindMountsForRegularUsers != nil {
|
||||
@@ -76,9 +92,15 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
if payload.EnableHostManagementFeatures != nil {
|
||||
settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||
}
|
||||
|
||||
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
||||
settings.SnapshotInterval = *payload.SnapshotInterval
|
||||
handler.JobScheduler.UpdateSnapshotJob(settings.SnapshotInterval)
|
||||
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update snapshot interval", err}
|
||||
}
|
||||
}
|
||||
|
||||
tlsError := handler.updateTLS(settings)
|
||||
@@ -94,6 +116,32 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
return response.JSON(w, settings)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error {
|
||||
settings.SnapshotInterval = snapshotInterval
|
||||
|
||||
schedules, err := handler.ScheduleService.SchedulesByJobType(portainer.SnapshotJobType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(schedules) != 0 {
|
||||
snapshotSchedule := schedules[0]
|
||||
snapshotSchedule.CronExpression = "@every " + snapshotInterval
|
||||
|
||||
err := handler.JobScheduler.UpdateSystemJobSchedule(portainer.SnapshotJobType, snapshotSchedule.CronExpression)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.ScheduleService.UpdateSchedule(snapshotSchedule.ID, &snapshotSchedule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateTLS(settings *portainer.Settings) *httperror.HandlerError {
|
||||
if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
|
||||
|
||||
@@ -41,6 +41,10 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http
|
||||
}
|
||||
|
||||
func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
|
||||
if user.Password == "" {
|
||||
return handler.deleteUser(w, user)
|
||||
}
|
||||
|
||||
users, err := handler.UserService.Users()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *portainer.Endpoint, resourceID string) *httperror.HandlerError {
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint)
|
||||
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Error creating docker client", err}
|
||||
}
|
||||
|
||||
@@ -111,12 +111,13 @@ func (handler *Handler) proxyWebsocketRequest(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
signature, err := handler.SignatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
signature, err := handler.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy.Director = func(incoming *http.Request, out http.Header) {
|
||||
out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey())
|
||||
out.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func buildOperation(request *http.Request) error {
|
||||
dockerfileContent = []byte(req.Content)
|
||||
}
|
||||
|
||||
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile")
|
||||
buffer, err := archive.TarFileInBuffer(dockerfileContent, "Dockerfile", 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
|
||||
request.URL.Path = path
|
||||
|
||||
if p.enableSignature {
|
||||
signature, err := p.SignatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||
signature, err := p.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type proxyFactory struct {
|
||||
|
||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return newSingleHostReverseProxyWithHostHeader(u)
|
||||
return httputil.NewSingleHostReverseProxy(u)
|
||||
}
|
||||
|
||||
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
|
||||
|
||||
@@ -3,18 +3,26 @@ package proxy
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"strconv"
|
||||
|
||||
"github.com/orcaman/concurrent-map"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// TODO: contain code related to legacy extension management
|
||||
|
||||
var extensionPorts = map[portainer.ExtensionID]string{
|
||||
portainer.RegistryManagementExtension: "7001",
|
||||
portainer.OAuthAuthenticationExtension: "7002",
|
||||
}
|
||||
|
||||
type (
|
||||
// Manager represents a service used to manage Docker proxies.
|
||||
Manager struct {
|
||||
proxyFactory *proxyFactory
|
||||
proxies cmap.ConcurrentMap
|
||||
extensionProxies cmap.ConcurrentMap
|
||||
proxyFactory *proxyFactory
|
||||
proxies cmap.ConcurrentMap
|
||||
extensionProxies cmap.ConcurrentMap
|
||||
legacyExtensionProxies cmap.ConcurrentMap
|
||||
}
|
||||
|
||||
// ManagerParams represents the required parameters to create a new Manager instance.
|
||||
@@ -31,8 +39,9 @@ type (
|
||||
// NewManager initializes a new proxy Service
|
||||
func NewManager(parameters *ManagerParams) *Manager {
|
||||
return &Manager{
|
||||
proxies: cmap.New(),
|
||||
extensionProxies: cmap.New(),
|
||||
proxies: cmap.New(),
|
||||
extensionProxies: cmap.New(),
|
||||
legacyExtensionProxies: cmap.New(),
|
||||
proxyFactory: &proxyFactory{
|
||||
ResourceControlService: parameters.ResourceControlService,
|
||||
TeamMembershipService: parameters.TeamMembershipService,
|
||||
@@ -44,6 +53,88 @@ func NewManager(parameters *ManagerParams) *Manager {
|
||||
}
|
||||
}
|
||||
|
||||
// GetProxy returns the proxy associated to a key
|
||||
func (manager *Manager) GetProxy(key string) http.Handler {
|
||||
proxy, ok := manager.proxies.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// CreateAndRegisterProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
||||
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
proxy, err := manager.createProxy(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.proxies.Set(string(endpoint.ID), proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// DeleteProxy deletes the proxy associated to a key
|
||||
func (manager *Manager) DeleteProxy(key string) {
|
||||
manager.proxies.Remove(key)
|
||||
}
|
||||
|
||||
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
||||
func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) http.Handler {
|
||||
proxy, ok := manager.extensionProxies.Get(strconv.Itoa(int(extensionID)))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and
|
||||
// registers it in the extension map associated to the specified extension identifier
|
||||
func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
||||
address := "http://localhost:" + extensionPorts[extensionID]
|
||||
|
||||
extensionURL, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
||||
manager.extensionProxies.Set(strconv.Itoa(int(extensionID)), proxy)
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table
|
||||
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
|
||||
return "http://localhost:" + extensionPorts[extensionID]
|
||||
}
|
||||
|
||||
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
|
||||
func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID) {
|
||||
manager.extensionProxies.Remove(strconv.Itoa(int(extensionID)))
|
||||
}
|
||||
|
||||
// GetLegacyExtensionProxy returns a legacy extension proxy associated to a key
|
||||
func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
||||
proxy, ok := manager.legacyExtensionProxies.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies.
|
||||
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||
extensionURL, err := url.Parse(extensionAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
||||
manager.extensionProxies.Set(key, proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration) (http.Handler, error) {
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
|
||||
@@ -69,59 +160,3 @@ func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler,
|
||||
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAndRegisterProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
||||
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
proxy, err := manager.createProxy(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.proxies.Set(string(endpoint.ID), proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetProxy returns the proxy associated to a key
|
||||
func (manager *Manager) GetProxy(key string) http.Handler {
|
||||
proxy, ok := manager.proxies.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// DeleteProxy deletes the proxy associated to a key
|
||||
func (manager *Manager) DeleteProxy(key string) {
|
||||
manager.proxies.Remove(key)
|
||||
}
|
||||
|
||||
// CreateAndRegisterExtensionProxy creates a new HTTP reverse proxy for an extension and adds it to the registered proxies.
|
||||
func (manager *Manager) CreateAndRegisterExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||
extensionURL, err := url.Parse(extensionAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
||||
manager.extensionProxies.Set(key, proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetExtensionProxy returns the extension proxy associated to a key
|
||||
func (manager *Manager) GetExtensionProxy(key string) http.Handler {
|
||||
proxy, ok := manager.extensionProxies.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// DeleteExtensionProxies deletes all the extension proxies associated to a key
|
||||
func (manager *Manager) DeleteExtensionProxies(key string) {
|
||||
for _, k := range manager.extensionProxies.Keys() {
|
||||
if strings.Contains(k, key+"_") {
|
||||
manager.extensionProxies.Remove(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,10 +153,10 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta
|
||||
return true
|
||||
}
|
||||
|
||||
// AuthorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
|
||||
// authorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
|
||||
// It will check if the user is part of the authorized users or part of a team that is
|
||||
// listed in the authorized teams.
|
||||
func AuthorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,31 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegistryAccess retrieves the JWT token from the request context and verifies
|
||||
// that the user can access the specified registry.
|
||||
// An error is returned when access is denied.
|
||||
func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portainer.Registry) error {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return nil
|
||||
}
|
||||
|
||||
memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !AuthorizedRegistryAccess(registry, tokenData.ID, memberships) {
|
||||
return portainer.ErrEndpointAccessDenied
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -124,7 +124,7 @@ func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *Res
|
||||
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||
|
||||
for _, group := range endpointGroups {
|
||||
if AuthorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
|
||||
if authorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
|
||||
filteredEndpointGroups = append(filteredEndpointGroups, group)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,16 @@ import (
|
||||
"github.com/portainer/portainer/docker"
|
||||
"github.com/portainer/portainer/http/handler"
|
||||
"github.com/portainer/portainer/http/handler/auth"
|
||||
"github.com/portainer/portainer/http/handler/deploykeys"
|
||||
"github.com/portainer/portainer/http/handler/dockerhub"
|
||||
"github.com/portainer/portainer/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/http/handler/extensions"
|
||||
"github.com/portainer/portainer/http/handler/file"
|
||||
"github.com/portainer/portainer/http/handler/motd"
|
||||
"github.com/portainer/portainer/http/handler/registries"
|
||||
"github.com/portainer/portainer/http/handler/resourcecontrols"
|
||||
"github.com/portainer/portainer/http/handler/schedules"
|
||||
"github.com/portainer/portainer/http/handler/settings"
|
||||
"github.com/portainer/portainer/http/handler/stacks"
|
||||
"github.com/portainer/portainer/http/handler/status"
|
||||
@@ -41,6 +42,7 @@ type Server struct {
|
||||
AuthDisabled bool
|
||||
EndpointManagement bool
|
||||
Status *portainer.Status
|
||||
ExtensionManager portainer.ExtensionManager
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
CryptoService portainer.CryptoService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
@@ -49,13 +51,14 @@ type Server struct {
|
||||
DockerHubService portainer.DockerHubService
|
||||
EndpointService portainer.EndpointService
|
||||
EndpointGroupService portainer.EndpointGroupService
|
||||
DigitalDeploykeyService portainer.DigitalDeploykeyService
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
ExtensionService portainer.ExtensionService
|
||||
RegistryService portainer.RegistryService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
ScheduleService portainer.ScheduleService
|
||||
SettingsService portainer.SettingsService
|
||||
StackService portainer.StackService
|
||||
SwarmStackManager portainer.SwarmStackManager
|
||||
@@ -64,13 +67,13 @@ type Server struct {
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
TemplateService portainer.TemplateService
|
||||
UserService portainer.UserService
|
||||
DeploykeyService portainer.DeploykeyService
|
||||
WebhookService portainer.WebhookService
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
SSLCert string
|
||||
SSLKey string
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
JobService portainer.JobService
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
@@ -83,6 +86,7 @@ func (server *Server) Start() error {
|
||||
AuthDisabled: server.AuthDisabled,
|
||||
}
|
||||
requestBouncer := security.NewRequestBouncer(requestBouncerParameters)
|
||||
|
||||
proxyManagerParameters := &proxy.ManagerParams{
|
||||
ResourceControlService: server.ResourceControlService,
|
||||
TeamMembershipService: server.TeamMembershipService,
|
||||
@@ -92,6 +96,7 @@ func (server *Server) Start() error {
|
||||
SignatureService: server.SignatureService,
|
||||
}
|
||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
var authHandler = auth.NewHandler(requestBouncer, rateLimiter, server.AuthDisabled)
|
||||
@@ -102,6 +107,8 @@ func (server *Server) Start() error {
|
||||
authHandler.SettingsService = server.SettingsService
|
||||
authHandler.TeamService = server.TeamService
|
||||
authHandler.TeamMembershipService = server.TeamMembershipService
|
||||
authHandler.ExtensionService = server.ExtensionService
|
||||
authHandler.ProxyManager = proxyManager
|
||||
|
||||
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
|
||||
dockerHubHandler.DockerHubService = server.DockerHubService
|
||||
@@ -112,6 +119,7 @@ func (server *Server) Start() error {
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
endpointHandler.Snapshotter = server.Snapshotter
|
||||
endpointHandler.JobService = server.JobService
|
||||
|
||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
||||
@@ -125,17 +133,33 @@ func (server *Server) Start() error {
|
||||
|
||||
var motdHandler = motd.NewHandler(requestBouncer)
|
||||
|
||||
var extensionHandler = extensions.NewHandler(requestBouncer)
|
||||
extensionHandler.ExtensionService = server.ExtensionService
|
||||
extensionHandler.ExtensionManager = server.ExtensionManager
|
||||
|
||||
var registryHandler = registries.NewHandler(requestBouncer)
|
||||
registryHandler.RegistryService = server.RegistryService
|
||||
registryHandler.ExtensionService = server.ExtensionService
|
||||
registryHandler.FileService = server.FileService
|
||||
registryHandler.ProxyManager = proxyManager
|
||||
|
||||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||
resourceControlHandler.ResourceControlService = server.ResourceControlService
|
||||
|
||||
var schedulesHandler = schedules.NewHandler(requestBouncer)
|
||||
schedulesHandler.ScheduleService = server.ScheduleService
|
||||
schedulesHandler.EndpointService = server.EndpointService
|
||||
schedulesHandler.FileService = server.FileService
|
||||
schedulesHandler.JobService = server.JobService
|
||||
schedulesHandler.JobScheduler = server.JobScheduler
|
||||
schedulesHandler.SettingsService = server.SettingsService
|
||||
|
||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||
settingsHandler.SettingsService = server.SettingsService
|
||||
settingsHandler.LDAPService = server.LDAPService
|
||||
settingsHandler.FileService = server.FileService
|
||||
settingsHandler.JobScheduler = server.JobScheduler
|
||||
settingsHandler.ScheduleService = server.ScheduleService
|
||||
|
||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||
stackHandler.FileService = server.FileService
|
||||
@@ -155,10 +179,6 @@ func (server *Server) Start() error {
|
||||
teamHandler.TeamService = server.TeamService
|
||||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||
|
||||
var deploykeyHandler = deploykeys.NewHandler(requestBouncer)
|
||||
deploykeyHandler.DeploykeyService = server.DeploykeyService
|
||||
deploykeyHandler.DigitalDeploykeyService = server.DigitalDeploykeyService
|
||||
|
||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
@@ -195,6 +215,7 @@ func (server *Server) Start() error {
|
||||
EndpointProxyHandler: endpointProxyHandler,
|
||||
FileHandler: fileHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
ExtensionHandler: extensionHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
@@ -208,7 +229,7 @@ func (server *Server) Start() error {
|
||||
UserHandler: userHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
WebhookHandler: webhookHandler,
|
||||
DeploykeyHandler: deploykeyHandler,
|
||||
SchedulesHanlder: schedulesHandler,
|
||||
}
|
||||
|
||||
if server.SSL {
|
||||
|
||||
281
api/portainer.go
281
api/portainer.go
@@ -47,7 +47,7 @@ type (
|
||||
// LDAPSettings represents the settings used to connect to a LDAP server
|
||||
LDAPSettings struct {
|
||||
ReaderDN string `json:"ReaderDN"`
|
||||
Password string `json:"Password"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
URL string `json:"URL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
StartTLS bool `json:"StartTLS"`
|
||||
@@ -56,6 +56,20 @@ type (
|
||||
AutoCreateUsers bool `json:"AutoCreateUsers"`
|
||||
}
|
||||
|
||||
// OAuthSettings represents the settings used to authorize with an authorization server
|
||||
OAuthSettings struct {
|
||||
ClientID string `json:"ClientID"`
|
||||
ClientSecret string `json:"ClientSecret,omitempty"`
|
||||
AccessTokenURI string `json:"AccessTokenURI"`
|
||||
AuthorizationURI string `json:"AuthorizationURI"`
|
||||
ResourceURI string `json:"ResourceURI"`
|
||||
RedirectURI string `json:"RedirectURI"`
|
||||
UserIdentifier string `json:"UserIdentifier"`
|
||||
Scopes string `json:"Scopes"`
|
||||
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
|
||||
DefaultTeamID TeamID `json:"DefaultTeamID"`
|
||||
}
|
||||
|
||||
// TLSConfiguration represents a TLS configuration
|
||||
TLSConfiguration struct {
|
||||
TLS bool `json:"TLS"`
|
||||
@@ -85,10 +99,12 @@ type (
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
|
||||
// Deprecated fields
|
||||
DisplayDonationHeader bool
|
||||
@@ -164,17 +180,32 @@ type (
|
||||
// RegistryID represents a registry identifier
|
||||
RegistryID int
|
||||
|
||||
// RegistryType represents a type of registry
|
||||
RegistryType int
|
||||
|
||||
// Registry represents a Docker registry with all the info required
|
||||
// to connect to it
|
||||
Registry struct {
|
||||
ID RegistryID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
ID RegistryID `json:"Id"`
|
||||
Type RegistryType `json:"Type"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
||||
}
|
||||
|
||||
// RegistryManagementConfiguration represents a configuration that can be used to query
|
||||
// the registry API via the registry management extension.
|
||||
RegistryManagementConfiguration struct {
|
||||
Type RegistryType `json:"Type"`
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
}
|
||||
|
||||
// DockerHub represents all the required information to connect and use the
|
||||
@@ -220,7 +251,44 @@ type (
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookID represents an webhook identifier.
|
||||
// ScheduleID represents a schedule identifier.
|
||||
ScheduleID int
|
||||
|
||||
// JobType represents a job type
|
||||
JobType int
|
||||
|
||||
// ScriptExecutionJob represents a scheduled job that can execute a script via a privileged container
|
||||
ScriptExecutionJob struct {
|
||||
Endpoints []EndpointID
|
||||
Image string
|
||||
ScriptPath string
|
||||
RetryCount int
|
||||
RetryInterval int
|
||||
}
|
||||
|
||||
// SnapshotJob represents a scheduled job that can create endpoint snapshots
|
||||
SnapshotJob struct{}
|
||||
|
||||
// EndpointSyncJob represents a scheduled job that synchronize endpoints based on an external file
|
||||
EndpointSyncJob struct{}
|
||||
|
||||
// Schedule represents a scheduled job.
|
||||
// It only contains a pointer to one of the JobRunner implementations
|
||||
// based on the JobType.
|
||||
// NOTE: The Recurring option is only used by ScriptExecutionJob at the moment
|
||||
Schedule struct {
|
||||
ID ScheduleID `json:"Id"`
|
||||
Name string
|
||||
CronExpression string
|
||||
Recurring bool
|
||||
Created int64
|
||||
JobType JobType
|
||||
ScriptExecutionJob *ScriptExecutionJob
|
||||
SnapshotJob *SnapshotJob
|
||||
EndpointSyncJob *EndpointSyncJob
|
||||
}
|
||||
|
||||
// WebhookID represents a webhook identifier.
|
||||
WebhookID int
|
||||
|
||||
// WebhookType represents the type of resource a webhook is related to
|
||||
@@ -245,17 +313,28 @@ type (
|
||||
|
||||
// Snapshot represents a snapshot of a specific endpoint at a specific time
|
||||
Snapshot struct {
|
||||
Time int64 `json:"Time"`
|
||||
DockerVersion string `json:"DockerVersion"`
|
||||
Swarm bool `json:"Swarm"`
|
||||
TotalCPU int `json:"TotalCPU"`
|
||||
TotalMemory int64 `json:"TotalMemory"`
|
||||
RunningContainerCount int `json:"RunningContainerCount"`
|
||||
StoppedContainerCount int `json:"StoppedContainerCount"`
|
||||
VolumeCount int `json:"VolumeCount"`
|
||||
ImageCount int `json:"ImageCount"`
|
||||
ServiceCount int `json:"ServiceCount"`
|
||||
StackCount int `json:"StackCount"`
|
||||
Time int64 `json:"Time"`
|
||||
DockerVersion string `json:"DockerVersion"`
|
||||
Swarm bool `json:"Swarm"`
|
||||
TotalCPU int `json:"TotalCPU"`
|
||||
TotalMemory int64 `json:"TotalMemory"`
|
||||
RunningContainerCount int `json:"RunningContainerCount"`
|
||||
StoppedContainerCount int `json:"StoppedContainerCount"`
|
||||
VolumeCount int `json:"VolumeCount"`
|
||||
ImageCount int `json:"ImageCount"`
|
||||
ServiceCount int `json:"ServiceCount"`
|
||||
StackCount int `json:"StackCount"`
|
||||
SnapshotRaw SnapshotRaw `json:"SnapshotRaw"`
|
||||
}
|
||||
|
||||
// SnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
||||
SnapshotRaw struct {
|
||||
Containers interface{} `json:"Containers"`
|
||||
Volumes interface{} `json:"Volumes"`
|
||||
Networks interface{} `json:"Networks"`
|
||||
Images interface{} `json:"Images"`
|
||||
Info interface{} `json:"Info"`
|
||||
Version interface{} `json:"Version"`
|
||||
}
|
||||
|
||||
// EndpointGroupID represents an endpoint group identifier
|
||||
@@ -274,7 +353,8 @@ type (
|
||||
Labels []Pair `json:"Labels"`
|
||||
}
|
||||
|
||||
// EndpointExtension represents a extension associated to an endpoint
|
||||
// EndpointExtension represents a deprecated form of Portainer extension
|
||||
// TODO: legacy extension management
|
||||
EndpointExtension struct {
|
||||
Type EndpointExtensionType `json:"Type"`
|
||||
URL string `json:"URL"`
|
||||
@@ -321,19 +401,6 @@ type (
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
||||
}
|
||||
|
||||
// DeploykeyID represents a key identifier
|
||||
DeploykeyID int
|
||||
|
||||
// Deploykey represents a key that can be associated to a resource
|
||||
Deploykey struct {
|
||||
ID DeploykeyID
|
||||
Name string `json:"Name"`
|
||||
Privatekeypath string `json:"Privatekeypath"`
|
||||
Publickeypath string `json:"Publickeypath"`
|
||||
UserID int `json:"UserID"`
|
||||
LastUsage string `json:"LastUsage"`
|
||||
}
|
||||
|
||||
// TagID represents a tag identifier
|
||||
TagID int
|
||||
|
||||
@@ -423,6 +490,37 @@ type (
|
||||
// It can be either a TLS CA file, a TLS certificate file or a TLS key file
|
||||
TLSFileType int
|
||||
|
||||
// ExtensionID represents a extension identifier
|
||||
ExtensionID int
|
||||
|
||||
// Extension represents a Portainer extension
|
||||
Extension struct {
|
||||
ID ExtensionID `json:"Id"`
|
||||
Enabled bool `json:"Enabled"`
|
||||
Name string `json:"Name,omitempty"`
|
||||
ShortDescription string `json:"ShortDescription,omitempty"`
|
||||
Description string `json:"Description,omitempty"`
|
||||
DescriptionURL string `json:"DescriptionURL,omitempty"`
|
||||
Price string `json:"Price,omitempty"`
|
||||
PriceDescription string `json:"PriceDescription,omitempty"`
|
||||
Deal bool `json:"Deal,omitempty"`
|
||||
Available bool `json:"Available,omitempty"`
|
||||
License LicenseInformation `json:"License,omitempty"`
|
||||
Version string `json:"Version"`
|
||||
UpdateAvailable bool `json:"UpdateAvailable"`
|
||||
ShopURL string `json:"ShopURL,omitempty"`
|
||||
Images []string `json:"Images,omitempty"`
|
||||
Logo string `json:"Logo,omitempty"`
|
||||
}
|
||||
|
||||
// LicenseInformation represents information about an extension license
|
||||
LicenseInformation struct {
|
||||
LicenseKey string `json:"LicenseKey,omitempty"`
|
||||
Company string `json:"Company,omitempty"`
|
||||
Expiration string `json:"Expiration,omitempty"`
|
||||
Valid bool `json:"Valid,omitempty"`
|
||||
}
|
||||
|
||||
// CLIService represents a service for managing CLI
|
||||
CLIService interface {
|
||||
ParseFlags(version string) (*CLIFlags, error)
|
||||
@@ -463,13 +561,6 @@ type (
|
||||
DeleteTeam(ID TeamID) error
|
||||
}
|
||||
|
||||
// DeploykeyService represents a service for managing key data
|
||||
DeploykeyService interface {
|
||||
Deploykeys() ([]Deploykey, error)
|
||||
CreateDeploykey(deploykey *Deploykey) error
|
||||
DeleteDeploykey(ID DeploykeyID) error
|
||||
}
|
||||
|
||||
// TeamMembershipService represents a service for managing team membership data
|
||||
TeamMembershipService interface {
|
||||
TeamMembership(ID TeamMembershipID) (*TeamMembership, error)
|
||||
@@ -561,6 +652,17 @@ type (
|
||||
DeleteResourceControl(ID ResourceControlID) error
|
||||
}
|
||||
|
||||
// ScheduleService represents a service for managing schedule data
|
||||
ScheduleService interface {
|
||||
Schedule(ID ScheduleID) (*Schedule, error)
|
||||
Schedules() ([]Schedule, error)
|
||||
SchedulesByJobType(jobType JobType) ([]Schedule, error)
|
||||
CreateSchedule(schedule *Schedule) error
|
||||
UpdateSchedule(ID ScheduleID, schedule *Schedule) error
|
||||
DeleteSchedule(ID ScheduleID) error
|
||||
GetNextIdentifier() int
|
||||
}
|
||||
|
||||
// TagService represents a service for managing tag data
|
||||
TagService interface {
|
||||
Tags() ([]Tag, error)
|
||||
@@ -577,6 +679,14 @@ type (
|
||||
DeleteTemplate(ID TemplateID) error
|
||||
}
|
||||
|
||||
// ExtensionService represents a service for managing extension data
|
||||
ExtensionService interface {
|
||||
Extension(ID ExtensionID) (*Extension, error)
|
||||
Extensions() ([]Extension, error)
|
||||
Persist(extension *Extension) error
|
||||
DeleteExtension(ID ExtensionID) error
|
||||
}
|
||||
|
||||
// CryptoService represents a service for encrypting/hashing data
|
||||
CryptoService interface {
|
||||
Hash(data string) (string, error)
|
||||
@@ -589,19 +699,9 @@ type (
|
||||
GenerateKeyPair() ([]byte, []byte, error)
|
||||
EncodedPublicKey() string
|
||||
PEMHeaders() (string, string)
|
||||
Sign(message string) (string, error)
|
||||
CreateSignature(message string) (string, error)
|
||||
}
|
||||
|
||||
//DigitalDeploykeyService represents a service to manage digital deploykey
|
||||
DigitalDeploykeyService interface {
|
||||
ParseKeyPair(private, public []byte) error
|
||||
GenerateKeyPair() ([]byte, []byte, error)
|
||||
GenerateSshKey()([]byte, error)
|
||||
EncodedPublicKey() string
|
||||
PEMHeaders() (string, string)
|
||||
Sign(message string) (string, error)
|
||||
}
|
||||
|
||||
// JWTService represents a service for managing JWT tokens
|
||||
JWTService interface {
|
||||
GenerateToken(data *TokenData) (string, error)
|
||||
@@ -619,11 +719,16 @@ type (
|
||||
DeleteTLSFiles(folder string) error
|
||||
GetStackProjectPath(stackIdentifier string) string
|
||||
StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error)
|
||||
StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error)
|
||||
KeyPairFilesExist() (bool, error)
|
||||
StoreKeyPair(private, public []byte, privatePEMHeader, publicPEMHeader string) error
|
||||
LoadKeyPair() ([]byte, []byte, error)
|
||||
WriteJSONToFile(path string, content interface{}) error
|
||||
FileExists(path string) (bool, error)
|
||||
StoreScheduledJobFileFromBytes(identifier string, data []byte) (string, error)
|
||||
GetScheduleFolder(identifier string) string
|
||||
ExtractExtensionArchive(data []byte) error
|
||||
GetBinaryFolder() string
|
||||
}
|
||||
|
||||
// GitService represents a service for managing Git
|
||||
@@ -634,12 +739,19 @@ type (
|
||||
|
||||
// JobScheduler represents a service to run jobs on a periodic basis
|
||||
JobScheduler interface {
|
||||
ScheduleEndpointSyncJob(endpointFilePath, interval string) error
|
||||
ScheduleSnapshotJob(interval string) error
|
||||
UpdateSnapshotJob(interval string)
|
||||
ScheduleJob(runner JobRunner) error
|
||||
UpdateJobSchedule(runner JobRunner) error
|
||||
UpdateSystemJobSchedule(jobType JobType, newCronExpression string) error
|
||||
UnscheduleJob(ID ScheduleID)
|
||||
Start()
|
||||
}
|
||||
|
||||
// JobRunner represents a service that can be used to run a job
|
||||
JobRunner interface {
|
||||
Run()
|
||||
GetSchedule() *Schedule
|
||||
}
|
||||
|
||||
// Snapshotter represents a service used to create endpoint snapshots
|
||||
Snapshotter interface {
|
||||
CreateSnapshot(endpoint *Endpoint) (*Snapshot, error)
|
||||
@@ -665,15 +777,34 @@ type (
|
||||
Up(stack *Stack, endpoint *Endpoint) error
|
||||
Down(stack *Stack, endpoint *Endpoint) error
|
||||
}
|
||||
|
||||
// JobService represents a service to manage job execution on hosts
|
||||
JobService interface {
|
||||
ExecuteScript(endpoint *Endpoint, nodeName, image string, script []byte, schedule *Schedule) error
|
||||
}
|
||||
|
||||
// ExtensionManager represents a service used to manage extensions
|
||||
ExtensionManager interface {
|
||||
FetchExtensionDefinitions() ([]Extension, error)
|
||||
EnableExtension(extension *Extension, licenseKey string) error
|
||||
DisableExtension(extension *Extension) error
|
||||
UpdateExtension(extension *Extension, version string) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "1.20-dev"
|
||||
APIVersion = "1.20.2"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 14
|
||||
DBVersion = 17
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||
MessageOfTheDayURL = "https://raw.githubusercontent.com/portainer/motd/master/message.html"
|
||||
MessageOfTheDayURL = AssetsServerURL + "/motd.html"
|
||||
// MessageOfTheDayTitleURL represents the URL where Portainer MOTD title can be retrieved
|
||||
MessageOfTheDayTitleURL = AssetsServerURL + "/motd-title.txt"
|
||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.20.2.json"
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
PortainerAgentHeader = "Portainer-Agent"
|
||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
||||
@@ -720,6 +851,8 @@ const (
|
||||
AuthenticationInternal
|
||||
// AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server)
|
||||
AuthenticationLDAP
|
||||
//AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server)
|
||||
AuthenticationOAuth
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -793,3 +926,33 @@ const (
|
||||
// ServiceWebhook is a webhook for restarting a docker service
|
||||
ServiceWebhook
|
||||
)
|
||||
|
||||
const (
|
||||
_ ExtensionID = iota
|
||||
// RegistryManagementExtension represents the registry management extension
|
||||
RegistryManagementExtension
|
||||
// OAuthAuthenticationExtension represents the OAuth authentication extension
|
||||
OAuthAuthenticationExtension
|
||||
)
|
||||
|
||||
const (
|
||||
_ JobType = iota
|
||||
// ScriptExecutionJobType is a non-system job used to execute a script against a list of
|
||||
// endpoints via privileged containers
|
||||
ScriptExecutionJobType
|
||||
// SnapshotJobType is a system job used to create endpoint snapshots
|
||||
SnapshotJobType
|
||||
// EndpointSyncJobType is a system job used to synchronize endpoints from
|
||||
// an external definition store
|
||||
EndpointSyncJobType
|
||||
)
|
||||
|
||||
const (
|
||||
_ RegistryType = iota
|
||||
// QuayRegistry represents a Quay.io registry
|
||||
QuayRegistry
|
||||
// AzureRegistry represents an ACR registry
|
||||
AzureRegistry
|
||||
// CustomRegistry represents a custom registry
|
||||
CustomRegistry
|
||||
)
|
||||
|
||||
408
api/swagger.yaml
408
api/swagger.yaml
@@ -54,15 +54,13 @@ info:
|
||||
|
||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||
|
||||
version: "1.20-dev"
|
||||
version: "1.20.2"
|
||||
title: "Portainer API"
|
||||
contact:
|
||||
email: "info@portainer.io"
|
||||
host: "portainer.domain"
|
||||
basePath: "/api"
|
||||
tags:
|
||||
- name: "deploykeys"
|
||||
description: "Manage deploykeys"
|
||||
- name: "auth"
|
||||
description: "Authenticate against Portainer HTTP API"
|
||||
- name: "dockerhub"
|
||||
@@ -155,6 +153,8 @@ paths:
|
||||
operationId: "DockerHubInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -177,6 +177,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -213,6 +215,8 @@ paths:
|
||||
operationId: "EndpointList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -235,6 +239,8 @@ paths:
|
||||
- "multipart/form-data"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "Name"
|
||||
in: "formData"
|
||||
@@ -267,7 +273,11 @@ paths:
|
||||
- name: "TLSSkipVerify"
|
||||
in: "formData"
|
||||
type: "string"
|
||||
description: "Skip server verification when using TLS" (example: false)
|
||||
description: "Skip server verification when using TLS (example: false)"
|
||||
- name: "TLSSkipClientVerify"
|
||||
in: "formData"
|
||||
type: "string"
|
||||
description: "Skip client verification when using TLS (example: false)"
|
||||
- name: "TLSCACertFile"
|
||||
in: "formData"
|
||||
type: "file"
|
||||
@@ -326,6 +336,8 @@ paths:
|
||||
operationId: "EndpointInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -367,6 +379,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -415,6 +429,8 @@ paths:
|
||||
Remove an endpoint.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -462,6 +478,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -497,6 +515,78 @@ paths:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/endpoints/{id}/job:
|
||||
post:
|
||||
tags:
|
||||
- "endpoints"
|
||||
summary: "Execute a job on the endpoint host"
|
||||
description: |
|
||||
Execute a job (script) on the underlying host of the endpoint.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointJob"
|
||||
consumes:
|
||||
- "multipart/form-data"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Endpoint identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
- name: "method"
|
||||
in: "query"
|
||||
description: "Job execution method. Possible values: file or string."
|
||||
required: true
|
||||
type: "string"
|
||||
- name: "nodeName"
|
||||
in: "query"
|
||||
description: "Optional. Hostname of a node when targeting a Portainer agent cluster."
|
||||
required: true
|
||||
type: "string"
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Job details. Required when method equals string."
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/EndpointJobRequest"
|
||||
- name: "Image"
|
||||
in: "formData"
|
||||
type: "string"
|
||||
description: "Container image which will be used to execute the job. Required when method equals file."
|
||||
- name: "file"
|
||||
in: "formData"
|
||||
type: "file"
|
||||
description: "Job script file. Required when method equals file."
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/Endpoint"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
404:
|
||||
description: "Endpoint not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Endpoint not found"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/endpoint_groups:
|
||||
get:
|
||||
tags:
|
||||
@@ -510,6 +600,8 @@ paths:
|
||||
operationId: "EndpointGroupList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -532,6 +624,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -566,6 +660,8 @@ paths:
|
||||
operationId: "EndpointGroupInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -607,6 +703,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -657,6 +755,8 @@ paths:
|
||||
Remove an endpoint group.
|
||||
**Access policy**: administrator
|
||||
operationId: "EndpointGroupDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -704,6 +804,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -752,6 +854,8 @@ paths:
|
||||
operationId: "RegistryList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -774,6 +878,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -815,6 +921,8 @@ paths:
|
||||
operationId: "RegistryInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -856,6 +964,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -906,6 +1016,8 @@ paths:
|
||||
Remove a registry.
|
||||
**Access policy**: administrator
|
||||
operationId: "RegistryDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -946,6 +1058,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -994,6 +1108,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -1044,6 +1160,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1094,6 +1212,8 @@ paths:
|
||||
Remove a resource control.
|
||||
**Access policy**: restricted
|
||||
operationId: "ResourceControlDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1139,6 +1259,8 @@ paths:
|
||||
operationId: "SettingsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -1161,6 +1283,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -1195,6 +1319,8 @@ paths:
|
||||
operationId: "PublicSettingsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -1218,6 +1344,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -1250,6 +1378,8 @@ paths:
|
||||
operationId: "StatusInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -1273,6 +1403,8 @@ paths:
|
||||
operationId: "StackList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "filters"
|
||||
in: "query"
|
||||
@@ -1302,9 +1434,11 @@ paths:
|
||||
**Access policy**: restricted
|
||||
operationId: "StackCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
- "multipart/form-data"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "type"
|
||||
in: "query"
|
||||
@@ -1384,6 +1518,8 @@ paths:
|
||||
operationId: "StackInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1429,6 +1565,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1481,6 +1619,8 @@ paths:
|
||||
Remove a stack.
|
||||
**Access policy**: restricted
|
||||
operationId: "StackDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1531,6 +1671,8 @@ paths:
|
||||
operationId: "StackFileInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1576,6 +1718,8 @@ paths:
|
||||
operationId: "StackMigrate"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1630,6 +1774,8 @@ paths:
|
||||
operationId: "UserList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -1653,6 +1799,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -1701,6 +1849,8 @@ paths:
|
||||
operationId: "UserInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1742,6 +1892,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1792,6 +1944,8 @@ paths:
|
||||
Remove a user.
|
||||
**Access policy**: administrator
|
||||
operationId: "UserDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1830,6 +1984,8 @@ paths:
|
||||
operationId: "UserMembershipsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1873,6 +2029,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -1920,6 +2078,8 @@ paths:
|
||||
operationId: "UserAdminCheck"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
204:
|
||||
@@ -1949,6 +2109,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -1993,6 +2155,8 @@ paths:
|
||||
- multipart/form-data
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "path"
|
||||
name: "certificate"
|
||||
@@ -2034,6 +2198,8 @@ paths:
|
||||
operationId: "TagList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -2056,6 +2222,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -2095,6 +2263,8 @@ paths:
|
||||
Remove a tag.
|
||||
**Access policy**: administrator
|
||||
operationId: "TagDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2127,6 +2297,8 @@ paths:
|
||||
operationId: "TeamList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -2149,6 +2321,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -2197,6 +2371,8 @@ paths:
|
||||
operationId: "TeamInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2245,6 +2421,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2286,6 +2464,8 @@ paths:
|
||||
Remove a team.
|
||||
**Access policy**: administrator
|
||||
operationId: "TeamDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2313,105 +2493,6 @@ paths:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/deploykeys:
|
||||
get:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "List deploykeys"
|
||||
description: |
|
||||
List deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeysList"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/DeploykeyListResponse"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
404:
|
||||
description: "Key not found"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
post:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "Create a new deploykeys"
|
||||
description: |
|
||||
Create a new deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeyCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Deploykey details"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/DeploykeyCreateRequest"
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
schema:
|
||||
$ref: "#/definitions/Deploykey"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request data format"
|
||||
409:
|
||||
description: "Conflict"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "A deploykeys with the specified name already exists"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
/deploykeys/{id}:
|
||||
delete:
|
||||
tags:
|
||||
- "deploykeys"
|
||||
summary: "Remove a deploykeys"
|
||||
description: |
|
||||
Remove a deploykeys.
|
||||
**Access policy**: administrator
|
||||
operationId: "DeploykeyDelete"
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
description: "Deploykey identifier"
|
||||
required: true
|
||||
type: "integer"
|
||||
responses:
|
||||
204:
|
||||
description: "Success"
|
||||
400:
|
||||
description: "Invalid request"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
examples:
|
||||
application/json:
|
||||
err: "Invalid request"
|
||||
500:
|
||||
description: "Server error"
|
||||
schema:
|
||||
$ref: "#/definitions/GenericError"
|
||||
|
||||
/teams/{id}/memberships:
|
||||
get:
|
||||
@@ -2424,6 +2505,8 @@ paths:
|
||||
operationId: "TeamMembershipsInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2465,6 +2548,8 @@ paths:
|
||||
operationId: "TeamMembershipList"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
@@ -2494,6 +2579,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -2544,6 +2631,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2594,6 +2683,8 @@ paths:
|
||||
Remove a team membership. Access is only available to administrators leaders of the associated team.
|
||||
**Access policy**: restricted
|
||||
operationId: "TeamMembershipDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2640,7 +2731,9 @@ paths:
|
||||
operationId: "TemplateList"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
security:
|
||||
- jwt: []
|
||||
parameters: []
|
||||
responses:
|
||||
200:
|
||||
description: "Success"
|
||||
@@ -2662,6 +2755,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
@@ -2703,6 +2798,8 @@ paths:
|
||||
operationId: "TemplateInspect"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2751,6 +2848,8 @@ paths:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2799,6 +2898,8 @@ paths:
|
||||
Remove a template.
|
||||
**Access policy**: administrator
|
||||
operationId: "TemplateDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
parameters:
|
||||
- name: "id"
|
||||
in: "path"
|
||||
@@ -2836,35 +2937,6 @@ definitions:
|
||||
type: "string"
|
||||
example: "org/acme"
|
||||
description: "Tag name"
|
||||
|
||||
Deploykey:
|
||||
type: "object"
|
||||
properties:
|
||||
Id:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey identifier"
|
||||
Name:
|
||||
type: "string"
|
||||
example: "abcd"
|
||||
description: "Deploykey name"
|
||||
Privatekeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey private key path"
|
||||
Publickeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey public key path"
|
||||
UserID:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey user id"
|
||||
LastUsage:
|
||||
type: "string"
|
||||
example: "12:03:32"
|
||||
description: " last usage deploykey date"
|
||||
|
||||
Team:
|
||||
type: "object"
|
||||
properties:
|
||||
@@ -2946,7 +3018,7 @@ definitions:
|
||||
description: "Is analytics enabled"
|
||||
Version:
|
||||
type: "string"
|
||||
example: "1.20-dev"
|
||||
example: "1.20.2"
|
||||
description: "Portainer API version"
|
||||
PublicSettingsInspectResponse:
|
||||
type: "object"
|
||||
@@ -3074,7 +3146,7 @@ definitions:
|
||||
$ref: "#/definitions/LDAPGroupSearchSettings"
|
||||
AutoCreateUsers:
|
||||
type: "boolean"
|
||||
example: "true"
|
||||
example: true
|
||||
description: "Automatically provision users and assign them to matching LDAP group names"
|
||||
|
||||
Settings:
|
||||
@@ -3534,6 +3606,7 @@ definitions:
|
||||
- "Authentication"
|
||||
- "Name"
|
||||
- "Password"
|
||||
- "Type"
|
||||
- "URL"
|
||||
- "Username"
|
||||
properties:
|
||||
@@ -3541,6 +3614,10 @@ definitions:
|
||||
type: "string"
|
||||
example: "my-registry"
|
||||
description: "Name that will be used to identify this registry"
|
||||
Type:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)"
|
||||
URL:
|
||||
type: "string"
|
||||
example: "registry.mydomain.tld:2375"
|
||||
@@ -3847,37 +3924,6 @@ definitions:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/TeamMembership"
|
||||
DeploykeyListResponse:
|
||||
type: "array"
|
||||
items:
|
||||
$ref: "#/definitions/Deploykey"
|
||||
|
||||
DeploykeyCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "Name"
|
||||
properties:
|
||||
Name:
|
||||
type: "string"
|
||||
example: "abcd"
|
||||
description: "Deploykey name"
|
||||
Privatekeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey private key path"
|
||||
Publickeypath:
|
||||
type: "string"
|
||||
example: "abc@1dd45%"
|
||||
description: "Deploykey public key path"
|
||||
UserID:
|
||||
type: "integer"
|
||||
example: 1
|
||||
description: "Deploykey user id"
|
||||
LastUsage:
|
||||
type: "string"
|
||||
example: "12:03:32"
|
||||
description: "last usage deploykey date"
|
||||
|
||||
|
||||
TeamMembershipCreateRequest:
|
||||
type: "object"
|
||||
@@ -3944,9 +3990,9 @@ definitions:
|
||||
TemplateCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "type"
|
||||
- "title"
|
||||
- "description"
|
||||
- "type"
|
||||
- "title"
|
||||
- "description"
|
||||
properties:
|
||||
type:
|
||||
type: "integer"
|
||||
@@ -3996,7 +4042,7 @@ definitions:
|
||||
description: "A list of categories associated to the template"
|
||||
items:
|
||||
type: "string"
|
||||
exampe: "database"
|
||||
example: "database"
|
||||
registry:
|
||||
type: "string"
|
||||
example: "quay.io"
|
||||
@@ -4092,7 +4138,7 @@ definitions:
|
||||
description: "A list of categories associated to the template"
|
||||
items:
|
||||
type: "string"
|
||||
exampe: "database"
|
||||
example: "database"
|
||||
registry:
|
||||
type: "string"
|
||||
example: "quay.io"
|
||||
@@ -4192,7 +4238,7 @@ definitions:
|
||||
description: "A list of categories associated to the template"
|
||||
items:
|
||||
type: "string"
|
||||
exampe: "database"
|
||||
example: "database"
|
||||
registry:
|
||||
type: "string"
|
||||
example: "quay.io"
|
||||
@@ -4298,7 +4344,7 @@ definitions:
|
||||
TemplateRepository:
|
||||
type: "object"
|
||||
required:
|
||||
- "URL"
|
||||
- "URL"
|
||||
properties:
|
||||
URL:
|
||||
type: "string"
|
||||
@@ -4325,6 +4371,20 @@ definitions:
|
||||
type: "string"
|
||||
example: "new-stack"
|
||||
description: "If provided will rename the migrated stack"
|
||||
EndpointJobRequest:
|
||||
type: "object"
|
||||
required:
|
||||
- "Image"
|
||||
- "FileContent"
|
||||
properties:
|
||||
Image:
|
||||
type: "string"
|
||||
example: "ubuntu:latest"
|
||||
description: "Container image which will be used to execute the job"
|
||||
FileContent:
|
||||
type: "string"
|
||||
example: "ls -lah /host/tmp"
|
||||
description: "Content of the job script"
|
||||
StackCreateRequest:
|
||||
type: "object"
|
||||
required:
|
||||
|
||||
5
api/swagger_config.json
Normal file
5
api/swagger_config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"packageName": "portainer",
|
||||
"packageVersion": "1.20.2",
|
||||
"projectName": "portainer"
|
||||
}
|
||||
@@ -1,3 +1,12 @@
|
||||
import '../assets/css/app.css';
|
||||
import angular from 'angular';
|
||||
|
||||
import './agent/_module';
|
||||
import './azure/_module';
|
||||
import './docker/__module';
|
||||
import './extensions/storidge/__module';
|
||||
import './portainer/__module';
|
||||
|
||||
angular.module('portainer', [
|
||||
'ui.bootstrap',
|
||||
'ui.router',
|
||||
@@ -17,10 +26,19 @@ angular.module('portainer', [
|
||||
'angular-clipboard',
|
||||
'ngFileSaver',
|
||||
'luegg.directives',
|
||||
'portainer.templates',
|
||||
'portainer.app',
|
||||
'portainer.agent',
|
||||
'portainer.azure',
|
||||
'portainer.docker',
|
||||
'portainer.extensions',
|
||||
'extension.storidge',
|
||||
'rzModule']);
|
||||
'rzModule',
|
||||
'moment-picker'
|
||||
]);
|
||||
|
||||
if (require) {
|
||||
var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im);
|
||||
req.keys().forEach(function(key) {
|
||||
req(key);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('portainer.agent').component('fileUploader', {
|
||||
templateUrl: 'app/agent/components/file-uploader/file-uploader.html',
|
||||
templateUrl: './file-uploader.html',
|
||||
controller: 'FileUploaderController',
|
||||
bindings: {
|
||||
uploadFile: '<onFileSelected'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter"
|
||||
placeholder="Search..." auto-focus>
|
||||
ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('portainer.agent').component('filesDatatable', {
|
||||
templateUrl: 'app/agent/components/files-datatable/files-datatable.html',
|
||||
templateUrl: './files-datatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.agent').controller('HostBrowserController', [
|
||||
'HostBrowserService', 'Notifications', 'FileSaver', 'ModalService',
|
||||
function HostBrowserController(HostBrowserService, Notifications, FileSaver, ModalService) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('portainer.agent').component('hostBrowser', {
|
||||
controller: 'HostBrowserController',
|
||||
templateUrl: 'app/agent/components/host-browser/host-browser.html',
|
||||
templateUrl: './host-browser.html',
|
||||
bindings: {}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('portainer.agent').component('nodeSelector', {
|
||||
templateUrl: 'app/agent/components/node-selector/nodeSelector.html',
|
||||
templateUrl: './nodeSelector.html',
|
||||
controller: 'NodeSelectorController',
|
||||
bindings: {
|
||||
model: '='
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
angular.module('portainer.agent').component('volumeBrowser', {
|
||||
templateUrl: 'app/agent/components/volume-browser/volumeBrowser.html',
|
||||
templateUrl: './volumeBrowser.html',
|
||||
controller: 'VolumeBrowserController',
|
||||
bindings: {
|
||||
volumeId: '<',
|
||||
nodeName: '<'
|
||||
nodeName: '<',
|
||||
isUploadEnabled: '<'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -8,4 +8,7 @@
|
||||
rename="$ctrl.rename(name, newName)"
|
||||
download="$ctrl.download(name)"
|
||||
delete="$ctrl.delete(name)"
|
||||
|
||||
is-upload-allowed="$ctrl.isUploadEnabled"
|
||||
on-file-selected-for-upload="$ctrl.onFileSelectedForUpload"
|
||||
></files-datatable>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.agent')
|
||||
.controller('VolumeBrowserController', ['HttpRequestHelper', 'VolumeBrowserService', 'FileSaver', 'Blob', 'ModalService', 'Notifications',
|
||||
function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) {
|
||||
@@ -84,6 +86,16 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
|
||||
});
|
||||
}
|
||||
|
||||
this.onFileSelectedForUpload = function onFileSelectedForUpload(file) {
|
||||
VolumeBrowserService.upload(ctrl.state.path, file, ctrl.volumeId)
|
||||
.then(function onFileUpload() {
|
||||
onFileUploaded();
|
||||
})
|
||||
.catch(function onFileUpload(err) {
|
||||
Notifications.error('Failure', err, 'Unable to upload file');
|
||||
});
|
||||
};
|
||||
|
||||
function parentPath(path) {
|
||||
if (path.lastIndexOf('/') === 0) {
|
||||
return '/';
|
||||
@@ -112,4 +124,14 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
|
||||
});
|
||||
};
|
||||
|
||||
function onFileUploaded() {
|
||||
refreshList();
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
browse(ctrl.state.path);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function AgentViewModel(data) {
|
||||
export function AgentViewModel(data) {
|
||||
this.IPAddress = data.IPAddress;
|
||||
this.NodeName = data.NodeName;
|
||||
this.NodeRole = data.NodeRole;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
angular.module('portainer.agent')
|
||||
.factory('Agent', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) {
|
||||
.factory('Agent', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager',
|
||||
function AgentFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/agents', {
|
||||
endpointId: EndpointProvider.endpointID
|
||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:endpointId/docker/v:version/agents', {
|
||||
endpointId: EndpointProvider.endpointID,
|
||||
version: StateManager.getAgentApiVersion
|
||||
},
|
||||
{
|
||||
query: { method: 'GET', isArray: true }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user