Compare commits
435 Commits
feat/fedex
...
feat/EE-49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb815df8f4 | ||
|
|
96de026eba | ||
|
|
d340c4ea96 | ||
|
|
9567072ce0 | ||
|
|
d18b276e30 | ||
|
|
af77e33993 | ||
|
|
fdd79cece8 | ||
|
|
ac94d344df | ||
|
|
bcbdb01785 | ||
|
|
a2f734051c | ||
|
|
93866644c6 | ||
|
|
6242952141 | ||
|
|
b4dd5c5989 | ||
|
|
ef00350922 | ||
|
|
8acea44ee8 | ||
|
|
c193360741 | ||
|
|
4f34a78f7f | ||
|
|
f96e7ff434 | ||
|
|
e37e87971d | ||
|
|
5daef54456 | ||
|
|
db93e5880f | ||
|
|
881fa01eb2 | ||
|
|
14fa60f6e6 | ||
|
|
b58cd1e87e | ||
|
|
395d86dcd1 | ||
|
|
dbd476008b | ||
|
|
5a04338087 | ||
|
|
dc5f866a24 | ||
|
|
83551201fb | ||
|
|
e156243e43 | ||
|
|
1473cc208b | ||
|
|
d29b688eb9 | ||
|
|
077046030d | ||
|
|
5f3c0ff835 | ||
|
|
23e3cdb193 | ||
|
|
e6984c5787 | ||
|
|
0743f26ab8 | ||
|
|
8fa49d47f4 | ||
|
|
6ef53f0598 | ||
|
|
365316971b | ||
|
|
511adabce2 | ||
|
|
5b96136dd2 | ||
|
|
42fce1ec57 | ||
|
|
22f4c5d650 | ||
|
|
945798a662 | ||
|
|
6a29198c5c | ||
|
|
7197ca435a | ||
|
|
c3c2221437 | ||
|
|
d8fcce4c31 | ||
|
|
c86b76261a | ||
|
|
acc340b324 | ||
|
|
e0609e3d93 | ||
|
|
926ca19a1b | ||
|
|
c03b2ebbc1 | ||
|
|
e82c88317e | ||
|
|
59f543f442 | ||
|
|
f092b85f55 | ||
|
|
cfed481d6e | ||
|
|
5f6ddc2fad | ||
|
|
334eee0c8c | ||
|
|
550e235d59 | ||
|
|
9970fb3940 | ||
|
|
5d2723f4b9 | ||
|
|
a062a0bfbe | ||
|
|
706d66a76e | ||
|
|
2d22c4ff7d | ||
|
|
d77a0887a7 | ||
|
|
2383d243d5 | ||
|
|
426c132f97 | ||
|
|
1ff19f8604 | ||
|
|
14a581e86b | ||
|
|
ed279ba65b | ||
|
|
19eceaf37f | ||
|
|
1963d064a3 | ||
|
|
58d130ee37 | ||
|
|
98e6393274 | ||
|
|
745bbb7d79 | ||
|
|
757461d58b | ||
|
|
f20d3e72b9 | ||
|
|
731f3959c7 | ||
|
|
0f9a0e25f2 | ||
|
|
ae339a0047 | ||
|
|
77f8b9333a | ||
|
|
bbea0bc8a5 | ||
|
|
4b9c857d85 | ||
|
|
b5771df6a8 | ||
|
|
7ed8e9e167 | ||
|
|
80a3a5f16e | ||
|
|
3e654ff9b2 | ||
|
|
9b287f3020 | ||
|
|
a7404e00d1 | ||
|
|
3654109332 | ||
|
|
bf9dc8c2d0 | ||
|
|
67f8e8f3c2 | ||
|
|
56d6dfe02e | ||
|
|
861a9a5bbb | ||
|
|
1b470845b8 | ||
|
|
3c26aa8f34 | ||
|
|
de953da5a4 | ||
|
|
5356d1feeb | ||
|
|
7a8a20e0cc | ||
|
|
a7474188b9 | ||
|
|
6fe56f89c6 | ||
|
|
a98f480974 | ||
|
|
8ccac7c98f | ||
|
|
e0ce3671e8 | ||
|
|
62128d1069 | ||
|
|
a65ffe519a | ||
|
|
5ac1ea3df8 | ||
|
|
bf56bdb8f6 | ||
|
|
b00aa68c2b | ||
|
|
8c5edd2c97 | ||
|
|
c650868fe9 | ||
|
|
30a2bb0495 | ||
|
|
1a451823d9 | ||
|
|
feab2a757e | ||
|
|
17839aa473 | ||
|
|
fc1aec3bb8 | ||
|
|
d64e7eacfc | ||
|
|
7f805ac5be | ||
|
|
308a78db21 | ||
|
|
814fc9dfc0 | ||
|
|
3635df89dc | ||
|
|
30248eabb4 | ||
|
|
3636ac5c26 | ||
|
|
f6e8b25cf3 | ||
|
|
124e0bf9b9 | ||
|
|
45def82156 | ||
|
|
76bdf6f220 | ||
|
|
e142be399d | ||
|
|
13ba72ee07 | ||
|
|
f17a608dc7 | ||
|
|
6ee5cc6a56 | ||
|
|
44582732bb | ||
|
|
ea03024fbc | ||
|
|
795e6a5b3c | ||
|
|
2b17cb9104 | ||
|
|
347f66b1f1 | ||
|
|
40c387f4f4 | ||
|
|
15cbdb8af9 | ||
|
|
621a01ba3b | ||
|
|
37f382d286 | ||
|
|
77b49ae9c5 | ||
|
|
29648f517b | ||
|
|
8f42af49e8 | ||
|
|
0ab7987684 | ||
|
|
31d956dbcb | ||
|
|
2cc80e5e5d | ||
|
|
fb6e26a302 | ||
|
|
9cca299833 | ||
|
|
4c86be725d | ||
|
|
0669ad77d3 | ||
|
|
2bfc956f58 | ||
|
|
89194405ee | ||
|
|
5f0af62521 | ||
|
|
e3299eddd5 | ||
|
|
bdde278139 | ||
|
|
01ea9afe33 | ||
|
|
8345d1471e | ||
|
|
2a55d20eff | ||
|
|
7dca784ec6 | ||
|
|
37484566eb | ||
|
|
70710cfeb7 | ||
|
|
03712966e4 | ||
|
|
07100258cd | ||
|
|
4c6f5f961e | ||
|
|
77e1f5aa34 | ||
|
|
3baab6d695 | ||
|
|
d546ff269b | ||
|
|
60275dd31c | ||
|
|
07df4b1591 | ||
|
|
fd916bc8a2 | ||
|
|
769c8372fb | ||
|
|
d032119ebc | ||
|
|
ac47649631 | ||
|
|
8d6797dc9f | ||
|
|
197b0bcbde | ||
|
|
6918da2414 | ||
|
|
085381e6fc | ||
|
|
6074d1fcb5 | ||
|
|
96e5d44cc2 | ||
|
|
a45ef3d72e | ||
|
|
c819d4e7f7 | ||
|
|
bc6a667a6b | ||
|
|
7dcd6f9b9e | ||
|
|
c8d334e603 | ||
|
|
ab9b0c2147 | ||
|
|
6d659b4a2c | ||
|
|
defce0cf6d | ||
|
|
5f66020e42 | ||
|
|
b3e72ecaa0 | ||
|
|
b98c71f1ab | ||
|
|
f9a09301a8 | ||
|
|
2c247efd0f | ||
|
|
86d0e30eb7 | ||
|
|
69a91ff90a | ||
|
|
e0481f69b1 | ||
|
|
088262b6dc | ||
|
|
1b12ee9f01 | ||
|
|
5507b1e8c9 | ||
|
|
273a3f9a10 | ||
|
|
afe6cd6df0 | ||
|
|
95ac2cc4c3 | ||
|
|
9a8e95d017 | ||
|
|
631503fc1b | ||
|
|
23f3008500 | ||
|
|
89dd72b4ac | ||
|
|
5a375ff055 | ||
|
|
f081631808 | ||
|
|
e66dea44e3 | ||
|
|
392c7f74b8 | ||
|
|
1dba5e464b | ||
|
|
56dc2d1000 | ||
|
|
5c05ec489e | ||
|
|
cef9255161 | ||
|
|
0befdacc0e | ||
|
|
b2105f3614 | ||
|
|
58d66d3142 | ||
|
|
9f6702d0b8 | ||
|
|
44d69f3a3f | ||
|
|
e255bd710f | ||
|
|
d73622ed9c | ||
|
|
4753d52532 | ||
|
|
f9bbe000fb | ||
|
|
bfc610c192 | ||
|
|
51b9804fab | ||
|
|
e2168d21c7 | ||
|
|
2dddc1c6b9 | ||
|
|
c9253319d9 | ||
|
|
968fc98401 | ||
|
|
921e9cfc6e | ||
|
|
9b53960906 | ||
|
|
402a62a5e2 | ||
|
|
3470ea049a | ||
|
|
7fd263e8cc | ||
|
|
36c6d3f21b | ||
|
|
5f3dd0a64f | ||
|
|
42ca1287df | ||
|
|
2874a79279 | ||
|
|
8574dd2371 | ||
|
|
53eb5aa1ee | ||
|
|
eb8644330e | ||
|
|
8663de580a | ||
|
|
34298d96c5 | ||
|
|
9d103ffbeb | ||
|
|
5847c2b8ef | ||
|
|
a09fe7e10c | ||
|
|
5640cce4d6 | ||
|
|
00bbf4ac63 | ||
|
|
a748e15c16 | ||
|
|
cfdb9c126f | ||
|
|
851a3346a9 | ||
|
|
c9aae27b29 | ||
|
|
087848539f | ||
|
|
a74e389521 | ||
|
|
eff6ec9df9 | ||
|
|
8dec95c2cd | ||
|
|
5b02f636d7 | ||
|
|
ac458d0daa | ||
|
|
5b5dc320d5 | ||
|
|
d04747b309 | ||
|
|
07dd6bbe84 | ||
|
|
406ff8812c | ||
|
|
5942f4ff58 | ||
|
|
adf92ce5e0 | ||
|
|
fed3d14adf | ||
|
|
73db588080 | ||
|
|
6769326c8b | ||
|
|
e6d0e297dd | ||
|
|
0cd272211a | ||
|
|
6570f1f8eb | ||
|
|
1c180346e4 | ||
|
|
1d5d1bb12d | ||
|
|
0c27316034 | ||
|
|
d3bed3072b | ||
|
|
329e8bcad5 | ||
|
|
4bdf30c038 | ||
|
|
7793b98813 | ||
|
|
02de7b2715 | ||
|
|
9c0e0607a4 | ||
|
|
baf9c3db0a | ||
|
|
6c193a8a45 | ||
|
|
48a0f40621 | ||
|
|
4dc643acd9 | ||
|
|
1d42db93f1 | ||
|
|
33c3f8460c | ||
|
|
dd0d1737b0 | ||
|
|
3d28a6f877 | ||
|
|
2fc518f221 | ||
|
|
137ce37096 | ||
|
|
e529327851 | ||
|
|
3625ab6faa | ||
|
|
afb024d2a4 | ||
|
|
b2bc4b92d6 | ||
|
|
e5fd0c9595 | ||
|
|
649c1c9cee | ||
|
|
919a854d93 | ||
|
|
7fe0712b61 | ||
|
|
b4a6f6911c | ||
|
|
59d35d26d8 | ||
|
|
95558ed4ad | ||
|
|
e1b474d04f | ||
|
|
9732d1b5d8 | ||
|
|
701410d259 | ||
|
|
123754cee7 | ||
|
|
d75d2ba9ce | ||
|
|
046738c967 | ||
|
|
0436be7bc4 | ||
|
|
94d64997cc | ||
|
|
294d1668d4 | ||
|
|
4bd6618fb9 | ||
|
|
62197a67f7 | ||
|
|
c1dc1b49d1 | ||
|
|
b917e12b62 | ||
|
|
a8ccd2b153 | ||
|
|
68975620c5 | ||
|
|
67d3abcc9d | ||
|
|
90b0cb84f4 | ||
|
|
b22cdb3559 | ||
|
|
37896661d6 | ||
|
|
f38b8234d9 | ||
|
|
52e150fa29 | ||
|
|
929749c0da | ||
|
|
09bf5d03f4 | ||
|
|
ac6f52ab76 | ||
|
|
0ddcad66f3 | ||
|
|
930d9e5628 | ||
|
|
8936ae9b7a | ||
|
|
db9d87c918 | ||
|
|
b59a0ba823 | ||
|
|
2188005b48 | ||
|
|
a1528475ba | ||
|
|
5cbf52377d | ||
|
|
756ac034ec | ||
|
|
1008afd1fe | ||
|
|
563ead85cc | ||
|
|
eba5879ec8 | ||
|
|
b48aa1274d | ||
|
|
3e485c3152 | ||
|
|
dffd45c5f9 | ||
|
|
c1cc8bad77 | ||
|
|
8dcc5e4adb | ||
|
|
4558ce84cf | ||
|
|
adc87b8f8e | ||
|
|
ce8455953e | ||
|
|
a61b18dd93 | ||
|
|
d6a3fe23e9 | ||
|
|
cbaba43842 | ||
|
|
c173888b64 | ||
|
|
82e9e2a895 | ||
|
|
4fee359247 | ||
|
|
9cdc0da615 | ||
|
|
8fd0efa34f | ||
|
|
79bfd8f6fe | ||
|
|
2114c15f55 | ||
|
|
d2f6d1e415 | ||
|
|
241440a474 | ||
|
|
2e19f4ea6d | ||
|
|
95bc508462 | ||
|
|
d78b762f7b | ||
|
|
9dfac98a26 | ||
|
|
e26a607d28 | ||
|
|
6dc1841c14 | ||
|
|
c28be7aced | ||
|
|
dd01165224 | ||
|
|
d484a0eb64 | ||
|
|
fe8e834dbf | ||
|
|
0f0513c684 | ||
|
|
7006c17ce4 | ||
|
|
253a3a2b40 | ||
|
|
1e4c4e2616 | ||
|
|
75f40fe485 | ||
|
|
61e8e68c31 | ||
|
|
583346321e | ||
|
|
4cfa584c7c | ||
|
|
d012a4efc4 | ||
|
|
e0f3a8c0a2 | ||
|
|
bb48ab00cb | ||
|
|
eccc8131dd | ||
|
|
c21921a08d | ||
|
|
573e05d1c7 | ||
|
|
246e351817 | ||
|
|
6775c7b6ec | ||
|
|
881e99df53 | ||
|
|
78dcba614d | ||
|
|
30e23ea5b4 | ||
|
|
e1e81731b9 | ||
|
|
16377221f9 | ||
|
|
a0237852ef | ||
|
|
193e0c7d6f | ||
|
|
77c29ff87e | ||
|
|
2868da296a | ||
|
|
ff10588383 | ||
|
|
6b02d9a1e3 | ||
|
|
9f3d5185b0 | ||
|
|
f94147b07b | ||
|
|
49d02e0386 | ||
|
|
e82d0cfbdb | ||
|
|
c8051b68d4 | ||
|
|
37d4a80769 | ||
|
|
9ef2e27aae | ||
|
|
9e1f80cf37 | ||
|
|
459c95169a | ||
|
|
5048f08b5f | ||
|
|
e785d1572e | ||
|
|
95a4f83466 | ||
|
|
4edf232e41 | ||
|
|
903cf284e7 | ||
|
|
a550bfaedb | ||
|
|
446febb0f6 | ||
|
|
cb9fe2606c | ||
|
|
55211ef00e | ||
|
|
e48ceb15e9 | ||
|
|
1b12cc9f31 | ||
|
|
0365ed8e70 | ||
|
|
7624ff10ee | ||
|
|
535a26412f | ||
|
|
ee5600b6af | ||
|
|
3f51d077ac | ||
|
|
0219d41ba7 | ||
|
|
f3e2ccd487 | ||
|
|
368e6b2a44 | ||
|
|
1100a2bd28 | ||
|
|
16dc66f173 | ||
|
|
c1f94be9b2 | ||
|
|
58947fee69 | ||
|
|
0c995ae1c8 | ||
|
|
f6d6be90e4 | ||
|
|
5488389278 | ||
|
|
69f498c431 | ||
|
|
669327da7c | ||
|
|
191f8e17ee | ||
|
|
ae2bec4bd9 | ||
|
|
367f3dd6d4 | ||
|
|
8f1ac38963 |
@@ -1,44 +0,0 @@
|
||||
version: "2"
|
||||
checks:
|
||||
argument-count:
|
||||
enabled: false
|
||||
complex-logic:
|
||||
enabled: false
|
||||
file-lines:
|
||||
enabled: false
|
||||
method-complexity:
|
||||
enabled: false
|
||||
method-count:
|
||||
enabled: false
|
||||
method-lines:
|
||||
enabled: false
|
||||
nested-control-flow:
|
||||
enabled: false
|
||||
return-statements:
|
||||
enabled: false
|
||||
similar-code:
|
||||
enabled: false
|
||||
identical-code:
|
||||
enabled: false
|
||||
plugins:
|
||||
gofmt:
|
||||
enabled: true
|
||||
eslint:
|
||||
enabled: true
|
||||
channel: "eslint-5"
|
||||
config:
|
||||
config: .eslintrc.yml
|
||||
exclude_patterns:
|
||||
- assets/
|
||||
- build/
|
||||
- dist/
|
||||
- distribution/
|
||||
- node_modules
|
||||
- test/
|
||||
- webpack/
|
||||
- gruntfile.js
|
||||
- webpack.config.js
|
||||
- api/
|
||||
- "!app/kubernetes/**"
|
||||
- .github/
|
||||
- .tmp/
|
||||
@@ -83,10 +83,11 @@ overrides:
|
||||
'newlines-between': 'always',
|
||||
},
|
||||
]
|
||||
no-plusplus: off
|
||||
func-style: [error, 'declaration']
|
||||
import/prefer-default-export: off
|
||||
no-use-before-define: ['error', { functions: false }]
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false }]
|
||||
no-use-before-define: "off"
|
||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false, "allowNamedExports": true }]
|
||||
no-shadow: 'off'
|
||||
'@typescript-eslint/no-shadow': off
|
||||
jsx-a11y/no-autofocus: warn
|
||||
@@ -99,12 +100,17 @@ overrides:
|
||||
'@typescript-eslint/explicit-module-boundary-types': off
|
||||
'@typescript-eslint/no-unused-vars': 'error'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
|
||||
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either', controlComponents: ['Input', 'Checkbox'] }]
|
||||
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
|
||||
'react/jsx-no-bind': off
|
||||
'no-await-in-loop': 'off'
|
||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
|
||||
'regex/invalid': ['error', [{ 'regex': 'data-feather="(.*)"', 'message': 'Please use `react-feather` package instead' }]]
|
||||
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
|
||||
overrides: # allow props spreading for hoc files
|
||||
- files:
|
||||
- app/**/with*.ts{,x}
|
||||
rules:
|
||||
'react/jsx-props-no-spreading': off
|
||||
- files:
|
||||
- app/**/*.test.*
|
||||
extends:
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
cf5056d9c03b62d91a25c3b9127caac838695f98
|
||||
|
||||
# prettier v2
|
||||
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
|
||||
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
|
||||
|
||||
# tailwind prettier
|
||||
58d66d3142950bb90a7d85511c034ac9fabba9ba
|
||||
2
.github/workflows/label-conflcts.yaml
vendored
2
.github/workflows/label-conflcts.yaml
vendored
@@ -12,4 +12,4 @@ jobs:
|
||||
CONFLICT_LABEL_NAME: 'has conflicts'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAX_RETRIES: 5
|
||||
WAIT_MS: 5000
|
||||
WAIT_MS: 10000
|
||||
|
||||
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -21,10 +21,12 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.19.5
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run linters
|
||||
uses: wearerequired/lint-action@v1
|
||||
with:
|
||||
@@ -36,3 +38,9 @@ jobs:
|
||||
gofmt_dir: api/
|
||||
- name: Typecheck
|
||||
uses: icrawl/action-tsc@v1
|
||||
- name: GolangCI-Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
working-directory: api
|
||||
args: --timeout=10m -c .golangci.yaml
|
||||
|
||||
153
.github/workflows/nightly-security-scan.yml
vendored
153
.github/workflows/nightly-security-scan.yml
vendored
@@ -1,22 +1,23 @@
|
||||
name: Nightly Code Security Scan
|
||||
|
||||
on:
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 8 * * *'
|
||||
- cron: '0 20 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client dependency check
|
||||
name: Client Dependency Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
js: ${{ steps.set-matrix.outputs.js_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
- name: scan vulnerabilities by Snyk
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
@@ -24,166 +25,140 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: Upload js security scan result as artifact
|
||||
- name: upload scan result as develop artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
|
||||
- name: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
|
||||
|
||||
- name: Upload js result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-js-result-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: Analyse the js result
|
||||
- name: analyse vulnerabilities
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=js_result::${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
echo "js_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
server-dependencies:
|
||||
name: Server dependency check
|
||||
name: Server Dependency Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >- # only run for develop branch
|
||||
github.ref == 'refs/heads/develop'
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
go: ${{ steps.set-matrix.outputs.go_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Download go modules
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
|
||||
- name: download Go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/golang@master
|
||||
- name: scan vulnerabilities by Snyk
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --file=./api/go.mod
|
||||
json: true
|
||||
run: |
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
|
||||
- name: Upload go security scan result as artifact
|
||||
- name: upload scan result as develop artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-develop-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
|
||||
- name: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-go-result-${{github.run_id}}
|
||||
path: go-result.html
|
||||
|
||||
- name: Analyse the go result
|
||||
- name: analyse vulnerabilities
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
||||
echo "::set-output name=go_result::${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||
echo "go_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
image-vulnerability:
|
||||
name: Build docker image and Image vulnerability check
|
||||
name: Image Vulnerability Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
outputs:
|
||||
image: ${{ steps.set-matrix.outputs.image_result }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Use golang 1.18
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
- name: Install packages and build
|
||||
run: yarn install && yarn build
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: build/linux/Dockerfile
|
||||
tags: trivy-portainer:${{ github.sha }}
|
||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Load docker image
|
||||
run: |
|
||||
docker load --input /tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
- name: scan vulnerabilities by Trivy
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop
|
||||
|
||||
- name: Upload image security scan result as artifact
|
||||
- name: upload image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-develop-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
|
||||
- name: develop scan report export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-${{github.run_id}}
|
||||
path: image-result.html
|
||||
|
||||
- name: Analyse the trivy result
|
||||
- name: analyse vulnerabilities
|
||||
id: set-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
|
||||
echo "::set-output name=image_result::${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
|
||||
echo "image_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
result-analysis:
|
||||
name: Analyse scan result
|
||||
name: Analyse Scan Results
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.ref == 'refs/heads/develop'
|
||||
strategy:
|
||||
matrix:
|
||||
matrix:
|
||||
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
|
||||
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
|
||||
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
|
||||
steps:
|
||||
- name: Display the results of js, go and image
|
||||
- name: display the results of js, Go, and image scan
|
||||
run: |
|
||||
echo ${{ matrix.js.status }}
|
||||
echo ${{ matrix.go.status }}
|
||||
echo ${{ matrix.image.status }}
|
||||
echo ${{ matrix.js.summary }}
|
||||
echo ${{ matrix.go.summary }}
|
||||
echo ${{ matrix.image.summary }}
|
||||
echo "${{ matrix.js.status }}"
|
||||
echo "${{ matrix.go.status }}"
|
||||
echo "${{ matrix.image.status }}"
|
||||
echo "${{ matrix.js.summary }}"
|
||||
echo "${{ matrix.go.summary }}"
|
||||
echo "${{ matrix.image.summary }}"
|
||||
|
||||
- name: Send Slack message
|
||||
- name: send message to Slack
|
||||
if: >-
|
||||
matrix.js.status == 'failure' ||
|
||||
matrix.go.status == 'failure' ||
|
||||
matrix.image.status == 'failure'
|
||||
uses: slackapi/slack-github-action@v1.18.0
|
||||
uses: slackapi/slack-github-action@v1.23.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
|
||||
147
.github/workflows/pr-security.yml
vendored
147
.github/workflows/pr-security.yml
vendored
@@ -12,10 +12,11 @@ on:
|
||||
- 'build/linux/Dockerfile'
|
||||
- 'build/linux/alpine.Dockerfile'
|
||||
- 'build/windows/Dockerfile'
|
||||
- '.github/workflows/pr-security.yml'
|
||||
|
||||
jobs:
|
||||
client-dependencies:
|
||||
name: Client dependency check
|
||||
name: Client Dependency Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -23,9 +24,10 @@ jobs:
|
||||
outputs:
|
||||
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
- name: scan vulnerabilities by Snyk
|
||||
uses: snyk/actions/node@master
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
@@ -33,13 +35,13 @@ jobs:
|
||||
with:
|
||||
json: true
|
||||
|
||||
- name: Upload js security scan result as artifact
|
||||
- name: upload scan result as pull-request artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: js-security-scan-feat-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Download artifacts from develop branch
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -51,24 +53,24 @@ jobs:
|
||||
echo "null" > ./js-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result")
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=table --export --export-filename="/data/js-result")
|
||||
|
||||
- name: Upload js result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-js-result-compare-to-develop-${{github.run_id}}
|
||||
path: js-result.html
|
||||
|
||||
- name: Analyse the js diff result
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=js_diff_result::${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=matrix)
|
||||
echo "js_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
server-dependencies:
|
||||
name: Server dependency check
|
||||
name: Server Dependency Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -76,27 +78,32 @@ jobs:
|
||||
outputs:
|
||||
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Download go modules
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.19.5'
|
||||
|
||||
- name: download Go modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/golang@master
|
||||
- name: scan vulnerabilities by Snyk
|
||||
continue-on-error: true # To make sure that artifact upload gets called
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
args: --file=./api/go.mod
|
||||
json: true
|
||||
run: |
|
||||
yarn global add snyk
|
||||
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||
|
||||
- name: Upload go security scan result as artifact
|
||||
- name: upload scan result as pull-request artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: go-security-scan-feature-result
|
||||
path: snyk.json
|
||||
|
||||
- name: Download artifacts from develop branch
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -108,24 +115,24 @@ jobs:
|
||||
echo "null" > ./go-snyk-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result")
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=table --export --export-filename="/data/go-result")
|
||||
|
||||
- name: Upload go result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-go-result-compare-to-develop-${{github.run_id}}
|
||||
path: go-result.html
|
||||
|
||||
- name: Analyse the go diff result
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=go_diff_result::${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=matrix)
|
||||
echo "go_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
image-vulnerability:
|
||||
name: Build docker image and Image vulnerability check
|
||||
name: Image Vulnerability Check
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
@@ -133,50 +140,53 @@ jobs:
|
||||
outputs:
|
||||
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
- name: checkout code
|
||||
uses: actions/checkout@master
|
||||
|
||||
- name: Use golang 1.18
|
||||
- name: install Go 1.19.5
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
go-version: '1.19.5'
|
||||
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
- name: install Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install packages and build
|
||||
run: yarn install && yarn build
|
||||
- name: Install packages
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: build
|
||||
run: make build
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
- name: set up docker buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: build and compress image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: build/linux/Dockerfile
|
||||
tags: trivy-portainer:${{ github.sha }}
|
||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Load docker image
|
||||
- name: load docker image
|
||||
run: |
|
||||
docker load --input /tmp/trivy-portainer-image.tar
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
- name: scan vulnerabilities by Trivy
|
||||
uses: docker://docker.io/aquasec/trivy:latest
|
||||
continue-on-error: true
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||
|
||||
- name: Upload image security scan result as artifact
|
||||
- name: upload image security scan result as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: image-security-scan-feature-result
|
||||
path: image-trivy.json
|
||||
|
||||
- name: Download artifacts from develop branch
|
||||
- name: download artifacts from develop branch built by nightly scan
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -188,46 +198,45 @@ jobs:
|
||||
echo "null" > ./image-trivy-develop.json
|
||||
fi
|
||||
|
||||
- name: Export scan result to html file
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="/data/image-trivy-develop.json" -output-type=table -export -export-filename="/data/image-result")
|
||||
- name: pr vs develop scan report comparison export to html
|
||||
run: |
|
||||
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-result")
|
||||
|
||||
- name: Upload image result html file
|
||||
- name: upload html file as artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: html-image-result-compare-to-develop-${{github.run_id}}
|
||||
path: image-result.html
|
||||
|
||||
- name: Analyse the image diff result
|
||||
- name: analyse different vulnerabilities against develop branch
|
||||
id: set-diff-matrix
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
|
||||
echo "::set-output name=image_diff_result::${result}"
|
||||
run: |
|
||||
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=matrix)
|
||||
echo "image_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||
|
||||
result-analysis:
|
||||
name: Analyse scan result compared to develop
|
||||
name: Analyse Scan Result Against develop Branch
|
||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request &&
|
||||
github.event.review.body == '/scan'
|
||||
strategy:
|
||||
matrix:
|
||||
matrix:
|
||||
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
|
||||
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
|
||||
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
|
||||
steps:
|
||||
|
||||
- name: Check job status of diff result
|
||||
- name: check job status of diff result
|
||||
if: >-
|
||||
matrix.jsdiff.status == 'failure' ||
|
||||
matrix.godiff.status == 'failure' ||
|
||||
matrix.imagediff.status == 'failure'
|
||||
matrix.imagediff.status == 'failure'
|
||||
run: |
|
||||
echo ${{ matrix.jsdiff.status }}
|
||||
echo ${{ matrix.godiff.status }}
|
||||
echo ${{ matrix.imagediff.status }}
|
||||
echo ${{ matrix.jsdiff.summary }}
|
||||
echo ${{ matrix.godiff.summary }}
|
||||
echo ${{ matrix.imagediff.summary }}
|
||||
echo "${{ matrix.jsdiff.status }}"
|
||||
echo "${{ matrix.godiff.status }}"
|
||||
echo "${{ matrix.imagediff.status }}"
|
||||
echo "${{ matrix.jsdiff.summary }}"
|
||||
echo "${{ matrix.godiff.summary }}"
|
||||
echo "${{ matrix.imagediff.summary }}"
|
||||
exit 1
|
||||
|
||||
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
@@ -8,12 +8,12 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '18'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: yarn test:client
|
||||
run: yarn jest --maxWorkers=2
|
||||
# test-server:
|
||||
# runs-on: ubuntu-latest
|
||||
# env:
|
||||
|
||||
29
.github/workflows/validate-openapi-spec.yaml
vendored
Normal file
29
.github/workflows/validate-openapi-spec.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Validate OpenAPI specs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
openapi-spec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.18'
|
||||
|
||||
- name: Download golang modules
|
||||
run: cd ./api && go get -t -v -d ./...
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
cache: 'yarn'
|
||||
- run: yarn --frozen-lockfile
|
||||
|
||||
- name: Validate OpenAPI Spec
|
||||
run: make docs-validate
|
||||
53
.github/workflows/validate-openapi-spec.yml
vendored
53
.github/workflows/validate-openapi-spec.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Validate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
- 'release/*'
|
||||
|
||||
jobs:
|
||||
openapi-spec:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node v14
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
|
||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Setup Go v1.17.3
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.17.3'
|
||||
|
||||
- name: Prebuild docs
|
||||
run: yarn prebuild:docs
|
||||
|
||||
- name: Build OpenAPI 2.0 Spec
|
||||
run: yarn build:docs
|
||||
|
||||
# Install dependencies globally to bypass installing all frontend deps
|
||||
- name: Install swagger2openapi and swagger-cli
|
||||
run: yarn global add swagger2openapi @apidevtools/swagger-cli
|
||||
|
||||
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
|
||||
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
|
||||
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
|
||||
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
|
||||
run: yarn validate:docs
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
||||
@@ -29,10 +29,30 @@ module.exports = {
|
||||
extensions: config.resolve.extensions,
|
||||
}),
|
||||
];
|
||||
|
||||
const svgRule = config.module.rules.find((rule) => rule.test && typeof rule.test.test === 'function' && rule.test.test('.svg'));
|
||||
svgRule.test = new RegExp(svgRule.test.source.replace('svg|', ''));
|
||||
|
||||
config.module.rules.unshift({
|
||||
test: /\.svg$/i,
|
||||
type: 'asset',
|
||||
resourceQuery: { not: [/c/] }, // exclude react component if *.svg?url
|
||||
});
|
||||
|
||||
config.module.rules.unshift({
|
||||
test: /\.svg$/i,
|
||||
issuer: /\.(js|ts)(x)?$/,
|
||||
resourceQuery: /c/, // *.svg?c
|
||||
use: [{ loader: '@svgr/webpack', options: { icon: true } }],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
core: {
|
||||
builder: 'webpack5',
|
||||
},
|
||||
staticDirs: ['./public'],
|
||||
typescript: {
|
||||
reactDocgen: 'react-docgen-typescript-plugin',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"React Named Export Component": {
|
||||
"prefix": "rnec",
|
||||
"body": [
|
||||
"export function $TM_FILENAME_BASE() {",
|
||||
" return <div>$TM_FILENAME_BASE</div>;",
|
||||
"}"
|
||||
],
|
||||
"description": "React Named Export Component"
|
||||
},
|
||||
"Component": {
|
||||
"scope": "javascript",
|
||||
"prefix": "mycomponent",
|
||||
|
||||
@@ -25,7 +25,7 @@ Each commit message should include a **type**, a **scope** and a **subject**:
|
||||
<type>(<scope>): <subject>
|
||||
```
|
||||
|
||||
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
|
||||
Lines should not exceed 100 characters. This allows the message to be easier to read on GitHub as well as in various git tools and produces a nice, neat commit log ie:
|
||||
|
||||
```
|
||||
#271 feat(containers): add exposed ports in the containers view
|
||||
@@ -63,7 +63,7 @@ The subject contains succinct description of the change:
|
||||
|
||||
## Contribution process
|
||||
|
||||
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
|
||||
Our contribution process is described below. Some of the steps can be visualized inside GitHub via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
|
||||
|
||||
### Bug report
|
||||
|
||||
@@ -93,7 +93,7 @@ $ yarn start
|
||||
|
||||
Portainer can now be accessed at <https://localhost:9443>.
|
||||
|
||||
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
|
||||
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
|
||||
|
||||
### Build customisation
|
||||
|
||||
@@ -103,6 +103,10 @@ You can customise the following settings:
|
||||
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
|
||||
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
|
||||
|
||||
## Testing your build
|
||||
|
||||
The `--log-level=DEBUG` flag can be passed to the Portainer container in order to provide additional debug output which may be useful when troubleshooting your builds. Please note that this flag was originally intended for internal use and as such the format, functionality and output may change between releases without warning.
|
||||
|
||||
## Adding api docs
|
||||
|
||||
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
|
||||
|
||||
126
Makefile
Normal file
126
Makefile
Normal file
@@ -0,0 +1,126 @@
|
||||
# See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
|
||||
# For a list of valid GOOS and GOARCH values
|
||||
# Note: these can be overriden on the command line e.g. `make PLATFORM=<platform> ARCH=<arch>`
|
||||
PLATFORM=$(shell go env GOOS)
|
||||
ARCH=$(shell go env GOARCH)
|
||||
|
||||
# build target, can be one of "production", "testing", "development"
|
||||
ENV=development
|
||||
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
||||
TAG=latest
|
||||
|
||||
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.8.11
|
||||
GOTESTSUM=go run gotest.tools/gotestsum@latest
|
||||
|
||||
# Don't change anything below this line unless you know what you're doing
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
|
||||
##@ Building
|
||||
.PHONY: init-dist build-storybook build build-client build-server build-image devops
|
||||
init-dist:
|
||||
@mkdir -p dist
|
||||
|
||||
build: build-server build-client ## Build the server and client
|
||||
|
||||
build-client: init-dist client-deps ## Build the client
|
||||
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
|
||||
|
||||
build-server: init-dist ## Build the server binary
|
||||
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
|
||||
|
||||
build-image: build ## Build the Portainer image locally
|
||||
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
|
||||
|
||||
devops: clean init-dist server-deps build-client ## Build the server binary for CI
|
||||
echo "Building the devops binary..."
|
||||
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
|
||||
|
||||
build-storybook:
|
||||
yarn storybook:build
|
||||
|
||||
##@ Build dependencies
|
||||
.PHONY: deps server-deps client-deps tidy
|
||||
deps-all: server-deps client-deps ## Download all client and server build dependancies
|
||||
|
||||
server-deps: ## Download dependant server binaries
|
||||
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
|
||||
|
||||
client-deps: ## Install client dependencies
|
||||
yarn
|
||||
|
||||
tidy: ## Tidy up the go.mod file
|
||||
cd api && go mod tidy
|
||||
|
||||
|
||||
##@ Cleanup
|
||||
.PHONY: clean
|
||||
clean: ## Remove all build and download artifacts
|
||||
@echo "Clearing the dist directory..."
|
||||
@rm -rf dist/*
|
||||
|
||||
|
||||
##@ Testing
|
||||
.PHONY: test test-client test-server
|
||||
test: test-server test-client ## Run all tests
|
||||
|
||||
test-client: ## Run client tests
|
||||
yarn test
|
||||
|
||||
test-server: ## Run server tests
|
||||
cd api && $(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
|
||||
|
||||
##@ Dev
|
||||
.PHONY: dev dev-client dev-server
|
||||
dev: ## Run both the client and server in development mode
|
||||
make dev-server
|
||||
make dev-client
|
||||
|
||||
dev-client: ## Run the client in development mode
|
||||
yarn dev
|
||||
|
||||
dev-server: ## Run the server in development mode
|
||||
@./dev/run_container.sh
|
||||
|
||||
|
||||
##@ Format
|
||||
.PHONY: format format-client format-server
|
||||
|
||||
format: format-client format-server ## Format all code
|
||||
|
||||
format-client: ## Format client code
|
||||
yarn format
|
||||
|
||||
format-server: ## Format server code
|
||||
cd api && go fmt ./...
|
||||
|
||||
##@ Lint
|
||||
.PHONY: lint lint-client lint-server
|
||||
lint: lint-client lint-server ## Lint all code
|
||||
|
||||
lint-client: ## Lint client code
|
||||
yarn lint
|
||||
|
||||
lint-server: ## Lint server code
|
||||
cd api && go vet ./...
|
||||
|
||||
|
||||
##@ Extension
|
||||
.PHONY: dev-extension
|
||||
dev-extension: build-server build-client ## Run the extension in development mode
|
||||
make local -f build/docker-extension/Makefile
|
||||
|
||||
|
||||
##@ Docs
|
||||
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
|
||||
docs-build: init-dist ## Build docs
|
||||
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./
|
||||
|
||||
docs-validate: docs-build ## Validate docs
|
||||
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
|
||||
yarn swagger-cli validate dist/docs/openapi.yaml
|
||||
|
||||
##@ Helpers
|
||||
.PHONY: help
|
||||
help: ## Display this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
29
api/.golangci.yaml
Normal file
29
api/.golangci.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
linters:
|
||||
# Disable all linters, the defaults don't pass on our code yet
|
||||
disable-all: true
|
||||
|
||||
# Enable these for now
|
||||
enable:
|
||||
- depguard
|
||||
- govet
|
||||
- errorlint
|
||||
- exportloopref
|
||||
linters-settings:
|
||||
depguard:
|
||||
list-type: denylist
|
||||
include-go-root: true
|
||||
packages:
|
||||
- github.com/sirupsen/logrus
|
||||
- golang.org/x/exp
|
||||
packages-with-error-message:
|
||||
- github.com/sirupsen/logrus: 'logging is allowed only by github.com/rs/zerolog'
|
||||
ignore-file-rules:
|
||||
- '**/*_test.go'
|
||||
|
||||
# errorlint is causing a typecheck error for some reason. The go compiler will report these
|
||||
# anyway, so ignore them from the linter
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: ./
|
||||
linters:
|
||||
- typecheck
|
||||
@@ -21,7 +21,7 @@ type Monitor struct {
|
||||
datastore dataservices.DataStore
|
||||
shutdownCtx context.Context
|
||||
cancellationFunc context.CancelFunc
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
adminInitDisabled bool
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ func (m *Monitor) Start() {
|
||||
case <-time.After(m.timeout):
|
||||
initialized, err := m.WasInitialized()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
log.Error().Err(err).Msg("AdminMonitor failed to determine if Portainer is Initialized")
|
||||
return
|
||||
}
|
||||
|
||||
if !initialized {
|
||||
@@ -82,6 +83,7 @@ func (m *Monitor) Stop() {
|
||||
if m.cancellationFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.cancellationFunc()
|
||||
m.cancellationFunc = nil
|
||||
}
|
||||
@@ -92,12 +94,14 @@ func (m *Monitor) WasInitialized() (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return len(users) > 0, nil
|
||||
}
|
||||
|
||||
func (m *Monitor) WasInstanceDisabled() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return m.adminInitDisabled
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -42,7 +43,9 @@ func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (port
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
|
||||
@@ -3,6 +3,7 @@ package archive
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -84,7 +85,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
|
||||
if err == io.EOF {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -109,7 +110,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
|
||||
}
|
||||
outFile.Close()
|
||||
default:
|
||||
return fmt.Errorf("Tar: uknown type: %v in %s",
|
||||
return fmt.Errorf("tar: unknown type: %v in %s",
|
||||
header.Typeflag,
|
||||
header.Name)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package archive
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -28,10 +27,10 @@ func listFiles(dir string) []string {
|
||||
func Test_shouldCreateArhive(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
content := []byte("content")
|
||||
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||
|
||||
gzPath, err := TarGzDir(tmpdir)
|
||||
assert.Nil(t, err)
|
||||
@@ -48,7 +47,7 @@ func Test_shouldCreateArhive(t *testing.T) {
|
||||
wasExtracted := func(p string) {
|
||||
fullpath := path.Join(extractionDir, p)
|
||||
assert.Contains(t, extractedFiles, fullpath)
|
||||
copyContent, _ := ioutil.ReadFile(fullpath)
|
||||
copyContent, _ := os.ReadFile(fullpath)
|
||||
assert.Equal(t, content, copyContent)
|
||||
}
|
||||
|
||||
@@ -60,10 +59,10 @@ func Test_shouldCreateArhive(t *testing.T) {
|
||||
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
||||
tmpdir := t.TempDir()
|
||||
content := []byte("content")
|
||||
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||
|
||||
gzPath, err := TarGzDir(tmpdir)
|
||||
assert.Nil(t, err)
|
||||
@@ -80,7 +79,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
||||
wasExtracted := func(p string) {
|
||||
fullpath := path.Join(extractionDir, p)
|
||||
assert.Contains(t, extractedFiles, fullpath)
|
||||
copyContent, _ := ioutil.ReadFile(fullpath)
|
||||
copyContent, _ := os.ReadFile(fullpath)
|
||||
assert.Equal(t, content, copyContent)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
|
||||
@@ -36,7 +36,7 @@ func extractFileFromArchive(file *zip.File, dest string) error {
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package archive
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnzipFile(t *testing.T) {
|
||||
|
||||
@@ -3,8 +3,10 @@ package backup
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -43,6 +45,12 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
|
||||
return errors.Wrap(err, "Failed to stop db")
|
||||
}
|
||||
|
||||
// At some point, backups were created containing a subdirectory, now we need to handle both
|
||||
restorePath, err = getRestoreSourcePath(restorePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
|
||||
}
|
||||
|
||||
if err = restoreFiles(restorePath, filestorePath); err != nil {
|
||||
return errors.Wrap(err, "failed to restore the system state")
|
||||
}
|
||||
@@ -59,6 +67,26 @@ func extractArchive(r io.Reader, destinationDirPath string) error {
|
||||
return archive.ExtractTarGz(r, destinationDirPath)
|
||||
}
|
||||
|
||||
func getRestoreSourcePath(dir string) (string, error) {
|
||||
// find portainer.db or portainer.edb file. Return the parent directory
|
||||
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
|
||||
|
||||
backupDirPath := dir
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if portainerdbRegex.MatchString(d.Name()) {
|
||||
backupDirPath = filepath.Dir(path)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return backupDirPath, err
|
||||
}
|
||||
|
||||
func restoreFiles(srcDir string, destinationDir string) error {
|
||||
for _, filename := range filesToRestore {
|
||||
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
|
||||
|
||||
@@ -2,12 +2,17 @@ package chisel
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
)
|
||||
|
||||
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
|
||||
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
|
||||
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
|
||||
if endpoint.Edge.AsyncMode {
|
||||
return
|
||||
}
|
||||
|
||||
service.mu.Lock()
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
tunnel := service.getTunnelDetails(endpoint.ID)
|
||||
|
||||
existingJobIndex := -1
|
||||
for idx, existingJob := range tunnel.Jobs {
|
||||
@@ -23,6 +28,8 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
|
||||
tunnel.Jobs[existingJobIndex] = *edgeJob
|
||||
}
|
||||
|
||||
cache.Del(endpoint.ID)
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -30,8 +37,7 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
|
||||
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
||||
service.mu.Lock()
|
||||
|
||||
for _, tunnel := range service.tunnelDetailsMap {
|
||||
// Filter in-place
|
||||
for endpointID, tunnel := range service.tunnelDetailsMap {
|
||||
n := 0
|
||||
for _, edgeJob := range tunnel.Jobs {
|
||||
if edgeJob.ID != edgeJobID {
|
||||
@@ -41,7 +47,28 @@ func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
||||
}
|
||||
|
||||
tunnel.Jobs = tunnel.Jobs[:n]
|
||||
|
||||
cache.Del(endpointID)
|
||||
}
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
|
||||
service.mu.Lock()
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
|
||||
n := 0
|
||||
for _, edgeJob := range tunnel.Jobs {
|
||||
if edgeJob.ID != edgeJobID {
|
||||
tunnel.Jobs[n] = edgeJob
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
tunnel.Jobs = tunnel.Jobs[:n]
|
||||
|
||||
cache.Del(endpointID)
|
||||
|
||||
service.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package chisel
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -58,7 +59,11 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
|
||||
httpClient := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
_, err = httpClient.Do(req)
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -206,15 +211,23 @@ func (service *Service) checkTunnels() {
|
||||
|
||||
service.mu.Lock()
|
||||
for key, tunnel := range service.tunnelDetailsMap {
|
||||
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
|
||||
continue
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && time.Since(tunnel.LastActivity) < requiredTimeout {
|
||||
continue
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive && time.Since(tunnel.LastActivity) < activeTimeout {
|
||||
continue
|
||||
}
|
||||
|
||||
tunnels[key] = *tunnel
|
||||
}
|
||||
service.mu.Unlock()
|
||||
|
||||
for endpointID, tunnel := range tunnels {
|
||||
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
|
||||
continue
|
||||
}
|
||||
|
||||
elapsed := time.Since(tunnel.LastActivity)
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
@@ -222,9 +235,7 @@ func (service *Service) checkTunnels() {
|
||||
Float64("status_time_seconds", elapsed.Seconds()).
|
||||
Msg("environment tunnel monitoring")
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
|
||||
continue
|
||||
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
|
||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Str("status", tunnel.Status).
|
||||
@@ -233,9 +244,7 @@ func (service *Service) checkTunnels() {
|
||||
Msg("REQUIRED state timeout exceeded")
|
||||
}
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
|
||||
continue
|
||||
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
|
||||
if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
|
||||
log.Debug().
|
||||
Int("endpoint_id", int(endpointID)).
|
||||
Str("status", tunnel.Status).
|
||||
|
||||
@@ -2,14 +2,17 @@ package chisel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
"github.com/portainer/libcrypto"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
|
||||
"github.com/dchest/uniuri"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -49,6 +52,8 @@ func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *porta
|
||||
|
||||
service.tunnelDetailsMap[endpointID] = tunnel
|
||||
|
||||
cache.Del(endpointID)
|
||||
|
||||
return tunnel
|
||||
}
|
||||
|
||||
@@ -62,6 +67,10 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portai
|
||||
|
||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
|
||||
if endpoint.Edge.AsyncMode {
|
||||
return portainer.TunnelDetails{}, errors.New("cannot open tunnel on async endpoint")
|
||||
}
|
||||
|
||||
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||
|
||||
if tunnel.Status == portainer.EdgeAgentActive {
|
||||
@@ -99,6 +108,8 @@ func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID)
|
||||
tunnel.Credentials = ""
|
||||
tunnel.LastActivity = time.Now()
|
||||
service.mu.Unlock()
|
||||
|
||||
cache.Del(endpointID)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
|
||||
@@ -121,6 +132,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
||||
|
||||
service.mu.Unlock()
|
||||
|
||||
cache.Del(endpointID)
|
||||
}
|
||||
|
||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
||||
@@ -129,6 +142,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
|
||||
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
|
||||
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
||||
defer cache.Del(endpointID)
|
||||
|
||||
tunnel := service.getTunnelDetails(endpointID)
|
||||
|
||||
service.mu.Lock()
|
||||
|
||||
@@ -36,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
|
||||
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
||||
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
|
||||
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
|
||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
|
||||
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
|
||||
@@ -62,6 +62,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
|
||||
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
|
||||
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
|
||||
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("PRETTY", "JSON"),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
@@ -71,6 +72,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
||||
}
|
||||
|
||||
@@ -79,7 +81,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
|
||||
// ValidateFlags validates the values of the flags.
|
||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
|
||||
displayDeprecationWarnings(flags)
|
||||
|
||||
err := validateEndpointURL(*flags.EndpointURL)
|
||||
@@ -110,31 +111,38 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
||||
}
|
||||
|
||||
func validateEndpointURL(endpointURL string) error {
|
||||
if endpointURL != "" {
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
if endpointURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketOrNamedPipeNotFound
|
||||
}
|
||||
return err
|
||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errSocketOrNamedPipeNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSnapshotInterval(snapshotInterval string) error {
|
||||
if snapshotInterval != "" {
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
if snapshotInterval == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := time.ParseDuration(snapshotInterval)
|
||||
if err != nil {
|
||||
return errInvalidSnapshotInterval
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,23 +2,24 @@ package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Confirm starts a rollback db cli application
|
||||
func Confirm(message string) (bool, error) {
|
||||
log.Printf("%s [y/N]", message)
|
||||
fmt.Printf("%s [y/N]", message)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
answer, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
answer = strings.Replace(answer, "\n", "", -1)
|
||||
|
||||
answer = strings.ReplaceAll(answer, "\n", "")
|
||||
answer = strings.ToLower(answer)
|
||||
|
||||
return answer == "y" || answer == "yes", nil
|
||||
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func importFromJson(fileService portainer.FileService, store *datastore.Store) {
|
||||
// EXPERIMENTAL - if used with an incomplete json file, it will fail, as we don't have a way to default the model values
|
||||
importFile := "/data/import.json"
|
||||
if exists, _ := fileService.FileExists(importFile); exists {
|
||||
if err := store.Import(importFile); err != nil {
|
||||
log.Error().Str("filename", importFile).Err(err).Msg("import failed")
|
||||
// TODO: should really rollback on failure, but then we have nothing.
|
||||
} else {
|
||||
log.Info().Str("filename", importFile).Msg("successfully imported the file to a new portainer database")
|
||||
}
|
||||
|
||||
// TODO: this is bad - its to ensure that any defaults that were broken in import, or migrations get set back to what we want
|
||||
// I also suspect that everything from "Init to Init" is potentially a migration
|
||||
err := store.Init()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing data store")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -31,3 +33,23 @@ func setLoggingLevel(level string) {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func setLoggingMode(mode string) {
|
||||
switch mode {
|
||||
case "PRETTY":
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
NoColor: true,
|
||||
TimeFormat: "2006/01/02 03:04PM",
|
||||
FormatMessage: formatMessage})
|
||||
case "JSON":
|
||||
log.Logger = log.Output(os.Stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func formatMessage(i interface{}) string {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s |", i)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libhelm"
|
||||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
"github.com/portainer/docker-compose-wrapper/compose"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
@@ -19,22 +19,26 @@ import (
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/database"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/git"
|
||||
"github.com/portainer/portainer/api/hostmanagement/openamt"
|
||||
"github.com/portainer/portainer/api/http"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/portainer/portainer/api/internal/ssl"
|
||||
"github.com/portainer/portainer/api/internal/upgrade"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
@@ -42,7 +46,10 @@ import (
|
||||
"github.com/portainer/portainer/api/oauth"
|
||||
"github.com/portainer/portainer/api/scheduler"
|
||||
"github.com/portainer/portainer/api/stacks/deployments"
|
||||
"github.com/portainer/portainer/pkg/featureflags"
|
||||
"github.com/portainer/portainer/pkg/libhelm"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -98,8 +105,6 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
|
||||
log.Info().Msg("exiting rollback")
|
||||
os.Exit(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init sets some defaults - it's basically a migration
|
||||
@@ -109,24 +114,27 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
}
|
||||
|
||||
if isNew {
|
||||
// from MigrateData
|
||||
store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
instanceId, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed generating instance id")
|
||||
}
|
||||
|
||||
err := updateSettingsFromFlags(store, flags)
|
||||
// from MigrateData
|
||||
v := models.Version{
|
||||
SchemaVersion: portainer.APIVersion,
|
||||
Edition: int(portainer.PortainerCE),
|
||||
InstanceID: instanceId.String(),
|
||||
}
|
||||
store.VersionService.UpdateVersion(&v)
|
||||
|
||||
err = updateSettingsFromFlags(store, flags)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed updating settings from flags")
|
||||
}
|
||||
} else {
|
||||
storedVersion, err := store.VersionService.DBVersion()
|
||||
err = store.MigrateData()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failure during creation of new database")
|
||||
}
|
||||
|
||||
if storedVersion != portainer.DBVersion {
|
||||
err = store.MigrateData()
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed migration")
|
||||
}
|
||||
log.Fatal().Err(err).Msg("failed migration")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,22 +147,13 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
||||
go func() {
|
||||
<-shutdownCtx.Done()
|
||||
defer connection.Close()
|
||||
|
||||
exportFilename := path.Join(*flags.Data, fmt.Sprintf("export-%d.json", time.Now().Unix()))
|
||||
|
||||
err := store.Export(exportFilename)
|
||||
if err != nil {
|
||||
log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
|
||||
} else {
|
||||
log.Debug().Str("filename", exportFilename).Msg("exported")
|
||||
}
|
||||
}()
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
|
||||
func initComposeStackManager(composeDeployer libstack.Deployer, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed creating compose manager")
|
||||
}
|
||||
@@ -235,18 +234,18 @@ func initSSLService(addr, certPath, keyPath string, fileService portainer.FileSe
|
||||
return sslService, nil
|
||||
}
|
||||
|
||||
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
|
||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *dockerclient.ClientFactory {
|
||||
return dockerclient.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
|
||||
}
|
||||
|
||||
func initSnapshotService(
|
||||
snapshotIntervalFromFlag string,
|
||||
dataStore dataservices.DataStore,
|
||||
dockerClientFactory *docker.ClientFactory,
|
||||
dockerClientFactory *dockerclient.ClientFactory,
|
||||
kubernetesClientFactory *kubecli.ClientFactory,
|
||||
shutdownCtx context.Context,
|
||||
) (portainer.SnapshotService, error) {
|
||||
@@ -316,51 +315,7 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
|
||||
sslSettings.HTTPEnabled = true
|
||||
}
|
||||
|
||||
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// enableFeaturesFromFlags turns on or off feature flags
|
||||
// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true)
|
||||
// note, settings are persisted to the DB. To turn off `--feat open-amt=false`
|
||||
func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if settings.FeatureFlagSettings == nil {
|
||||
settings.FeatureFlagSettings = make(map[portainer.Feature]bool)
|
||||
}
|
||||
|
||||
// loop through feature flags to check if they are supported
|
||||
for _, feat := range *flags.FeatureFlags {
|
||||
var correspondingFeature *portainer.Feature
|
||||
for i, supportedFeat := range portainer.SupportedFeatureFlags {
|
||||
if strings.EqualFold(feat.Name, string(supportedFeat)) {
|
||||
correspondingFeature = &portainer.SupportedFeatureFlags[i]
|
||||
}
|
||||
}
|
||||
|
||||
if correspondingFeature == nil {
|
||||
return fmt.Errorf("unknown feature flag '%s'", feat.Name)
|
||||
}
|
||||
|
||||
featureState, err := strconv.ParseBool(feat.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
|
||||
}
|
||||
|
||||
log.Info().Str("feature", string(*correspondingFeature)).Bool("state", featureState).Msg("")
|
||||
|
||||
settings.FeatureFlagSettings[*correspondingFeature] = featureState
|
||||
}
|
||||
|
||||
return dataStore.Settings().UpdateSettings(settings)
|
||||
return dataStore.SSLSettings().UpdateSettings(sslSettings)
|
||||
}
|
||||
|
||||
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||
@@ -392,147 +347,6 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D
|
||||
return generateAndStoreKeyPair(fileService, signatureService)
|
||||
}
|
||||
|
||||
func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
||||
tlsConfiguration := portainer.TLSConfiguration{
|
||||
TLS: *flags.TLS,
|
||||
TLSSkipVerify: *flags.TLSSkipVerify,
|
||||
}
|
||||
|
||||
if *flags.TLS {
|
||||
tlsConfiguration.TLSCACertPath = *flags.TLSCacert
|
||||
tlsConfiguration.TLSCertPath = *flags.TLSCert
|
||||
tlsConfiguration.TLSKeyPath = *flags.TLSKey
|
||||
} else if !*flags.TLS && *flags.TLSSkipVerify {
|
||||
tlsConfiguration.TLS = true
|
||||
}
|
||||
|
||||
endpointID := dataStore.Endpoint().GetNextIdentifier()
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: "primary",
|
||||
URL: *flags.EndpointURL,
|
||||
GroupID: portainer.EndpointGroupID(1),
|
||||
Type: portainer.DockerEnvironment,
|
||||
TLSConfig: tlsConfiguration,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration.TLSCACertPath, tlsConfiguration.TLSCertPath, tlsConfiguration.TLSKeyPath, tlsConfiguration.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(endpoint.URL, tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if agentOnDockerEnvironment {
|
||||
endpoint.Type = portainer.AgentOnDockerEnvironment
|
||||
}
|
||||
}
|
||||
|
||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("endpoint", endpoint.Name).
|
||||
Str("URL", endpoint.URL).
|
||||
Err(err).
|
||||
Msg("environment snapshot error")
|
||||
}
|
||||
|
||||
return dataStore.Endpoint().Create(endpoint)
|
||||
}
|
||||
|
||||
func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
||||
if strings.HasPrefix(endpointURL, "tcp://") {
|
||||
_, err := client.ExecutePingOperation(endpointURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpointID := dataStore.Endpoint().GetNextIdentifier()
|
||||
endpoint := &portainer.Endpoint{
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: "primary",
|
||||
URL: endpointURL,
|
||||
GroupID: portainer.EndpointGroupID(1),
|
||||
Type: portainer.DockerEnvironment,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str("endpoint", endpoint.Name).
|
||||
Str("URL", endpoint.URL).Err(err).
|
||||
Msg("environment snapshot error")
|
||||
}
|
||||
|
||||
return dataStore.Endpoint().Create(endpoint)
|
||||
}
|
||||
|
||||
func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
||||
if *flags.EndpointURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
endpoints, err := dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(endpoints) > 0 {
|
||||
log.Info().Msg("instance already has defined environments, skipping the environment defined via CLI")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if *flags.TLS || *flags.TLSSkipVerify {
|
||||
return createTLSSecuredEndpoint(flags, dataStore, snapshotService)
|
||||
}
|
||||
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
|
||||
}
|
||||
|
||||
func loadEncryptionSecretKey(keyfilename string) []byte {
|
||||
content, err := os.ReadFile(path.Join("/run/secrets", keyfilename))
|
||||
if err != nil {
|
||||
@@ -553,6 +367,10 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
|
||||
func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
|
||||
|
||||
if flags.FeatureFlags != nil {
|
||||
featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
|
||||
}
|
||||
|
||||
fileService := initFileService(*flags.Data)
|
||||
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
|
||||
if encryptionKey == nil {
|
||||
@@ -582,11 +400,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Err(err).Msg("failed initializing JWT service")
|
||||
}
|
||||
|
||||
err = enableFeaturesFromFlags(dataStore, flags)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed enabling feature flag")
|
||||
}
|
||||
|
||||
ldapService := initLDAPService()
|
||||
|
||||
oauthService := initOAuthService()
|
||||
@@ -599,6 +412,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
edgeStacksService := edgestacks.NewService(dataStore)
|
||||
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
@@ -617,7 +432,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
|
||||
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
|
||||
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
|
||||
if err != nil {
|
||||
@@ -638,7 +453,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
|
||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
||||
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing compose deployer")
|
||||
}
|
||||
|
||||
composeStackManager := initComposeStackManager(composeDeployer, reverseTunnelService, proxyManager)
|
||||
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
|
||||
if err != nil {
|
||||
@@ -667,10 +487,10 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
}
|
||||
|
||||
err = initEndpoint(flags, dataStore, snapshotService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing environment")
|
||||
}
|
||||
// channel to control when the admin user is created
|
||||
adminCreationDone := make(chan struct{}, 1)
|
||||
|
||||
go endpointutils.InitEndpoint(shutdownCtx, adminCreationDone, flags, dataStore, snapshotService)
|
||||
|
||||
adminPasswordHash := ""
|
||||
if *flags.AdminPasswordFile != "" {
|
||||
@@ -705,6 +525,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed creating admin user")
|
||||
}
|
||||
|
||||
// notify the admin user is created, the endpoint initialization can start
|
||||
adminCreationDone <- struct{}{}
|
||||
} else {
|
||||
log.Info().Msg("instance already has an administrator user defined, skipping admin password related flags.")
|
||||
}
|
||||
@@ -716,7 +539,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
}
|
||||
|
||||
scheduler := scheduler.NewScheduler(shutdownCtx)
|
||||
stackDeployer := deployments.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
|
||||
stackDeployer := deployments.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer, dockerClientFactory, dataStore)
|
||||
deployments.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
|
||||
|
||||
sslDBSettings, err := dataStore.SSLSettings().Settings()
|
||||
@@ -724,6 +547,24 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
||||
}
|
||||
|
||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||
}
|
||||
|
||||
// Our normal migrations run as part of the database initialization
|
||||
// but some more complex migrations require access to a kubernetes or docker
|
||||
// client. Therefore we run a separate migration process just before
|
||||
// starting the server.
|
||||
postInitMigrator := datastore.NewPostInitMigrator(
|
||||
kubernetesClientFactory,
|
||||
dockerClientFactory,
|
||||
dataStore,
|
||||
)
|
||||
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
||||
log.Fatal().Err(err).Msg("failure during post init migrations")
|
||||
}
|
||||
|
||||
return &http.Server{
|
||||
AuthorizationService: authorizationService,
|
||||
ReverseTunnelService: reverseTunnelService,
|
||||
@@ -733,6 +574,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
HTTPEnabled: sslDBSettings.HTTPEnabled,
|
||||
AssetsPath: *flags.Assets,
|
||||
DataStore: dataStore,
|
||||
EdgeStacksService: edgeStacksService,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
@@ -758,15 +600,21 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
DemoService: demoService,
|
||||
UpgradeService: upgradeService,
|
||||
AdminCreationDone: adminCreationDone,
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
configureLogger()
|
||||
setLoggingMode("PRETTY")
|
||||
|
||||
flags := initCLI()
|
||||
|
||||
setLoggingLevel(*flags.LogLevel)
|
||||
setLoggingMode(*flags.LogMode)
|
||||
|
||||
for {
|
||||
server := buildServer(flags)
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/alecthomas/kingpin.v2"
|
||||
)
|
||||
|
||||
type mockKingpinSetting string
|
||||
|
||||
func (m mockKingpinSetting) SetValue(value kingpin.Value) {
|
||||
value.Set(string(m))
|
||||
}
|
||||
|
||||
func Test_enableFeaturesFromFlags(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
tests := []struct {
|
||||
featureFlag string
|
||||
isSupported bool
|
||||
}{
|
||||
{"test", false},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(test.featureFlag)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
if test.isSupported {
|
||||
is.NoError(err)
|
||||
} else {
|
||||
is.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("passes for all supported feature flags", func(t *testing.T) {
|
||||
for _, flag := range portainer.SupportedFeatureFlags {
|
||||
mockKingpinSetting := mockKingpinSetting(flag)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const FeatTest portainer.Feature = "optional-test"
|
||||
|
||||
func optionalFunc(dataStore dataservices.DataStore) string {
|
||||
|
||||
// TODO: this is a code smell - finding out if a feature flag is enabled should not require having access to the store, and asking for a settings obj.
|
||||
// ideally, the `if` should look more like:
|
||||
// if featureflags.FlagEnabled(FeatTest) {}
|
||||
settings, err := dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
if settings.FeatureFlagSettings[FeatTest] {
|
||||
return "enabled"
|
||||
}
|
||||
return "disabled"
|
||||
}
|
||||
|
||||
func Test_optionalFeature(t *testing.T) {
|
||||
portainer.SupportedFeatureFlags = append(portainer.SupportedFeatureFlags, FeatTest)
|
||||
|
||||
is := assert.New(t)
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
// Enable the test feature
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(FeatTest)
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
is.Equal("enabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// Same store, so the feature flag should still be enabled
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
is.Equal("enabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// disable the test feature
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
mockKingpinSetting := mockKingpinSetting(FeatTest + "=false")
|
||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
||||
err := enableFeaturesFromFlags(store, flags)
|
||||
is.NoError(err)
|
||||
is.Equal("disabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
// Same store, so feature flag should still be disabled
|
||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
||||
is.Equal("disabled", optionalFunc(store))
|
||||
})
|
||||
|
||||
}
|
||||
@@ -4,10 +4,35 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type ReadTransaction interface {
|
||||
GetObject(bucketName string, key []byte, object interface{}) error
|
||||
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
}
|
||||
|
||||
type Transaction interface {
|
||||
ReadTransaction
|
||||
|
||||
SetServiceName(bucketName string) error
|
||||
UpdateObject(bucketName string, key []byte, object interface{}) error
|
||||
DeleteObject(bucketName string, key []byte) error
|
||||
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
|
||||
CreateObjectWithId(bucketName string, id int, obj interface{}) error
|
||||
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
|
||||
DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error
|
||||
GetNextIdentifier(bucketName string) int
|
||||
}
|
||||
|
||||
type Connection interface {
|
||||
Transaction
|
||||
|
||||
Open() error
|
||||
Close() error
|
||||
|
||||
UpdateTx(fn func(Transaction) error) error
|
||||
ViewTx(fn func(Transaction) error) error
|
||||
|
||||
// write the db contents to filename as json (the schema needs defining)
|
||||
ExportRaw(filename string) error
|
||||
|
||||
@@ -21,19 +46,9 @@ type Connection interface {
|
||||
NeedsEncryptionMigration() (bool, error)
|
||||
SetEncrypted(encrypted bool)
|
||||
|
||||
SetServiceName(bucketName string) error
|
||||
GetObject(bucketName string, key []byte, object interface{}) error
|
||||
UpdateObject(bucketName string, key []byte, object interface{}) error
|
||||
DeleteObject(bucketName string, key []byte) error
|
||||
DeleteAllObjects(bucketName string, matching func(o interface{}) (id int, ok bool)) error
|
||||
GetNextIdentifier(bucketName string) int
|
||||
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
|
||||
CreateObjectWithId(bucketName string, id int, obj interface{}) error
|
||||
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
|
||||
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||
ConvertToKey(v int) []byte
|
||||
|
||||
BackupMetadata() (map[string]interface{}, error)
|
||||
RestoreMetadata(s map[string]interface{}) error
|
||||
|
||||
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
|
||||
ConvertToKey(v int) []byte
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// Person with better knowledge is welcomed to improve it.
|
||||
// sourced from https://golang.org/src/crypto/cipher/example_test.go
|
||||
|
||||
var emptySalt []byte = make([]byte, 0, 0)
|
||||
var emptySalt []byte = make([]byte, 0)
|
||||
|
||||
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
|
||||
// passphrase is used to generate an encryption key.
|
||||
|
||||
@@ -2,7 +2,6 @@ package crypto
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -20,7 +19,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
ioutil.WriteFile(originFilePath, content, 0600)
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
@@ -30,7 +29,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
|
||||
@@ -45,7 +44,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||
}
|
||||
|
||||
@@ -59,7 +58,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
ioutil.WriteFile(originFilePath, content, 0600)
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
@@ -69,7 +68,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
|
||||
@@ -84,7 +83,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||
}
|
||||
|
||||
@@ -98,7 +97,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
||||
)
|
||||
|
||||
content := []byte("content")
|
||||
ioutil.WriteFile(originFilePath, content, 0600)
|
||||
os.WriteFile(originFilePath, content, 0600)
|
||||
|
||||
originFile, _ := os.Open(originFilePath)
|
||||
defer originFile.Close()
|
||||
@@ -108,7 +107,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
||||
|
||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
||||
assert.Nil(t, err, "Failed to encrypt a file")
|
||||
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
|
||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||
|
||||
@@ -123,6 +122,6 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
||||
|
||||
io.Copy(decryptedFileWriter, decryptedReader)
|
||||
|
||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
)
|
||||
@@ -115,9 +114,6 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||
|
||||
hash := libcrypto.HashFromBytes([]byte(message))
|
||||
|
||||
r := big.NewInt(0)
|
||||
s := big.NewInt(0)
|
||||
|
||||
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
@@ -3,11 +3,11 @@ package crypto
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
|
||||
func CreateServerTLSConfiguration() *tls.Config {
|
||||
// CreateTLSConfiguration creates a basic tls.Config with recommended TLS settings
|
||||
func CreateTLSConfiguration() *tls.Config {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
@@ -20,6 +20,8 @@ func CreateServerTLSConfiguration() *tls.Config {
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -27,7 +29,7 @@ func CreateServerTLSConfiguration() *tls.Config {
|
||||
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
// loaded from memory.
|
||||
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
|
||||
config := &tls.Config{}
|
||||
config := CreateTLSConfiguration()
|
||||
config.InsecureSkipVerify = skipServerVerification
|
||||
|
||||
if !skipClientVerification {
|
||||
@@ -50,7 +52,7 @@ func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerific
|
||||
// CreateTLSConfigurationFromDisk initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
// loaded from disk.
|
||||
func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipServerVerification bool) (*tls.Config, error) {
|
||||
config := &tls.Config{}
|
||||
config := CreateTLSConfiguration()
|
||||
config.InsecureSkipVerify = skipServerVerification
|
||||
|
||||
if certPath != "" && keyPath != "" {
|
||||
@@ -63,7 +65,7 @@ func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipSe
|
||||
}
|
||||
|
||||
if !skipServerVerification && caCertPath != "" {
|
||||
caCert, err := ioutil.ReadFile(caCertPath)
|
||||
caCert, err := os.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -133,9 +133,11 @@ func (connection *DbConnection) Open() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db.MaxBatchSize = connection.MaxBatchSize
|
||||
db.MaxBatchDelay = connection.MaxBatchDelay
|
||||
connection.DB = db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -145,9 +147,30 @@ func (connection *DbConnection) Close() error {
|
||||
if connection.DB != nil {
|
||||
return connection.DB.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (connection *DbConnection) txFn(fn func(portainer.Transaction) error) func(*bolt.Tx) error {
|
||||
return func(tx *bolt.Tx) error {
|
||||
return fn(&DbTransaction{conn: connection, tx: tx})
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTx executes the given function inside a read-write transaction
|
||||
func (connection *DbConnection) UpdateTx(fn func(portainer.Transaction) error) error {
|
||||
if connection.MaxBatchDelay > 0 && connection.MaxBatchSize > 1 {
|
||||
return connection.Batch(connection.txFn(fn))
|
||||
}
|
||||
|
||||
return connection.Update(connection.txFn(fn))
|
||||
}
|
||||
|
||||
// ViewTx executes the given function inside a read-only transaction
|
||||
func (connection *DbConnection) ViewTx(fn func(portainer.Transaction) error) error {
|
||||
return connection.View(connection.txFn(fn))
|
||||
}
|
||||
|
||||
// BackupTo backs up db to a provided writer.
|
||||
// It does hot backup and doesn't block other database reads and writes
|
||||
func (connection *DbConnection) BackupTo(w io.Writer) error {
|
||||
@@ -160,14 +183,14 @@ func (connection *DbConnection) BackupTo(w io.Writer) error {
|
||||
func (connection *DbConnection) ExportRaw(filename string) error {
|
||||
databasePath := connection.GetDatabaseFilePath()
|
||||
if _, err := os.Stat(databasePath); err != nil {
|
||||
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
|
||||
return fmt.Errorf("stat on %s failed, error: %w", databasePath, err)
|
||||
}
|
||||
|
||||
b, err := connection.ExportJSON(databasePath, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(filename, b, 0600)
|
||||
return os.WriteFile(filename, b, 0600)
|
||||
}
|
||||
|
||||
// ConvertToKey returns an 8-byte big endian representation of v.
|
||||
@@ -179,36 +202,32 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
// CreateBucket is a generic function used to create a bucket inside a database database.
|
||||
// keyToString Converts a key to a string value suitable for logging
|
||||
func keyToString(b []byte) string {
|
||||
if len(b) != 8 {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
v := binary.BigEndian.Uint64(b)
|
||||
if v <= math.MaxInt32 {
|
||||
return fmt.Sprintf("%d", v)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// CreateBucket is a generic function used to create a bucket inside a database.
|
||||
func (connection *DbConnection) SetServiceName(bucketName string) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
return err
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.SetServiceName(bucketName)
|
||||
})
|
||||
}
|
||||
|
||||
// GetObject is a generic function used to retrieve an unmarshalled object from a database database.
|
||||
// GetObject is a generic function used to retrieve an unmarshalled object from a database.
|
||||
func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error {
|
||||
var data []byte
|
||||
|
||||
err := connection.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
|
||||
value := bucket.Get(key)
|
||||
if value == nil {
|
||||
return dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
|
||||
return nil
|
||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetObject(bucketName, key, object)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return connection.UnmarshalObjectWithJsoniter(data, object)
|
||||
}
|
||||
|
||||
func (connection *DbConnection) getEncryptionKey() []byte {
|
||||
@@ -219,50 +238,51 @@ func (connection *DbConnection) getEncryptionKey() []byte {
|
||||
return connection.EncryptionKey
|
||||
}
|
||||
|
||||
// UpdateObject is a generic function used to update an object inside a database database.
|
||||
// UpdateObject is a generic function used to update an object inside a database.
|
||||
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
||||
data, err := connection.MarshalObject(object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.UpdateObject(bucketName, key, object)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateObjectFunc is a generic function used to update an object safely without race conditions.
|
||||
func (connection *DbConnection) UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
|
||||
data := bucket.Get(key)
|
||||
if data == nil {
|
||||
return fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||
}
|
||||
|
||||
err := connection.UnmarshalObjectWithJsoniter(data, object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateFn()
|
||||
|
||||
data, err = connection.MarshalObject(object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(key, data)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteObject is a generic function used to delete an object inside a database database.
|
||||
// DeleteObject is a generic function used to delete an object inside a database.
|
||||
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
return bucket.Delete(key)
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.DeleteObject(bucketName, key)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAllObjects delete all objects where matching() returns (id, ok).
|
||||
// TODO: think about how to return the error inside (maybe change ok to type err, and use "notfound"?
|
||||
func (connection *DbConnection) DeleteAllObjects(bucketName string, matching func(o interface{}) (id int, ok bool)) error {
|
||||
return connection.Batch(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 obj interface{}
|
||||
err := connection.UnmarshalObject(v, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if id, ok := matching(obj); ok {
|
||||
err := bucket.Delete(connection.ConvertToKey(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.DeleteAllObjects(bucketName, obj, matching)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -270,13 +290,8 @@ func (connection *DbConnection) DeleteAllObjects(bucketName string, matching fun
|
||||
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
|
||||
var identifier int
|
||||
|
||||
connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
identifier = int(id)
|
||||
_ = connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
identifier = tx.GetNextIdentifier(bucketName)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -285,108 +300,41 @@ func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
|
||||
|
||||
// CreateObject creates a new object in the bucket, using the next bucket sequence id
|
||||
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
|
||||
seqId, _ := bucket.NextSequence()
|
||||
id, obj := fn(seqId)
|
||||
|
||||
data, err := connection.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(connection.ConvertToKey(int(id)), data)
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.CreateObject(bucketName, fn)
|
||||
})
|
||||
}
|
||||
|
||||
// CreateObjectWithId creates a new object in the bucket, using the specified id
|
||||
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
data, err := connection.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(connection.ConvertToKey(id), data)
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.CreateObjectWithId(bucketName, id, obj)
|
||||
})
|
||||
}
|
||||
|
||||
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
|
||||
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
|
||||
return connection.Batch(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
data, err := connection.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(id, data)
|
||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.CreateObjectWithStringId(bucketName, id, obj)
|
||||
})
|
||||
}
|
||||
|
||||
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
err := connection.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() {
|
||||
err := connection.UnmarshalObject(v, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj, err = append(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetAll(bucketName, obj, append)
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: decide which Unmarshal to use, and use one...
|
||||
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
err := connection.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() {
|
||||
err := connection.UnmarshalObjectWithJsoniter(v, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj, err = append(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetAllWithJsoniter(bucketName, obj, append)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
return connection.View(func(tx *bolt.Tx) error {
|
||||
cursor := tx.Bucket([]byte(bucketName)).Cursor()
|
||||
|
||||
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
|
||||
err := connection.UnmarshalObjectWithJsoniter(v, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj, err = append(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, append)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -397,7 +345,6 @@ func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error)
|
||||
err := connection.View(func(tx *bolt.Tx) error {
|
||||
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
bucketName := string(name)
|
||||
bucket = tx.Bucket([]byte(bucketName))
|
||||
seqId := bucket.Sequence()
|
||||
buckets[bucketName] = int(seqId)
|
||||
return nil
|
||||
|
||||
@@ -14,7 +14,6 @@ func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
|
||||
err := connection.View(func(tx *bolt.Tx) error {
|
||||
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||
bucketName := string(name)
|
||||
bucket = tx.Bucket([]byte(bucketName))
|
||||
seqId := bucket.Sequence()
|
||||
buckets[bucketName] = int(seqId)
|
||||
return nil
|
||||
|
||||
@@ -129,7 +129,7 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
|
||||
var object string
|
||||
err := conn.UnmarshalObject(test.object, &object)
|
||||
is.NoError(err)
|
||||
is.Equal(test.expected, string(object))
|
||||
is.Equal(test.expected, object)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
172
api/database/boltdb/tx.go
Normal file
172
api/database/boltdb/tx.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
type DbTransaction struct {
|
||||
conn *DbConnection
|
||||
tx *bolt.Tx
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) SetServiceName(bucketName string) error {
|
||||
_, err := tx.tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||
return err
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interface{}) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
value := bucket.Get(key)
|
||||
if value == nil {
|
||||
return fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||
}
|
||||
|
||||
data := make([]byte, len(value))
|
||||
copy(data, value)
|
||||
|
||||
return tx.conn.UnmarshalObjectWithJsoniter(data, object)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
||||
data, err := tx.conn.MarshalObject(object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
return bucket.Put(key, data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) DeleteObject(bucketName string, key []byte) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
return bucket.Delete(key)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
err := tx.conn.UnmarshalObject(v, &obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if id, ok := matching(obj); ok {
|
||||
err := bucket.Delete(tx.conn.ConvertToKey(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifer")
|
||||
return 0
|
||||
}
|
||||
|
||||
return int(id)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
seqId, _ := bucket.NextSequence()
|
||||
id, obj := fn(seqId)
|
||||
|
||||
data, err := tx.conn.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
data, err := tx.conn.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
data, err := tx.conn.MarshalObject(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put(id, data)
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
err := tx.conn.UnmarshalObject(v, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj, err = append(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj, err = append(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||
cursor := tx.tx.Bucket([]byte(bucketName)).Cursor()
|
||||
|
||||
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
|
||||
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
obj, err = append(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
126
api/database/boltdb/tx_test.go
Normal file
126
api/database/boltdb/tx_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package boltdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const testBucketName = "test-bucket"
|
||||
const testId = 1234
|
||||
|
||||
type testStruct struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func TestTxs(t *testing.T) {
|
||||
conn := DbConnection{
|
||||
Path: t.TempDir(),
|
||||
}
|
||||
|
||||
err := conn.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Error propagation
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return errors.New("this is an error")
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("an error was expected, got nil instead")
|
||||
}
|
||||
|
||||
// Create an object
|
||||
newObj := testStruct{
|
||||
Key: "key",
|
||||
Value: "value",
|
||||
}
|
||||
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
err = tx.SetServiceName(testBucketName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.CreateObjectWithId(testBucketName, testId, newObj)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
obj := testStruct{}
|
||||
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if obj.Key != newObj.Key || obj.Value != newObj.Value {
|
||||
t.Fatalf("expected %s:%s, got %s:%s instead", newObj.Key, newObj.Value, obj.Key, obj.Value)
|
||||
}
|
||||
|
||||
// Update an object
|
||||
updatedObj := testStruct{
|
||||
Key: "updated-key",
|
||||
Value: "updated-value",
|
||||
}
|
||||
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.UpdateObject(testBucketName, conn.ConvertToKey(testId), &updatedObj)
|
||||
})
|
||||
|
||||
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if obj.Key != updatedObj.Key || obj.Value != updatedObj.Value {
|
||||
t.Fatalf("expected %s:%s, got %s:%s instead", updatedObj.Key, updatedObj.Value, obj.Key, obj.Value)
|
||||
}
|
||||
|
||||
// Delete an object
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return tx.DeleteObject(testBucketName, conn.ConvertToKey(testId))
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||
})
|
||||
if !dataservices.IsErrObjectNotFound(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get next identifier
|
||||
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||
id1 := tx.GetNextIdentifier(testBucketName)
|
||||
id2 := tx.GetNextIdentifier(testBucketName)
|
||||
|
||||
if id1+1 != id2 {
|
||||
return errors.New("unexpected identifier sequence")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Try to write in a read transaction
|
||||
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||
return tx.CreateObjectWithId(testBucketName, testId, newObj)
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("an error was expected, got nil instead")
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,7 @@ import (
|
||||
|
||||
// NewDatabase should use config options to return a connection to the requested database
|
||||
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
|
||||
switch storeType {
|
||||
case "boltdb":
|
||||
if storeType == "boltdb" {
|
||||
return &boltdb.DbConnection{
|
||||
Path: storePath,
|
||||
EncryptionKey: encryptionKey,
|
||||
|
||||
8
api/database/models/version.go
Normal file
8
api/database/models/version.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
type Version struct {
|
||||
SchemaVersion string
|
||||
MigratorCount int
|
||||
Edition int
|
||||
InstanceID string
|
||||
}
|
||||
@@ -2,10 +2,11 @@ package apikeyrepository
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -78,12 +79,12 @@ func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, err
|
||||
return &portainer.APIKey{}, nil
|
||||
})
|
||||
|
||||
if err == stop {
|
||||
if errors.Is(err, stop) {
|
||||
return k, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, errors.ErrObjectNotFound
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
package edgegroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edgegroups"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "edgegroups"
|
||||
|
||||
// Service represents a service for managing Edge group data.
|
||||
type Service struct {
|
||||
@@ -34,59 +28,65 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EdgeGroups return an array containing all the Edge groups.
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// EdgeGroups return a slice containing all the Edge groups.
|
||||
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
|
||||
var groups = make([]portainer.EdgeGroup, 0)
|
||||
var groups []portainer.EdgeGroup
|
||||
var err error
|
||||
|
||||
err := service.connection.GetAllWithJsoniter(
|
||||
BucketName,
|
||||
&portainer.EdgeGroup{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
group, ok := obj.(*portainer.EdgeGroup)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
|
||||
return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
|
||||
}
|
||||
groups = append(groups, *group)
|
||||
|
||||
return &portainer.EdgeGroup{}, nil
|
||||
})
|
||||
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
groups, err = service.Tx(tx).EdgeGroups()
|
||||
return err
|
||||
})
|
||||
|
||||
return groups, err
|
||||
}
|
||||
|
||||
// EdgeGroup returns an Edge group by ID.
|
||||
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
|
||||
var group portainer.EdgeGroup
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
var group *portainer.EdgeGroup
|
||||
var err error
|
||||
|
||||
err := service.connection.GetObject(BucketName, identifier, &group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
group, err = service.Tx(tx).EdgeGroup(ID)
|
||||
return err
|
||||
})
|
||||
|
||||
return &group, nil
|
||||
return group, err
|
||||
}
|
||||
|
||||
// UpdateEdgeGroup updates an Edge group.
|
||||
// UpdateEdgeGroup updates an edge group.
|
||||
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, group)
|
||||
}
|
||||
|
||||
// Deprecated: UpdateEdgeGroupFunc updates an edge group inside a transaction avoiding data races.
|
||||
func (service *Service) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
|
||||
id := service.connection.ConvertToKey(int(ID))
|
||||
edgeGroup := &portainer.EdgeGroup{}
|
||||
|
||||
return service.connection.UpdateObjectFunc(BucketName, id, edgeGroup, func() {
|
||||
updateFunc(edgeGroup)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteEdgeGroup deletes an Edge group.
|
||||
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).DeleteEdgeGroup(ID)
|
||||
})
|
||||
}
|
||||
|
||||
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
|
||||
func (service *Service) Create(group *portainer.EdgeGroup) error {
|
||||
return service.connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
group.ID = portainer.EdgeGroupID(id)
|
||||
return int(group.ID), group
|
||||
},
|
||||
)
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Create(group)
|
||||
})
|
||||
}
|
||||
|
||||
80
api/dataservices/edgegroup/tx.go
Normal file
80
api/dataservices/edgegroup/tx.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package edgegroup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// EdgeGroups return a slice containing all the Edge groups.
|
||||
func (service ServiceTx) EdgeGroups() ([]portainer.EdgeGroup, error) {
|
||||
var groups = make([]portainer.EdgeGroup, 0)
|
||||
|
||||
err := service.tx.GetAllWithJsoniter(
|
||||
BucketName,
|
||||
&portainer.EdgeGroup{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
group, ok := obj.(*portainer.EdgeGroup)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
|
||||
return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
|
||||
}
|
||||
groups = append(groups, *group)
|
||||
|
||||
return &portainer.EdgeGroup{}, nil
|
||||
})
|
||||
|
||||
return groups, err
|
||||
}
|
||||
|
||||
// EdgeGroup returns an Edge group by ID.
|
||||
func (service ServiceTx) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
|
||||
var group portainer.EdgeGroup
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// UpdateEdgeGroup updates an edge group.
|
||||
func (service ServiceTx) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, group)
|
||||
}
|
||||
|
||||
// UpdateEdgeGroupFunc is a no-op inside a transaction.
|
||||
func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
// DeleteEdgeGroup deletes an Edge group.
|
||||
func (service ServiceTx) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
|
||||
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
|
||||
return service.tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
group.ID = portainer.EdgeGroupID(id)
|
||||
return int(group.ID), group
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edgejobs"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "edgejobs"
|
||||
|
||||
// Service represents a service for managing edge jobs data.
|
||||
type Service struct {
|
||||
@@ -34,6 +32,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// EdgeJobs returns a list of Edge jobs
|
||||
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
|
||||
var edgeJobs = make([]portainer.EdgeJob, 0)
|
||||
@@ -80,12 +85,22 @@ func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJo
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateEdgeJob updates an Edge job by ID
|
||||
// Deprecated: use UpdateEdgeJobFunc instead
|
||||
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, edgeJob)
|
||||
}
|
||||
|
||||
// UpdateEdgeJobFunc updates an edge job inside a transaction avoiding data races.
|
||||
func (service *Service) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
|
||||
id := service.connection.ConvertToKey(int(ID))
|
||||
edgeJob := &portainer.EdgeJob{}
|
||||
|
||||
return service.connection.UpdateObjectFunc(BucketName, id, edgeJob, func() {
|
||||
updateFunc(edgeJob)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteEdgeJob deletes an Edge job
|
||||
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
84
api/dataservices/edgejob/tx.go
Normal file
84
api/dataservices/edgejob/tx.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package edgejob
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// EdgeJobs returns a list of Edge jobs
|
||||
func (service ServiceTx) EdgeJobs() ([]portainer.EdgeJob, error) {
|
||||
var edgeJobs = make([]portainer.EdgeJob, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.EdgeJob{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
job, ok := obj.(*portainer.EdgeJob)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
|
||||
return nil, fmt.Errorf("failed to convert to EdgeJob object: %s", obj)
|
||||
}
|
||||
|
||||
edgeJobs = append(edgeJobs, *job)
|
||||
|
||||
return &portainer.EdgeJob{}, nil
|
||||
})
|
||||
|
||||
return edgeJobs, err
|
||||
}
|
||||
|
||||
// EdgeJob returns an Edge job by ID
|
||||
func (service ServiceTx) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
|
||||
var edgeJob portainer.EdgeJob
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &edgeJob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &edgeJob, nil
|
||||
}
|
||||
|
||||
// Create creates a new EdgeJob
|
||||
func (service ServiceTx) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
edgeJob.ID = ID
|
||||
|
||||
return service.tx.CreateObjectWithId(BucketName, int(edgeJob.ID), edgeJob)
|
||||
}
|
||||
|
||||
// UpdateEdgeJob updates an edge job
|
||||
func (service ServiceTx) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, edgeJob)
|
||||
}
|
||||
|
||||
// UpdateEdgeJobFunc is a no-op inside a transaction.
|
||||
func (service ServiceTx) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
// DeleteEdgeJob deletes an Edge job
|
||||
func (service ServiceTx) DeleteEdgeJob(ID portainer.EdgeJobID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.tx.GetNextIdentifier(BucketName)
|
||||
}
|
||||
@@ -2,20 +2,22 @@ package edgestack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edge_stack"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "edge_stack"
|
||||
|
||||
// Service represents a service for managing Edge stack data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
connection portainer.Connection
|
||||
idxVersion map[portainer.EdgeStackID]int
|
||||
mu sync.RWMutex
|
||||
cacheInvalidationFn func(portainer.EdgeStackID)
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
@@ -23,15 +25,39 @@ func (service *Service) BucketName() string {
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.EdgeStackID)) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
s := &Service{
|
||||
connection: connection,
|
||||
idxVersion: make(map[portainer.EdgeStackID]int),
|
||||
cacheInvalidationFn: cacheInvalidationFn,
|
||||
}
|
||||
|
||||
if s.cacheInvalidationFn == nil {
|
||||
s.cacheInvalidationFn = func(portainer.EdgeStackID) {}
|
||||
}
|
||||
|
||||
es, err := s.EdgeStacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, e := range es {
|
||||
s.idxVersion[e.ID] = e.Version
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// EdgeStacks returns an array containing all edge stacks
|
||||
@@ -42,7 +68,6 @@ func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
|
||||
BucketName,
|
||||
&portainer.EdgeStack{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
//var tag portainer.Tag
|
||||
stack, ok := obj.(*portainer.EdgeStack)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
|
||||
@@ -70,28 +95,92 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
|
||||
return &stack, nil
|
||||
}
|
||||
|
||||
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
|
||||
func (service *Service) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
|
||||
service.mu.RLock()
|
||||
v, ok := service.idxVersion[ID]
|
||||
service.mu.RUnlock()
|
||||
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// CreateEdgeStack saves an Edge stack object to db.
|
||||
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
|
||||
edgeStack.ID = id
|
||||
|
||||
return service.connection.CreateObjectWithId(
|
||||
err := service.connection.CreateObjectWithId(
|
||||
BucketName,
|
||||
int(edgeStack.ID),
|
||||
edgeStack,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.mu.Lock()
|
||||
service.idxVersion[id] = edgeStack.Version
|
||||
service.cacheInvalidationFn(id)
|
||||
service.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEdgeStack updates an Edge stack.
|
||||
// Deprecated: Use UpdateEdgeStackFunc instead.
|
||||
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, edgeStack)
|
||||
|
||||
err := service.connection.UpdateObject(BucketName, identifier, edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.idxVersion[ID] = edgeStack.Version
|
||||
service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEdgeStackFunc updates an Edge stack inside a transaction avoiding data races.
|
||||
func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||
id := service.connection.ConvertToKey(int(ID))
|
||||
edgeStack := &portainer.EdgeStack{}
|
||||
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
return service.connection.UpdateObjectFunc(BucketName, id, edgeStack, func() {
|
||||
updateFunc(edgeStack)
|
||||
|
||||
service.idxVersion[ID] = edgeStack.Version
|
||||
service.cacheInvalidationFn(ID)
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateEdgeStackFuncTx is a helper function used to call UpdateEdgeStackFunc inside a transaction.
|
||||
func (service *Service) UpdateEdgeStackFuncTx(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||
return service.Tx(tx).UpdateEdgeStackFunc(ID, updateFunc)
|
||||
}
|
||||
|
||||
// DeleteEdgeStack deletes an Edge stack.
|
||||
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
|
||||
err := service.connection.DeleteObject(BucketName, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(service.idxVersion, ID)
|
||||
|
||||
service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
|
||||
137
api/dataservices/edgestack/tx.go
Normal file
137
api/dataservices/edgestack/tx.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package edgestack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// EdgeStacks returns an array containing all edge stacks
|
||||
func (service ServiceTx) EdgeStacks() ([]portainer.EdgeStack, error) {
|
||||
var stacks = make([]portainer.EdgeStack, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.EdgeStack{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
stack, ok := obj.(*portainer.EdgeStack)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
|
||||
return nil, fmt.Errorf("failed to convert to EdgeStack object: %s", obj)
|
||||
}
|
||||
|
||||
stacks = append(stacks, *stack)
|
||||
|
||||
return &portainer.EdgeStack{}, nil
|
||||
})
|
||||
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
// EdgeStack returns an Edge stack by ID.
|
||||
func (service ServiceTx) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
|
||||
var stack portainer.EdgeStack
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stack, nil
|
||||
}
|
||||
|
||||
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
|
||||
func (service ServiceTx) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
|
||||
service.service.mu.RLock()
|
||||
v, ok := service.service.idxVersion[ID]
|
||||
service.service.mu.RUnlock()
|
||||
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// CreateEdgeStack saves an Edge stack object to db.
|
||||
func (service ServiceTx) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
edgeStack.ID = id
|
||||
|
||||
err := service.tx.CreateObjectWithId(
|
||||
BucketName,
|
||||
int(edgeStack.ID),
|
||||
edgeStack,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
service.service.idxVersion[id] = edgeStack.Version
|
||||
service.service.cacheInvalidationFn(id)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEdgeStack updates an Edge stack.
|
||||
func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||
service.service.mu.Lock()
|
||||
defer service.service.mu.Unlock()
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.UpdateObject(BucketName, identifier, edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.idxVersion[ID] = edgeStack.Version
|
||||
service.service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: use UpdateEdgeStack inside a transaction instead.
|
||||
func (service ServiceTx) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||
edgeStack, err := service.EdgeStack(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateFunc(edgeStack)
|
||||
|
||||
return service.UpdateEdgeStack(ID, edgeStack)
|
||||
}
|
||||
|
||||
// DeleteEdgeStack deletes an Edge stack.
|
||||
func (service ServiceTx) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||
service.service.mu.Lock()
|
||||
defer service.service.mu.Unlock()
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.DeleteObject(BucketName, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
delete(service.service.idxVersion, ID)
|
||||
|
||||
service.service.cacheInvalidationFn(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.tx.GetNextIdentifier(BucketName)
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
package edgeupdateschedule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "edge_update_schedule"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Edge Update Schedule data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
|
||||
mu sync.Mutex
|
||||
idxActiveSchedules map[portainer.EndpointID]*edgetypes.EndpointUpdateScheduleRelation
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
connection: connection,
|
||||
}
|
||||
|
||||
service.idxActiveSchedules = map[portainer.EndpointID]*edgetypes.EndpointUpdateScheduleRelation{}
|
||||
|
||||
schedules, err := service.List()
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "Unable to list schedules")
|
||||
}
|
||||
|
||||
for _, schedule := range schedules {
|
||||
service.setRelation(&schedule)
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (service *Service) ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
return service.idxActiveSchedules[environmentID]
|
||||
}
|
||||
|
||||
func (service *Service) ActiveSchedules(environmentsIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
schedules := []edgetypes.EndpointUpdateScheduleRelation{}
|
||||
|
||||
for _, environmentID := range environmentsIDs {
|
||||
if s, ok := service.idxActiveSchedules[environmentID]; ok {
|
||||
schedules = append(schedules, *s)
|
||||
}
|
||||
}
|
||||
|
||||
return schedules
|
||||
}
|
||||
|
||||
// List return an array containing all the items in the bucket.
|
||||
func (service *Service) List() ([]edgetypes.UpdateSchedule, error) {
|
||||
var list = make([]edgetypes.UpdateSchedule, 0)
|
||||
|
||||
err := service.connection.GetAll(
|
||||
BucketName,
|
||||
&edgetypes.UpdateSchedule{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
item, ok := obj.(*edgetypes.UpdateSchedule)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeUpdateSchedule object")
|
||||
return nil, fmt.Errorf("Failed to convert to EdgeUpdateSchedule object: %s", obj)
|
||||
}
|
||||
list = append(list, *item)
|
||||
return &edgetypes.UpdateSchedule{}, nil
|
||||
})
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// Item returns a item by ID.
|
||||
func (service *Service) Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error) {
|
||||
var item edgetypes.UpdateSchedule
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.connection.GetObject(BucketName, identifier, &item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
// Create assign an ID to a new object and saves it.
|
||||
func (service *Service) Create(item *edgetypes.UpdateSchedule) error {
|
||||
err := service.connection.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
item.ID = edgetypes.UpdateScheduleID(id)
|
||||
return int(item.ID), item
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.setRelation(item)
|
||||
}
|
||||
|
||||
// Update updates an item.
|
||||
func (service *Service) Update(id edgetypes.UpdateScheduleID, item *edgetypes.UpdateSchedule) error {
|
||||
identifier := service.connection.ConvertToKey(int(id))
|
||||
err := service.connection.UpdateObject(BucketName, identifier, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.cleanRelation(id)
|
||||
|
||||
return service.setRelation(item)
|
||||
}
|
||||
|
||||
// Delete deletes an item.
|
||||
func (service *Service) Delete(id edgetypes.UpdateScheduleID) error {
|
||||
|
||||
service.cleanRelation(id)
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(id))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
|
||||
func (service *Service) cleanRelation(id edgetypes.UpdateScheduleID) {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
for _, schedule := range service.idxActiveSchedules {
|
||||
if schedule != nil && schedule.ScheduleID == id {
|
||||
delete(service.idxActiveSchedules, schedule.EnvironmentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) setRelation(schedule *edgetypes.UpdateSchedule) error {
|
||||
service.mu.Lock()
|
||||
defer service.mu.Unlock()
|
||||
|
||||
for environmentID, environmentStatus := range schedule.Status {
|
||||
if environmentStatus.Status != edgetypes.UpdateScheduleStatusPending {
|
||||
continue
|
||||
}
|
||||
|
||||
// this should never happen
|
||||
if service.idxActiveSchedules[environmentID] != nil && service.idxActiveSchedules[environmentID].ScheduleID != schedule.ID {
|
||||
return errors.New("Multiple schedules are pending for the same environment")
|
||||
}
|
||||
|
||||
service.idxActiveSchedules[environmentID] = &edgetypes.EndpointUpdateScheduleRelation{
|
||||
EnvironmentID: environmentID,
|
||||
ScheduleID: schedule.ID,
|
||||
TargetVersion: environmentStatus.TargetVersion,
|
||||
Status: environmentStatus.Status,
|
||||
Error: environmentStatus.Error,
|
||||
Type: schedule.Type,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "endpoints"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "endpoints"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
mu sync.RWMutex
|
||||
idxEdgeID map[string]portainer.EndpointID
|
||||
heartbeats sync.Map
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
@@ -29,64 +29,130 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
s := &Service{
|
||||
connection: connection,
|
||||
}, nil
|
||||
}
|
||||
idxEdgeID: make(map[string]portainer.EndpointID),
|
||||
}
|
||||
|
||||
// Endpoint returns an environment(endpoint) by ID.
|
||||
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||
var endpoint portainer.Endpoint
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.connection.GetObject(BucketName, identifier, &endpoint)
|
||||
es, err := s.endpoints()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &endpoint, nil
|
||||
for _, e := range es {
|
||||
if len(e.EdgeID) > 0 {
|
||||
s.idxEdgeID[e.EdgeID] = e.ID
|
||||
}
|
||||
|
||||
s.heartbeats.Store(e.ID, e.LastCheckInDate)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoint returns an environment(endpoint) by ID.
|
||||
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||
var endpoint *portainer.Endpoint
|
||||
var err error
|
||||
|
||||
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
endpoint, err = service.Tx(tx).Endpoint(ID)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint.LastCheckInDate, _ = service.Heartbeat(ID)
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
// UpdateEndpoint updates an environment(endpoint).
|
||||
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, endpoint)
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).UpdateEndpoint(ID, endpoint)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteEndpoint deletes an environment(endpoint).
|
||||
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).DeleteEndpoint(ID)
|
||||
})
|
||||
}
|
||||
|
||||
// Endpoints return an array containing all the environments(endpoints).
|
||||
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
func (service *Service) endpoints() ([]portainer.Endpoint, error) {
|
||||
var endpoints []portainer.Endpoint
|
||||
var err error
|
||||
|
||||
err := service.connection.GetAllWithJsoniter(
|
||||
BucketName,
|
||||
&portainer.Endpoint{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
endpoint, ok := obj.(*portainer.Endpoint)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Endpoint object")
|
||||
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, *endpoint)
|
||||
|
||||
return &portainer.Endpoint{}, nil
|
||||
})
|
||||
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
endpoints, err = service.Tx(tx).Endpoints()
|
||||
return err
|
||||
})
|
||||
|
||||
return endpoints, err
|
||||
}
|
||||
|
||||
// Endpoints return an array containing all the environments(endpoints).
|
||||
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||
endpoints, err := service.endpoints()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, e := range endpoints {
|
||||
t, _ := service.Heartbeat(e.ID)
|
||||
endpoints[i].LastCheckInDate = t
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// EndpointIDByEdgeID returns the EndpointID from the given EdgeID using an in-memory index
|
||||
func (service *Service) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
|
||||
service.mu.RLock()
|
||||
endpointID, ok := service.idxEdgeID[edgeID]
|
||||
service.mu.RUnlock()
|
||||
|
||||
return endpointID, ok
|
||||
}
|
||||
|
||||
func (service *Service) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
|
||||
if t, ok := service.heartbeats.Load(endpointID); ok {
|
||||
return t.(int64), true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (service *Service) UpdateHeartbeat(endpointID portainer.EndpointID) {
|
||||
service.heartbeats.Store(endpointID, time.Now().Unix())
|
||||
}
|
||||
|
||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||
func (service *Service) Create(endpoint *portainer.Endpoint) error {
|
||||
return service.connection.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
|
||||
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return service.Tx(tx).Create(endpoint)
|
||||
})
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.connection.GetNextIdentifier(BucketName)
|
||||
var identifier int
|
||||
|
||||
service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
identifier = service.Tx(tx).GetNextIdentifier()
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return identifier
|
||||
}
|
||||
|
||||
137
api/dataservices/endpoint/tx.go
Normal file
137
api/dataservices/endpoint/tx.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// Endpoint returns an environment(endpoint) by ID.
|
||||
func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||
var endpoint portainer.Endpoint
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &endpoint, nil
|
||||
}
|
||||
|
||||
// UpdateEndpoint updates an environment(endpoint).
|
||||
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.UpdateObject(BucketName, identifier, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
if len(endpoint.EdgeID) > 0 {
|
||||
service.service.idxEdgeID[endpoint.EdgeID] = ID
|
||||
}
|
||||
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
cache.Del(endpoint.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpoint deletes an environment(endpoint).
|
||||
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.DeleteObject(BucketName, identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
for edgeID, endpointID := range service.service.idxEdgeID {
|
||||
if endpointID == ID {
|
||||
delete(service.service.idxEdgeID, edgeID)
|
||||
break
|
||||
}
|
||||
}
|
||||
service.service.heartbeats.Delete(ID)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
cache.Del(ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Endpoints return an array containing all the environments(endpoints).
|
||||
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
|
||||
var endpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
err := service.tx.GetAllWithJsoniter(
|
||||
BucketName,
|
||||
&portainer.Endpoint{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
endpoint, ok := obj.(*portainer.Endpoint)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Endpoint object")
|
||||
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, *endpoint)
|
||||
|
||||
return &portainer.Endpoint{}, nil
|
||||
})
|
||||
|
||||
return endpoints, err
|
||||
}
|
||||
|
||||
func (service ServiceTx) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
|
||||
log.Error().Str("func", "EndpointIDByEdgeID").Msg("cannot be called inside a transaction")
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (service ServiceTx) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
|
||||
log.Error().Str("func", "Heartbeat").Msg("cannot be called inside a transaction")
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (service ServiceTx) UpdateHeartbeat(endpointID portainer.EndpointID) {
|
||||
log.Error().Str("func", "UpdateHeartbeat").Msg("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
|
||||
err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.service.mu.Lock()
|
||||
if len(endpoint.EdgeID) > 0 {
|
||||
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
|
||||
}
|
||||
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
|
||||
service.service.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||
func (service ServiceTx) GetNextIdentifier() int {
|
||||
return service.tx.GetNextIdentifier(BucketName)
|
||||
}
|
||||
@@ -34,6 +34,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// EndpointGroup returns an environment(endpoint) group by ID.
|
||||
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
|
||||
var endpointGroup portainer.EndpointGroup
|
||||
|
||||
76
api/dataservices/endpointgroup/tx.go
Normal file
76
api/dataservices/endpointgroup/tx.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package endpointgroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// EndpointGroup returns an environment(endpoint) group by ID.
|
||||
func (service ServiceTx) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
|
||||
var endpointGroup portainer.EndpointGroup
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &endpointGroup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &endpointGroup, nil
|
||||
}
|
||||
|
||||
// UpdateEndpointGroup updates an environment(endpoint) group.
|
||||
func (service ServiceTx) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, endpointGroup)
|
||||
}
|
||||
|
||||
// DeleteEndpointGroup deletes an environment(endpoint) group.
|
||||
func (service ServiceTx) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
|
||||
// EndpointGroups return an array containing all the environment(endpoint) groups.
|
||||
func (service ServiceTx) EndpointGroups() ([]portainer.EndpointGroup, error) {
|
||||
var endpointGroups = make([]portainer.EndpointGroup, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.EndpointGroup{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
endpointGroup, ok := obj.(*portainer.EndpointGroup)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointGroup object")
|
||||
return nil, fmt.Errorf("failed to convert to EndpointGroup object: %s", obj)
|
||||
}
|
||||
|
||||
endpointGroups = append(endpointGroups, *endpointGroup)
|
||||
|
||||
return &portainer.EndpointGroup{}, nil
|
||||
})
|
||||
|
||||
return endpointGroups, err
|
||||
}
|
||||
|
||||
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
|
||||
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
|
||||
return service.tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||
return int(endpointGroup.ID), endpointGroup
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -4,24 +4,33 @@ import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "endpoint_relations"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "endpoint_relations"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) relation data.
|
||||
type Service struct {
|
||||
connection portainer.Connection
|
||||
connection portainer.Connection
|
||||
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
updateStackFnTx func(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
func (service *Service) RegisterUpdateStackFunction(
|
||||
updateFunc func(portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
|
||||
updateFuncTx func(portainer.Transaction, portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
|
||||
) {
|
||||
service.updateStackFn = updateFunc
|
||||
service.updateStackFnTx = updateFuncTx
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
@@ -34,6 +43,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// EndpointRelations returns an array of all EndpointRelations
|
||||
func (service *Service) EndpointRelations() ([]portainer.EndpointRelation, error) {
|
||||
var all = make([]portainer.EndpointRelation, 0)
|
||||
@@ -71,17 +87,105 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
|
||||
|
||||
// CreateEndpointRelation saves endpointRelation
|
||||
func (service *Service) Create(endpointRelation *portainer.EndpointRelation) error {
|
||||
return service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||
err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||
cache.Del(endpointRelation.EndpointID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
|
||||
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
identifier := service.connection.ConvertToKey(int(EndpointID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, endpointRelation)
|
||||
func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
previousRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||
err := service.connection.UpdateObject(BucketName, identifier, endpointRelation)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
|
||||
identifier := service.connection.ConvertToKey(int(EndpointID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
|
||||
deletedRelation, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||
err := service.connection.DeleteObject(BucketName, identifier)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
|
||||
rels, err := service.EndpointRelations()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
|
||||
return
|
||||
}
|
||||
|
||||
for _, rel := range rels {
|
||||
for id := range rel.EdgeStacks {
|
||||
if edgeStackID == id {
|
||||
cache.Del(rel.EndpointID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
|
||||
relations, _ := service.EndpointRelations()
|
||||
|
||||
stacksToUpdate := map[portainer.EdgeStackID]bool{}
|
||||
|
||||
if previousRelationState != nil {
|
||||
for stackId, enabled := range previousRelationState.EdgeStacks {
|
||||
// flag stack for update if stack is not in the updated relation state
|
||||
// = stack has been removed for this relation
|
||||
// or this relation has been deleted
|
||||
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
|
||||
stacksToUpdate[stackId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updatedRelationState != nil {
|
||||
for stackId, enabled := range updatedRelationState.EdgeStacks {
|
||||
// flag stack for update if stack is not in the previous relation state
|
||||
// = stack has been added for this relation
|
||||
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
|
||||
stacksToUpdate[stackId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for each stack referenced by the updated relation
|
||||
// list how many time this stack is referenced in all relations
|
||||
// in order to update the stack deployments count
|
||||
for refStackId, refStackEnabled := range stacksToUpdate {
|
||||
if refStackEnabled {
|
||||
numDeployments := 0
|
||||
for _, r := range relations {
|
||||
for sId, enabled := range r.EdgeStacks {
|
||||
if enabled && sId == refStackId {
|
||||
numDeployments += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments = numDeployments
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
159
api/dataservices/endpointrelation/tx.go
Normal file
159
api/dataservices/endpointrelation/tx.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package endpointrelation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// EndpointRelations returns an array of all EndpointRelations
|
||||
func (service ServiceTx) EndpointRelations() ([]portainer.EndpointRelation, error) {
|
||||
var all = make([]portainer.EndpointRelation, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.EndpointRelation{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
r, ok := obj.(*portainer.EndpointRelation)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointRelation object")
|
||||
return nil, fmt.Errorf("failed to convert to EndpointRelation object: %s", obj)
|
||||
}
|
||||
|
||||
all = append(all, *r)
|
||||
|
||||
return &portainer.EndpointRelation{}, nil
|
||||
})
|
||||
|
||||
return all, err
|
||||
}
|
||||
|
||||
// EndpointRelation returns an Environment(Endpoint) relation object by EndpointID
|
||||
func (service ServiceTx) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
|
||||
var endpointRelation portainer.EndpointRelation
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &endpointRelation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &endpointRelation, nil
|
||||
}
|
||||
|
||||
// CreateEndpointRelation saves endpointRelation
|
||||
func (service ServiceTx) Create(endpointRelation *portainer.EndpointRelation) error {
|
||||
err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||
cache.Del(endpointRelation.EndpointID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
|
||||
func (service ServiceTx) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||
previousRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
err := service.tx.UpdateObject(BucketName, identifier, endpointRelation)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updatedRelationState, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||
func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
|
||||
deletedRelation, _ := service.EndpointRelation(endpointID)
|
||||
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
err := service.tx.DeleteObject(BucketName, identifier)
|
||||
cache.Del(endpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
|
||||
rels, err := service.EndpointRelations()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
|
||||
return
|
||||
}
|
||||
|
||||
for _, rel := range rels {
|
||||
for id := range rel.EdgeStacks {
|
||||
if edgeStackID == id {
|
||||
cache.Del(rel.EndpointID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
|
||||
relations, _ := service.EndpointRelations()
|
||||
|
||||
stacksToUpdate := map[portainer.EdgeStackID]bool{}
|
||||
|
||||
if previousRelationState != nil {
|
||||
for stackId, enabled := range previousRelationState.EdgeStacks {
|
||||
// flag stack for update if stack is not in the updated relation state
|
||||
// = stack has been removed for this relation
|
||||
// or this relation has been deleted
|
||||
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
|
||||
stacksToUpdate[stackId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updatedRelationState != nil {
|
||||
for stackId, enabled := range updatedRelationState.EdgeStacks {
|
||||
// flag stack for update if stack is not in the previous relation state
|
||||
// = stack has been added for this relation
|
||||
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
|
||||
stacksToUpdate[stackId] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for each stack referenced by the updated relation
|
||||
// list how many time this stack is referenced in all relations
|
||||
// in order to update the stack deployments count
|
||||
for refStackId, refStackEnabled := range stacksToUpdate {
|
||||
if refStackEnabled {
|
||||
numDeployments := 0
|
||||
for _, r := range relations {
|
||||
for sId, enabled := range r.EdgeStacks {
|
||||
if enabled && sId == refStackId {
|
||||
numDeployments += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||
edgeStack.NumDeployments = numDeployments
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package errors
|
||||
|
||||
import "errors"
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO: i'm pretty sure this needs wrapping at several levels
|
||||
ErrObjectNotFound = errors.New("object not found inside the database")
|
||||
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
|
||||
ErrDBImportFailed = errors.New("importing backup failed")
|
||||
ErrObjectNotFound = errors.New("object not found inside the database")
|
||||
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
|
||||
ErrDBImportFailed = errors.New("importing backup failed")
|
||||
ErrDatabaseIsUpdating = errors.New("database is currently in updating state. Failed prior upgrade. Please restore from backup or delete the database and restart Portainer")
|
||||
)
|
||||
|
||||
@@ -1,34 +1,22 @@
|
||||
package dataservices
|
||||
|
||||
// "github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/edgetypes"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
// DataStore defines the interface to manage the data
|
||||
DataStore interface {
|
||||
Open() (newStore bool, err error)
|
||||
Init() error
|
||||
Close() error
|
||||
MigrateData() error
|
||||
Rollback(force bool) error
|
||||
CheckCurrentEdition() error
|
||||
BackupTo(w io.Writer) error
|
||||
Export(filename string) (err error)
|
||||
DataStoreTx interface {
|
||||
IsErrObjectNotFound(err error) bool
|
||||
CustomTemplate() CustomTemplateService
|
||||
EdgeGroup() EdgeGroupService
|
||||
EdgeJob() EdgeJobService
|
||||
EdgeStack() EdgeStackService
|
||||
EdgeUpdateSchedule() EdgeUpdateScheduleService
|
||||
Endpoint() EndpointService
|
||||
EndpointGroup() EndpointGroupService
|
||||
EndpointRelation() EndpointRelationService
|
||||
@@ -51,6 +39,22 @@ type (
|
||||
Webhook() WebhookService
|
||||
}
|
||||
|
||||
// DataStore defines the interface to manage the data
|
||||
DataStore interface {
|
||||
Open() (newStore bool, err error)
|
||||
Init() error
|
||||
Close() error
|
||||
UpdateTx(func(DataStoreTx) error) error
|
||||
ViewTx(func(DataStoreTx) error) error
|
||||
MigrateData() error
|
||||
Rollback(force bool) error
|
||||
CheckCurrentEdition() error
|
||||
BackupTo(w io.Writer) error
|
||||
Export(filename string) (err error)
|
||||
|
||||
DataStoreTx
|
||||
}
|
||||
|
||||
// CustomTemplateService represents a service to manage custom templates
|
||||
CustomTemplateService interface {
|
||||
GetNextIdentifier() int
|
||||
@@ -68,6 +72,7 @@ type (
|
||||
EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error)
|
||||
Create(group *portainer.EdgeGroup) error
|
||||
UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error
|
||||
UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(group *portainer.EdgeGroup)) error
|
||||
DeleteEdgeGroup(ID portainer.EdgeGroupID) error
|
||||
BucketName() string
|
||||
}
|
||||
@@ -78,28 +83,20 @@ type (
|
||||
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
|
||||
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
||||
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
||||
UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error
|
||||
DeleteEdgeJob(ID portainer.EdgeJobID) error
|
||||
GetNextIdentifier() int
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
EdgeUpdateScheduleService interface {
|
||||
ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation
|
||||
ActiveSchedules(environmentIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation
|
||||
List() ([]edgetypes.UpdateSchedule, error)
|
||||
Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error)
|
||||
Create(edgeUpdateSchedule *edgetypes.UpdateSchedule) error
|
||||
Update(ID edgetypes.UpdateScheduleID, edgeUpdateSchedule *edgetypes.UpdateSchedule) error
|
||||
Delete(ID edgetypes.UpdateScheduleID) error
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
// EdgeStackService represents a service to manage Edge stacks
|
||||
EdgeStackService interface {
|
||||
EdgeStacks() ([]portainer.EdgeStack, error)
|
||||
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
|
||||
EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
|
||||
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
||||
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
||||
UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||
DeleteEdgeStack(ID portainer.EdgeStackID) error
|
||||
GetNextIdentifier() int
|
||||
BucketName() string
|
||||
@@ -108,6 +105,9 @@ type (
|
||||
// EndpointService represents a service for managing environment(endpoint) data
|
||||
EndpointService interface {
|
||||
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
||||
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
|
||||
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
|
||||
UpdateHeartbeat(endpointID portainer.EndpointID)
|
||||
Endpoints() ([]portainer.Endpoint, error)
|
||||
Create(endpoint *portainer.Endpoint) error
|
||||
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
|
||||
@@ -210,7 +210,6 @@ type (
|
||||
SettingsService interface {
|
||||
Settings() (*portainer.Settings, error)
|
||||
UpdateSettings(settings *portainer.Settings) error
|
||||
IsFeatureFlagEnabled(feature portainer.Feature) bool
|
||||
BucketName() string
|
||||
}
|
||||
|
||||
@@ -251,6 +250,7 @@ type (
|
||||
Tag(ID portainer.TagID) (*portainer.Tag, error)
|
||||
Create(tag *portainer.Tag) error
|
||||
UpdateTag(ID portainer.TagID, tag *portainer.Tag) error
|
||||
UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error
|
||||
DeleteTag(ID portainer.TagID) error
|
||||
BucketName() string
|
||||
}
|
||||
@@ -278,6 +278,7 @@ type (
|
||||
DeleteTeamMembershipByUserID(userID portainer.UserID) error
|
||||
DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error
|
||||
BucketName() string
|
||||
DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error
|
||||
}
|
||||
|
||||
// TunnelServerService represents a service for managing data associated to the tunnel server
|
||||
@@ -301,12 +302,11 @@ type (
|
||||
|
||||
// VersionService represents a service for managing version data
|
||||
VersionService interface {
|
||||
DBVersion() (int, error)
|
||||
Edition() (portainer.SoftwareEdition, error)
|
||||
InstanceID() (string, error)
|
||||
StoreDBVersion(version int) error
|
||||
StoreInstanceID(ID string) error
|
||||
BucketName() string
|
||||
UpdateInstanceID(ID string) error
|
||||
Version() (*models.Version, error)
|
||||
UpdateVersion(*models.Version) error
|
||||
}
|
||||
|
||||
// WebhookService represents a service for managing webhook data.
|
||||
@@ -323,5 +323,5 @@ type (
|
||||
)
|
||||
|
||||
func IsErrObjectNotFound(e error) bool {
|
||||
return e == errors.ErrObjectNotFound
|
||||
return errors.Is(e, dserrors.ErrObjectNotFound)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "registries"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "registries"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
@@ -34,7 +32,14 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Registry returns an registry by ID.
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// Registry returns a registry by ID.
|
||||
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
var registry portainer.Registry
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
@@ -69,7 +74,7 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
|
||||
return registries, err
|
||||
}
|
||||
|
||||
// CreateRegistry creates a new registry.
|
||||
// Create creates a new registry.
|
||||
func (service *Service) Create(registry *portainer.Registry) error {
|
||||
return service.connection.CreateObject(
|
||||
BucketName,
|
||||
@@ -80,13 +85,13 @@ func (service *Service) Create(registry *portainer.Registry) error {
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateRegistry updates an registry.
|
||||
// UpdateRegistry updates a registry.
|
||||
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, registry)
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes an registry.
|
||||
// DeleteRegistry deletes a registry.
|
||||
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.DeleteObject(BucketName, identifier)
|
||||
|
||||
75
api/dataservices/registry/tx.go
Normal file
75
api/dataservices/registry/tx.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// Registry returns a registry by ID.
|
||||
func (service ServiceTx) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
var registry portainer.Registry
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, ®istry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// Registries returns an array containing all the registries.
|
||||
func (service ServiceTx) Registries() ([]portainer.Registry, error) {
|
||||
var registries = make([]portainer.Registry, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Registry{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
registry, ok := obj.(*portainer.Registry)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Registry object")
|
||||
return nil, fmt.Errorf("Failed to convert to Registry object: %s", obj)
|
||||
}
|
||||
|
||||
registries = append(registries, *registry)
|
||||
|
||||
return &portainer.Registry{}, nil
|
||||
})
|
||||
|
||||
return registries, err
|
||||
}
|
||||
|
||||
// Create creates a new registry.
|
||||
func (service ServiceTx) Create(registry *portainer.Registry) error {
|
||||
return service.tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
registry.ID = portainer.RegistryID(id)
|
||||
return int(registry.ID), registry
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateRegistry updates a registry.
|
||||
func (service ServiceTx) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, registry)
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes a registry.
|
||||
func (service ServiceTx) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package resourcecontrol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -8,10 +9,8 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "resource_control"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "resource_control"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
@@ -34,6 +33,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceControl returns a ResourceControl object by ID
|
||||
func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
|
||||
var resourceControl portainer.ResourceControl
|
||||
@@ -77,7 +83,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
|
||||
|
||||
return &portainer.ResourceControl{}, nil
|
||||
})
|
||||
if err == stop {
|
||||
if errors.Is(err, stop) {
|
||||
return resourceControl, nil
|
||||
}
|
||||
|
||||
|
||||
114
api/dataservices/resourcecontrol/tx.go
Normal file
114
api/dataservices/resourcecontrol/tx.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package resourcecontrol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// ResourceControl returns a ResourceControl object by ID
|
||||
func (service ServiceTx) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
|
||||
var resourceControl portainer.ResourceControl
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &resourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resourceControl, nil
|
||||
}
|
||||
|
||||
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
|
||||
// if no ResourceControl was found.
|
||||
func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||
var resourceControl *portainer.ResourceControl
|
||||
stop := fmt.Errorf("ok")
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.ResourceControl{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
rc, ok := obj.(*portainer.ResourceControl)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||
return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
|
||||
}
|
||||
|
||||
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||
resourceControl = rc
|
||||
return nil, stop
|
||||
}
|
||||
|
||||
for _, subResourceID := range rc.SubResourceIDs {
|
||||
if subResourceID == resourceID {
|
||||
resourceControl = rc
|
||||
return nil, stop
|
||||
}
|
||||
}
|
||||
|
||||
return &portainer.ResourceControl{}, nil
|
||||
})
|
||||
if errors.Is(err, stop) {
|
||||
return resourceControl, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ResourceControls returns all the ResourceControl objects
|
||||
func (service ServiceTx) ResourceControls() ([]portainer.ResourceControl, error) {
|
||||
var rcs = make([]portainer.ResourceControl, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.ResourceControl{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
rc, ok := obj.(*portainer.ResourceControl)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||
return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
|
||||
}
|
||||
|
||||
rcs = append(rcs, *rc)
|
||||
|
||||
return &portainer.ResourceControl{}, nil
|
||||
})
|
||||
|
||||
return rcs, err
|
||||
}
|
||||
|
||||
// CreateResourceControl creates a new ResourceControl object
|
||||
func (service ServiceTx) Create(resourceControl *portainer.ResourceControl) error {
|
||||
return service.tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
resourceControl.ID = portainer.ResourceControlID(id)
|
||||
return int(resourceControl.ID), resourceControl
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateResourceControl saves a ResourceControl object.
|
||||
func (service ServiceTx) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, resourceControl)
|
||||
}
|
||||
|
||||
// DeleteResourceControl deletes a ResourceControl object by ID
|
||||
func (service ServiceTx) DeleteResourceControl(ID portainer.ResourceControlID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "roles"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "roles"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
@@ -34,6 +32,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// Role returns a Role by ID
|
||||
func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
|
||||
var set portainer.Role
|
||||
@@ -47,7 +52,7 @@ func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
|
||||
return &set, nil
|
||||
}
|
||||
|
||||
// Roles return an array containing all the sets.
|
||||
// Roles returns an array containing all the sets.
|
||||
func (service *Service) Roles() ([]portainer.Role, error) {
|
||||
var sets = make([]portainer.Role, 0)
|
||||
|
||||
@@ -58,7 +63,7 @@ func (service *Service) Roles() ([]portainer.Role, error) {
|
||||
set, ok := obj.(*portainer.Role)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Role object")
|
||||
return nil, fmt.Errorf("Failed to convert to Role object: %s", obj)
|
||||
return nil, fmt.Errorf("failed to convert to Role object: %s", obj)
|
||||
}
|
||||
|
||||
sets = append(sets, *set)
|
||||
|
||||
71
api/dataservices/role/tx.go
Normal file
71
api/dataservices/role/tx.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package role
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// Role returns a Role by ID
|
||||
func (service ServiceTx) Role(ID portainer.RoleID) (*portainer.Role, error) {
|
||||
var set portainer.Role
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &set)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &set, nil
|
||||
}
|
||||
|
||||
// Roles returns an array containing all the sets.
|
||||
func (service ServiceTx) Roles() ([]portainer.Role, error) {
|
||||
var sets = make([]portainer.Role, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Role{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
set, ok := obj.(*portainer.Role)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Role object")
|
||||
return nil, fmt.Errorf("failed to convert to Role object: %s", obj)
|
||||
}
|
||||
|
||||
sets = append(sets, *set)
|
||||
|
||||
return &portainer.Role{}, nil
|
||||
})
|
||||
|
||||
return sets, err
|
||||
}
|
||||
|
||||
// CreateRole creates a new Role.
|
||||
func (service ServiceTx) Create(role *portainer.Role) error {
|
||||
return service.tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
role.ID = portainer.RoleID(id)
|
||||
return int(role.ID), role
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateRole updates a role.
|
||||
func (service ServiceTx) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, role)
|
||||
}
|
||||
@@ -31,6 +31,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// Settings retrieve the settings object.
|
||||
func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
var settings portainer.Settings
|
||||
@@ -47,17 +54,3 @@ func (service *Service) Settings() (*portainer.Settings, error) {
|
||||
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
|
||||
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||
}
|
||||
|
||||
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
|
||||
settings, err := service.Settings()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
featureFlagSetting, ok := settings.FeatureFlagSettings[feature]
|
||||
if ok {
|
||||
return featureFlagSetting
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
31
api/dataservices/settings/tx.go
Normal file
31
api/dataservices/settings/tx.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// Settings retrieve the settings object.
|
||||
func (service ServiceTx) Settings() (*portainer.Settings, error) {
|
||||
var settings portainer.Settings
|
||||
|
||||
err := service.tx.GetObject(BucketName, []byte(settingsKey), &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// UpdateSettings persists a Settings object.
|
||||
func (service ServiceTx) UpdateSettings(settings *portainer.Settings) error {
|
||||
return service.tx.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||
}
|
||||
@@ -31,6 +31,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
func (service *Service) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) {
|
||||
var snapshot portainer.Snapshot
|
||||
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||
|
||||
63
api/dataservices/snapshot/tx.go
Normal file
63
api/dataservices/snapshot/tx.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package snapshot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
func (service ServiceTx) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) {
|
||||
var snapshot portainer.Snapshot
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &snapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &snapshot, nil
|
||||
}
|
||||
|
||||
func (service ServiceTx) Snapshots() ([]portainer.Snapshot, error) {
|
||||
var snapshots = make([]portainer.Snapshot, 0)
|
||||
|
||||
err := service.tx.GetAllWithJsoniter(
|
||||
BucketName,
|
||||
&portainer.Snapshot{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
snapshot, ok := obj.(*portainer.Snapshot)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Snapshot object")
|
||||
return nil, fmt.Errorf("failed to convert to Snapshot object: %s", obj)
|
||||
}
|
||||
snapshots = append(snapshots, *snapshot)
|
||||
return &portainer.Snapshot{}, nil
|
||||
})
|
||||
|
||||
return snapshots, err
|
||||
}
|
||||
|
||||
func (service ServiceTx) UpdateSnapshot(snapshot *portainer.Snapshot) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(snapshot.EndpointID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, snapshot)
|
||||
}
|
||||
|
||||
func (service ServiceTx) DeleteSnapshot(endpointID portainer.EndpointID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
|
||||
func (service ServiceTx) Create(snapshot *portainer.Snapshot) error {
|
||||
return service.tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package stack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -71,11 +72,11 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
|
||||
|
||||
return &portainer.Stack{}, nil
|
||||
})
|
||||
if err == stop {
|
||||
if errors.Is(err, stop) {
|
||||
return s, nil
|
||||
}
|
||||
if err == nil {
|
||||
return nil, errors.ErrObjectNotFound
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
@@ -92,7 +93,7 @@ func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
|
||||
stack, ok := obj.(portainer.Stack)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
|
||||
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
|
||||
return nil, fmt.Errorf("failed to convert to Stack object: %s", obj)
|
||||
}
|
||||
|
||||
if stack.Name == name {
|
||||
@@ -173,11 +174,11 @@ func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
|
||||
|
||||
return &portainer.Stack{}, nil
|
||||
})
|
||||
if err == stop {
|
||||
if errors.Is(err, stop) {
|
||||
return s, nil
|
||||
}
|
||||
if err == nil {
|
||||
return nil, errors.ErrObjectNotFound
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
|
||||
@@ -59,7 +59,7 @@ func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: 2,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: []portainer.Pair{{"Name1", "Value1"}},
|
||||
Env: []portainer.Pair{{Name: "Name1", Value: "Value1"}},
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ProjectPath: "/tmp/project",
|
||||
@@ -68,13 +68,13 @@ func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
|
||||
|
||||
if webhookID == "" {
|
||||
if b.count%2 == 0 {
|
||||
stack.AutoUpdate = &portainer.StackAutoUpdate{
|
||||
stack.AutoUpdate = &portainer.AutoUpdateSettings{
|
||||
Interval: "",
|
||||
Webhook: "",
|
||||
}
|
||||
} // else keep AutoUpdate nil
|
||||
} else {
|
||||
stack.AutoUpdate = &portainer.StackAutoUpdate{Webhook: webhookID}
|
||||
stack.AutoUpdate = &portainer.AutoUpdateSettings{Webhook: webhookID}
|
||||
}
|
||||
|
||||
err := b.store.StackService.Create(&stack)
|
||||
@@ -91,8 +91,8 @@ func Test_RefreshableStacks(t *testing.T) {
|
||||
defer teardown()
|
||||
|
||||
staticStack := portainer.Stack{ID: 1}
|
||||
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.StackAutoUpdate{Webhook: "webhook"}}
|
||||
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.StackAutoUpdate{Interval: "1m"}}
|
||||
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.AutoUpdateSettings{Webhook: "webhook"}}
|
||||
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.AutoUpdateSettings{Interval: "1m"}}
|
||||
|
||||
for _, stack := range []*portainer.Stack{&staticStack, &stackWithWebhook, &refreshableStack} {
|
||||
err := store.Stack().Create(stack)
|
||||
|
||||
@@ -34,6 +34,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// Tags return an array containing all the tags.
|
||||
func (service *Service) Tags() ([]portainer.Tag, error) {
|
||||
var tags = make([]portainer.Tag, 0)
|
||||
@@ -80,12 +87,22 @@ func (service *Service) Create(tag *portainer.Tag) error {
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateTag updates a tag.
|
||||
// Deprecated: Use UpdateTagFunc instead.
|
||||
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
return service.connection.UpdateObject(BucketName, identifier, tag)
|
||||
}
|
||||
|
||||
// UpdateTagFunc updates a tag inside a transaction avoiding data races.
|
||||
func (service *Service) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
|
||||
id := service.connection.ConvertToKey(int(ID))
|
||||
tag := &portainer.Tag{}
|
||||
|
||||
return service.connection.UpdateObjectFunc(BucketName, id, tag, func() {
|
||||
updateFunc(tag)
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTag deletes a tag.
|
||||
func (service *Service) DeleteTag(ID portainer.TagID) error {
|
||||
identifier := service.connection.ConvertToKey(int(ID))
|
||||
|
||||
82
api/dataservices/tag/tx.go
Normal file
82
api/dataservices/tag/tx.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package tag
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// Tags return an array containing all the tags.
|
||||
func (service ServiceTx) Tags() ([]portainer.Tag, error) {
|
||||
var tags = make([]portainer.Tag, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.Tag{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
tag, ok := obj.(*portainer.Tag)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Tag object")
|
||||
return nil, fmt.Errorf("failed to convert to Tag object: %s", obj)
|
||||
}
|
||||
|
||||
tags = append(tags, *tag)
|
||||
|
||||
return &portainer.Tag{}, nil
|
||||
})
|
||||
|
||||
return tags, err
|
||||
}
|
||||
|
||||
// Tag returns a tag by ID.
|
||||
func (service ServiceTx) Tag(ID portainer.TagID) (*portainer.Tag, error) {
|
||||
var tag portainer.Tag
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// CreateTag creates a new tag.
|
||||
func (service ServiceTx) Create(tag *portainer.Tag) error {
|
||||
return service.tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
tag.ID = portainer.TagID(id)
|
||||
return int(tag.ID), tag
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateTag updates a tag
|
||||
func (service ServiceTx) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, tag)
|
||||
}
|
||||
|
||||
// UpdateTagFunc is a no-op inside a transaction
|
||||
func (service ServiceTx) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
|
||||
return errors.New("cannot be called inside a transaction")
|
||||
}
|
||||
|
||||
// DeleteTag deletes a tag.
|
||||
func (service ServiceTx) DeleteTag(ID portainer.TagID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package team
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -71,11 +72,11 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
|
||||
|
||||
return &portainer.Team{}, nil
|
||||
})
|
||||
if err == stop {
|
||||
if errors.Is(err, stop) {
|
||||
return t, nil
|
||||
}
|
||||
if err == nil {
|
||||
return nil, errors.ErrObjectNotFound
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "team_membership"
|
||||
)
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "team_membership"
|
||||
|
||||
// Service represents a service for managing environment(endpoint) data.
|
||||
type Service struct {
|
||||
@@ -34,6 +32,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||
return ServiceTx{
|
||||
service: service,
|
||||
tx: tx,
|
||||
}
|
||||
}
|
||||
|
||||
// TeamMembership returns a TeamMembership object by ID
|
||||
func (service *Service) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
|
||||
var membership portainer.TeamMembership
|
||||
@@ -144,6 +149,7 @@ func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) erro
|
||||
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
|
||||
return service.connection.DeleteAllObjects(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
if !ok {
|
||||
@@ -164,6 +170,7 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
|
||||
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
|
||||
return service.connection.DeleteAllObjects(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
if !ok {
|
||||
@@ -179,3 +186,23 @@ func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) er
|
||||
return -1, false
|
||||
})
|
||||
}
|
||||
|
||||
func (service *Service) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error {
|
||||
return service.connection.DeleteAllObjects(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
return -1, false
|
||||
}
|
||||
|
||||
if membership.TeamID == teamID && membership.UserID == userID {
|
||||
return int(membership.ID), true
|
||||
}
|
||||
|
||||
return -1, false
|
||||
})
|
||||
}
|
||||
|
||||
186
api/dataservices/teammembership/tx.go
Normal file
186
api/dataservices/teammembership/tx.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package teammembership
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ServiceTx struct {
|
||||
service *Service
|
||||
tx portainer.Transaction
|
||||
}
|
||||
|
||||
func (service ServiceTx) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// TeamMembership returns a TeamMembership object by ID
|
||||
func (service ServiceTx) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
|
||||
var membership portainer.TeamMembership
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
|
||||
err := service.tx.GetObject(BucketName, identifier, &membership)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &membership, nil
|
||||
}
|
||||
|
||||
// TeamMemberships return an array containing all the TeamMembership objects.
|
||||
func (service ServiceTx) TeamMemberships() ([]portainer.TeamMembership, error) {
|
||||
var memberships = make([]portainer.TeamMembership, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
membership, ok := obj.(*portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
}
|
||||
|
||||
memberships = append(memberships, *membership)
|
||||
|
||||
return &portainer.TeamMembership{}, nil
|
||||
})
|
||||
|
||||
return memberships, err
|
||||
}
|
||||
|
||||
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
|
||||
func (service ServiceTx) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
|
||||
var memberships = make([]portainer.TeamMembership, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
membership, ok := obj.(*portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
}
|
||||
|
||||
if membership.UserID == userID {
|
||||
memberships = append(memberships, *membership)
|
||||
}
|
||||
|
||||
return &portainer.TeamMembership{}, nil
|
||||
})
|
||||
|
||||
return memberships, err
|
||||
}
|
||||
|
||||
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
|
||||
func (service ServiceTx) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
|
||||
var memberships = make([]portainer.TeamMembership, 0)
|
||||
|
||||
err := service.tx.GetAll(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (interface{}, error) {
|
||||
membership, ok := obj.(*portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
}
|
||||
|
||||
if membership.TeamID == teamID {
|
||||
memberships = append(memberships, *membership)
|
||||
}
|
||||
|
||||
return &portainer.TeamMembership{}, nil
|
||||
})
|
||||
|
||||
return memberships, err
|
||||
}
|
||||
|
||||
// UpdateTeamMembership saves a TeamMembership object.
|
||||
func (service ServiceTx) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.UpdateObject(BucketName, identifier, membership)
|
||||
}
|
||||
|
||||
// CreateTeamMembership creates a new TeamMembership object.
|
||||
func (service ServiceTx) Create(membership *portainer.TeamMembership) error {
|
||||
return service.tx.CreateObject(
|
||||
BucketName,
|
||||
func(id uint64) (int, interface{}) {
|
||||
membership.ID = portainer.TeamMembershipID(id)
|
||||
return int(membership.ID), membership
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteTeamMembership deletes a TeamMembership object.
|
||||
func (service ServiceTx) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
|
||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||
return service.tx.DeleteObject(BucketName, identifier)
|
||||
}
|
||||
|
||||
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
|
||||
func (service ServiceTx) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
|
||||
return service.tx.DeleteAllObjects(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
return -1, false
|
||||
}
|
||||
|
||||
if membership.UserID == userID {
|
||||
return int(membership.ID), true
|
||||
}
|
||||
|
||||
return -1, false
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
|
||||
func (service ServiceTx) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
|
||||
return service.tx.DeleteAllObjects(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
return -1, false
|
||||
}
|
||||
|
||||
if membership.TeamID == teamID {
|
||||
return int(membership.ID), true
|
||||
}
|
||||
|
||||
return -1, false
|
||||
})
|
||||
}
|
||||
|
||||
func (service ServiceTx) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error {
|
||||
return service.tx.DeleteAllObjects(
|
||||
BucketName,
|
||||
&portainer.TeamMembership{},
|
||||
func(obj interface{}) (id int, ok bool) {
|
||||
membership, ok := obj.(portainer.TeamMembership)
|
||||
if !ok {
|
||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||
return -1, false
|
||||
}
|
||||
|
||||
if membership.TeamID == teamID && membership.UserID == userID {
|
||||
return int(membership.ID), true
|
||||
}
|
||||
|
||||
return -1, false
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -72,12 +73,12 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
|
||||
return &portainer.User{}, nil
|
||||
})
|
||||
|
||||
if err == stop {
|
||||
if errors.Is(err, stop) {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, errors.ErrObjectNotFound
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"errors"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "version"
|
||||
versionKey = "DB_VERSION"
|
||||
instanceKey = "INSTANCE_ID"
|
||||
editionKey = "EDITION"
|
||||
versionKey = "VERSION"
|
||||
updatingKey = "DB_UPDATING"
|
||||
)
|
||||
|
||||
@@ -20,10 +21,6 @@ type Service struct {
|
||||
connection portainer.Connection
|
||||
}
|
||||
|
||||
func (service *Service) BucketName() string {
|
||||
return BucketName
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
@@ -36,56 +33,87 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DBVersion retrieves the stored database version.
|
||||
func (service *Service) DBVersion() (int, error) {
|
||||
var version string
|
||||
err := service.connection.GetObject(BucketName, []byte(versionKey), &version)
|
||||
func (service *Service) SchemaVersion() (string, error) {
|
||||
v, err := service.Version()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return "", err
|
||||
}
|
||||
return strconv.Atoi(version)
|
||||
|
||||
return v.SchemaVersion, nil
|
||||
}
|
||||
|
||||
func (service *Service) UpdateSchemaVersion(version string) error {
|
||||
v, err := service.Version()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.SchemaVersion = version
|
||||
return service.UpdateVersion(v)
|
||||
}
|
||||
|
||||
// Edition retrieves the stored portainer edition.
|
||||
func (service *Service) Edition() (portainer.SoftwareEdition, error) {
|
||||
var edition string
|
||||
err := service.connection.GetObject(BucketName, []byte(editionKey), &edition)
|
||||
v, err := service.Version()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
e, err := strconv.Atoi(edition)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return portainer.SoftwareEdition(e), nil
|
||||
}
|
||||
|
||||
// StoreDBVersion store the database version.
|
||||
func (service *Service) StoreDBVersion(version int) error {
|
||||
return service.connection.UpdateObject(BucketName, []byte(versionKey), strconv.Itoa(version))
|
||||
return portainer.SoftwareEdition(v.Edition), nil
|
||||
}
|
||||
|
||||
// IsUpdating retrieves the database updating status.
|
||||
func (service *Service) IsUpdating() (bool, error) {
|
||||
var isUpdating bool
|
||||
err := service.connection.GetObject(BucketName, []byte(updatingKey), &isUpdating)
|
||||
if err != nil && errors.Is(err, dserrors.ErrObjectNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return isUpdating, err
|
||||
}
|
||||
|
||||
// StoreIsUpdating store the database updating status.
|
||||
func (service *Service) StoreIsUpdating(isUpdating bool) error {
|
||||
return service.connection.UpdateObject(BucketName, []byte(updatingKey), isUpdating)
|
||||
return service.connection.DeleteObject(BucketName, []byte(updatingKey))
|
||||
}
|
||||
|
||||
// InstanceID retrieves the stored instance ID.
|
||||
func (service *Service) InstanceID() (string, error) {
|
||||
var id string
|
||||
err := service.connection.GetObject(BucketName, []byte(instanceKey), &id)
|
||||
return id, err
|
||||
v, err := service.Version()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return v.InstanceID, nil
|
||||
}
|
||||
|
||||
// StoreInstanceID store the instance ID.
|
||||
func (service *Service) StoreInstanceID(ID string) error {
|
||||
return service.connection.UpdateObject(BucketName, []byte(instanceKey), ID)
|
||||
func (service *Service) UpdateInstanceID(id string) error {
|
||||
v, err := service.Version()
|
||||
if err != nil {
|
||||
if !dataservices.IsErrObjectNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
v = &models.Version{}
|
||||
}
|
||||
|
||||
v.InstanceID = id
|
||||
return service.UpdateVersion(v)
|
||||
}
|
||||
|
||||
// Version retrieve the version object.
|
||||
func (service *Service) Version() (*models.Version, error) {
|
||||
var v models.Version
|
||||
|
||||
err := service.connection.GetObject(BucketName, []byte(versionKey), &v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
// UpdateVersion persists a Version object.
|
||||
func (service *Service) UpdateVersion(version *models.Version) error {
|
||||
return service.connection.UpdateObject(BucketName, []byte(versionKey), version)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -93,12 +94,12 @@ func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, erro
|
||||
return &portainer.Webhook{}, nil
|
||||
})
|
||||
|
||||
if err == stop {
|
||||
if errors.Is(err, stop) {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, errors.ErrObjectNotFound
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
@@ -127,12 +128,12 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
|
||||
return &portainer.Webhook{}, nil
|
||||
})
|
||||
|
||||
if err == stop {
|
||||
if errors.Is(err, stop) {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return nil, errors.ErrObjectNotFound
|
||||
return nil, dserrors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil, err
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -53,7 +54,7 @@ func (store *Store) copyDBFile(from string, to string) error {
|
||||
|
||||
// BackupOptions provide a helper to inject backup options
|
||||
type BackupOptions struct {
|
||||
Version int // I can't find this used for anything other than a filename
|
||||
Version string
|
||||
BackupDir string
|
||||
BackupFileName string
|
||||
BackupPath string
|
||||
@@ -70,26 +71,32 @@ func getBackupRestoreOptions(backupDir string) *BackupOptions {
|
||||
}
|
||||
|
||||
// Backup current database with default options
|
||||
func (store *Store) Backup() (string, error) {
|
||||
return store.backupWithOptions(nil)
|
||||
func (store *Store) Backup(version *models.Version) (string, error) {
|
||||
if version == nil {
|
||||
return store.backupWithOptions(nil)
|
||||
}
|
||||
|
||||
return store.backupWithOptions(&BackupOptions{
|
||||
Version: version.SchemaVersion,
|
||||
})
|
||||
}
|
||||
|
||||
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
|
||||
if options == nil {
|
||||
options = &BackupOptions{}
|
||||
}
|
||||
if options.Version == 0 {
|
||||
version, err := store.version()
|
||||
if options.Version == "" {
|
||||
v, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
version = 0
|
||||
options.Version = ""
|
||||
}
|
||||
options.Version = version
|
||||
options.Version = v.SchemaVersion
|
||||
}
|
||||
if options.BackupDir == "" {
|
||||
options.BackupDir = store.commonBackupDir()
|
||||
}
|
||||
if options.BackupFileName == "" {
|
||||
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
|
||||
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), options.Version, time.Now().Format("20060102150405"))
|
||||
}
|
||||
if options.BackupPath == "" {
|
||||
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
|
||||
@@ -108,7 +115,7 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
|
||||
|
||||
if err := store.Close(); err != nil {
|
||||
return options.BackupPath, fmt.Errorf(
|
||||
"error closing datastore before creating backup: %v",
|
||||
"error closing datastore before creating backup: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
@@ -119,7 +126,7 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
|
||||
|
||||
if _, err := store.Open(); err != nil {
|
||||
return options.BackupPath, fmt.Errorf(
|
||||
"error opening datastore after creating backup: %v",
|
||||
"error opening datastore after creating backup: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
@@ -168,7 +175,6 @@ func (store *Store) removeWithOptions(options *BackupOptions) error {
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to remove does not exist")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -176,7 +182,6 @@ func (store *Store) removeWithOptions(options *BackupOptions) error {
|
||||
err = os.Remove(options.BackupPath)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
)
|
||||
|
||||
func TestCreateBackupFolders(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(t, false, true)
|
||||
_, store, teardown := MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
|
||||
connection := store.GetConnection()
|
||||
@@ -45,10 +46,13 @@ func TestBackup(t *testing.T) {
|
||||
defer teardown()
|
||||
|
||||
t.Run("Backup should create default db backup", func(t *testing.T) {
|
||||
store.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
v := models.Version{
|
||||
SchemaVersion: portainer.APIVersion,
|
||||
}
|
||||
store.VersionService.UpdateVersion(&v)
|
||||
store.backupWithOptions(nil)
|
||||
|
||||
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%03d.*", portainer.DBVersion))
|
||||
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%s.*", portainer.APIVersion))
|
||||
if !isFileExist(backupFileName) {
|
||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -8,27 +9,12 @@ import (
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
portainerErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (store *Store) version() (int, error) {
|
||||
version, err := store.VersionService.DBVersion()
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
version = 0
|
||||
}
|
||||
return version, err
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
edition, err := store.VersionService.Edition()
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
edition = portainer.PortainerCE
|
||||
}
|
||||
return edition
|
||||
}
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
func NewStore(storePath string, fileService portainer.FileService, connection portainer.Connection) *Store {
|
||||
return &Store{
|
||||
@@ -39,8 +25,6 @@ func NewStore(storePath string, fileService portainer.FileService, connection po
|
||||
|
||||
// Open opens and initializes the BoltDB database.
|
||||
func (store *Store) Open() (newStore bool, err error) {
|
||||
newStore = true
|
||||
|
||||
encryptionReq, err := store.connection.NeedsEncryptionMigration()
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -55,37 +39,48 @@ func (store *Store) Open() (newStore bool, err error) {
|
||||
|
||||
err = store.connection.Open()
|
||||
if err != nil {
|
||||
return newStore, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = store.initServices()
|
||||
if err != nil {
|
||||
return newStore, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
// if we have DBVersion in the database then ensure we flag this as NOT a new store
|
||||
version, err := store.VersionService.DBVersion()
|
||||
// If no settings object exists then assume we have a new store
|
||||
_, err = store.SettingsService.Settings()
|
||||
if err != nil {
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
return newStore, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return newStore, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
if version > 0 {
|
||||
log.Debug().Int("version", version).Msg("opened existing store")
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return newStore, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (store *Store) Close() error {
|
||||
return store.connection.Close()
|
||||
}
|
||||
|
||||
func (store *Store) UpdateTx(fn func(dataservices.DataStoreTx) error) error {
|
||||
return store.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||
return fn(&StoreTx{
|
||||
store: store,
|
||||
tx: tx,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (store *Store) ViewTx(fn func(dataservices.DataStoreTx) error) error {
|
||||
return store.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||
return fn(&StoreTx{
|
||||
store: store,
|
||||
tx: tx,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// BackupTo backs up db to a provided writer.
|
||||
// It does hot backup and doesn't block other database reads and writes
|
||||
func (store *Store) BackupTo(w io.Writer) error {
|
||||
@@ -94,15 +89,27 @@ func (store *Store) BackupTo(w io.Writer) error {
|
||||
|
||||
// CheckCurrentEdition checks if current edition is community edition
|
||||
func (store *Store) CheckCurrentEdition() error {
|
||||
if store.edition() != portainer.PortainerCE {
|
||||
if store.edition() != portainer.Edition {
|
||||
return portainerErrors.ErrWrongDBEdition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *Store) edition() portainer.SoftwareEdition {
|
||||
edition, err := store.VersionService.Edition()
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
edition = portainer.PortainerCE
|
||||
}
|
||||
return edition
|
||||
}
|
||||
|
||||
// TODO: move the use of this to dataservices.IsErrObjectNotFound()?
|
||||
func (store *Store) IsErrObjectNotFound(e error) bool {
|
||||
return e == portainerErrors.ErrObjectNotFound
|
||||
return errors.Is(e, portainerErrors.ErrObjectNotFound)
|
||||
}
|
||||
|
||||
func (store *Store) Connection() portainer.Connection {
|
||||
return store.connection
|
||||
}
|
||||
|
||||
func (store *Store) Rollback(force bool) error {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
"os"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Init creates the default data set.
|
||||
func (store *Store) Init() error {
|
||||
err := store.checkOrCreateInstanceID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.checkOrCreateDefaultSettings()
|
||||
err := store.checkOrCreateDefaultSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -25,22 +21,13 @@ func (store *Store) Init() error {
|
||||
return store.checkOrCreateDefaultData()
|
||||
}
|
||||
|
||||
func (store *Store) checkOrCreateInstanceID() error {
|
||||
_, err := store.VersionService.InstanceID()
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
uid, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (store *Store) checkOrCreateDefaultSettings() error {
|
||||
|
||||
instanceID := uid.String()
|
||||
return store.VersionService.StoreInstanceID(instanceID)
|
||||
isDDExtention := false
|
||||
if _, ok := os.LookupEnv("DOCKER_EXTENSION"); ok {
|
||||
isDDExtention = true
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *Store) checkOrCreateDefaultSettings() error {
|
||||
// TODO: these need to also be applied when importing
|
||||
settings, err := store.SettingsService.Settings()
|
||||
if store.IsErrObjectNotFound(err) {
|
||||
@@ -72,6 +59,8 @@ func (store *Store) checkOrCreateDefaultSettings() error {
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
|
||||
KubectlShellImage: portainer.DefaultKubectlShellImage,
|
||||
|
||||
IsDockerDesktopExtension: isDDExtention,
|
||||
}
|
||||
|
||||
return store.SettingsService.UpdateSettings(defaultSettings)
|
||||
|
||||
@@ -6,35 +6,70 @@ import (
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/datastore/migrator"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
werrors "github.com/pkg/errors"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
|
||||
|
||||
func (store *Store) MigrateData() error {
|
||||
version, err := store.version()
|
||||
updating, err := store.VersionService.IsUpdating()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while checking if the store is updating")
|
||||
}
|
||||
|
||||
if updating {
|
||||
return dserrors.ErrDatabaseIsUpdating
|
||||
}
|
||||
|
||||
// migrate new version bucket if required (doesn't write anything to db yet)
|
||||
version, err := store.getOrMigrateLegacyVersion()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while migrating legacy version")
|
||||
}
|
||||
|
||||
migratorParams := store.newMigratorParameters(version)
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
if !migrator.NeedsMigration() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// before we alter anything in the DB, create a backup
|
||||
backupPath, err := store.Backup(version)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while backing up database")
|
||||
}
|
||||
|
||||
err = store.FailSafeMigrate(migrator, version)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "failed to migrate database")
|
||||
|
||||
log.Warn().Msg("migration failed, restoring database to previous version")
|
||||
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to restore database")
|
||||
}
|
||||
|
||||
log.Info().Msg("database restored to previous version")
|
||||
return err
|
||||
}
|
||||
|
||||
// Backup Database
|
||||
backupPath, err := store.Backup()
|
||||
if err != nil {
|
||||
return werrors.Wrap(err, "while backing up db before migration")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
migratorParams := &migrator.MigratorParameters{
|
||||
DatabaseVersion: version,
|
||||
func (store *Store) newMigratorParameters(version *models.Version) *migrator.MigratorParameters {
|
||||
return &migrator.MigratorParameters{
|
||||
CurrentDBVersion: version,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
ExtensionService: store.ExtensionService,
|
||||
FDOProfilesService: store.FDOProfilesService,
|
||||
RegistryService: store.RegistryService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
RoleService: store.RoleService,
|
||||
@@ -49,99 +84,43 @@ func (store *Store) MigrateData() error {
|
||||
FileService: store.fileService,
|
||||
DockerhubService: store.DockerHubService,
|
||||
AuthorizationService: authorization.NewService(store),
|
||||
EdgeStackService: store.EdgeStackService,
|
||||
EdgeJobService: store.EdgeJobService,
|
||||
}
|
||||
|
||||
// restore on error
|
||||
err = store.connectionMigrateData(migratorParams)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("while DB migration, restoring DB")
|
||||
|
||||
// Restore options
|
||||
options := BackupOptions{
|
||||
BackupPath: backupPath,
|
||||
}
|
||||
|
||||
err := store.restoreWithOptions(&options)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
Str("database_file", store.databasePath()).
|
||||
Str("backup", options.BackupPath).Err(err).
|
||||
Msg("failed restoring the backup, Portainer database file needs to restored manually by replacing the database file with a recent backup")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FailSafeMigrate backup and restore DB if migration fail
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
|
||||
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models.Version) (err error) {
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
store.Rollback(true)
|
||||
// return error with cause and stacktrace (recover() doesn't include a stacktrace)
|
||||
err = fmt.Errorf("%v %s", e, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
// !Important: we must use a named return value in the function definition and not a local
|
||||
// !variable referenced from the closure or else the return value will be incorrectly set
|
||||
return migrator.Migrate()
|
||||
}
|
||||
|
||||
// MigrateData automatically migrate the data based on the DBVersion.
|
||||
// This process is only triggered on an existing database, not if the database was just created.
|
||||
// if force is true, then migrate regardless.
|
||||
func (store *Store) connectionMigrateData(migratorParams *migrator.MigratorParameters) error {
|
||||
migrator := migrator.NewMigrator(migratorParams)
|
||||
|
||||
// backup db file before upgrading DB to support rollback
|
||||
isUpdating, err := migratorParams.VersionService.IsUpdating()
|
||||
if err != nil && err != errors.ErrObjectNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isUpdating && migrator.Version() != portainer.DBVersion {
|
||||
err = store.backupVersion(migrator)
|
||||
if err != nil {
|
||||
return werrors.Wrapf(err, "failed to backup database")
|
||||
}
|
||||
}
|
||||
|
||||
if migrator.Version() < portainer.DBVersion {
|
||||
log.Info().
|
||||
Int("migrator_version", migrator.Version()).
|
||||
Int("db_version", portainer.DBVersion).
|
||||
Msg("migrating database")
|
||||
|
||||
err = store.FailSafeMigrate(migrator)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("an error occurred during database migration")
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// backupVersion will backup the database or panic if any errors occur
|
||||
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
|
||||
log.Info().Msg("backing up database prior to version upgrade")
|
||||
|
||||
options := getBackupRestoreOptions(store.commonBackupDir())
|
||||
|
||||
_, err := store.backupWithOptions(options)
|
||||
err = store.VersionService.StoreIsUpdating(true)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("an error occurred during database backup")
|
||||
return errors.Wrap(err, "while updating the store")
|
||||
}
|
||||
|
||||
removalErr := store.removeWithOptions(options)
|
||||
if removalErr != nil {
|
||||
log.Error().Err(err).Msg("an error occurred during store removal prior to backup")
|
||||
}
|
||||
// now update the version to the new struct (if required)
|
||||
err = store.finishMigrateLegacyVersion(version)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "while updating version")
|
||||
}
|
||||
|
||||
log.Info().Msg("migrating database from version " + version.SchemaVersion + " to " + portainer.APIVersion)
|
||||
|
||||
err = migrator.Migrate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = store.VersionService.StoreIsUpdating(false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to update the store")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,39 +10,41 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// testVersion is a helper which tests current store version against wanted version
|
||||
func testVersion(store *Store, versionWant int, t *testing.T) {
|
||||
v, err := store.VersionService.DBVersion()
|
||||
func testVersion(store *Store, versionWant string, t *testing.T) {
|
||||
v, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
t.Errorf("Expect store version to be %d but was %d with error: %s", versionWant, v, err)
|
||||
t.Errorf("Expect store version to be %s but was %s with error: %s", versionWant, v.SchemaVersion, err)
|
||||
}
|
||||
if v != versionWant {
|
||||
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
|
||||
if v.SchemaVersion != versionWant {
|
||||
t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateData(t *testing.T) {
|
||||
snapshotTests := []struct {
|
||||
testName string
|
||||
srcPath string
|
||||
wantPath string
|
||||
testName string
|
||||
srcPath string
|
||||
wantPath string
|
||||
overrideInstanceId bool
|
||||
}{
|
||||
{
|
||||
testName: "migrate version 24 to latest",
|
||||
srcPath: "test_data/input_24.json",
|
||||
wantPath: "test_data/output_24_to_latest.json",
|
||||
testName: "migrate version 24 to latest",
|
||||
srcPath: "test_data/input_24.json",
|
||||
wantPath: "test_data/output_24_to_latest.json",
|
||||
overrideInstanceId: true,
|
||||
},
|
||||
}
|
||||
for _, test := range snapshotTests {
|
||||
t.Run(test.testName, func(t *testing.T) {
|
||||
err := migrateDBTestHelper(t, test.srcPath, test.wantPath)
|
||||
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
|
||||
if err != nil {
|
||||
t.Errorf(
|
||||
"Failed migrating mock database %v: %v",
|
||||
@@ -53,111 +55,111 @@ func TestMigrateData(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||
newStore, store, teardown := MustNewTestStore(t, false, true)
|
||||
defer teardown()
|
||||
// t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||
// newStore, store, teardown := MustNewTestStore(t, true, false)
|
||||
// defer teardown()
|
||||
|
||||
if !newStore {
|
||||
t.Error("Expect a new DB")
|
||||
}
|
||||
// if !newStore {
|
||||
// t.Error("Expect a new DB")
|
||||
// }
|
||||
|
||||
// not called for new stores
|
||||
//store.MigrateData()
|
||||
// testVersion(store, portainer.APIVersion, t)
|
||||
// store.Close()
|
||||
|
||||
testVersion(store, portainer.DBVersion, t)
|
||||
store.Close()
|
||||
// newStore, _ = store.Open()
|
||||
// if newStore {
|
||||
// t.Error("Expect store to NOT be new DB")
|
||||
// }
|
||||
// })
|
||||
|
||||
newStore, _ = store.Open()
|
||||
if newStore {
|
||||
t.Error("Expect store to NOT be new DB")
|
||||
}
|
||||
})
|
||||
// tests := []struct {
|
||||
// version string
|
||||
// expectedVersion string
|
||||
// }{
|
||||
// {version: "1.24.1", expectedVersion: portainer.APIVersion},
|
||||
// {version: "2.0.0", expectedVersion: portainer.APIVersion},
|
||||
// }
|
||||
// for _, tc := range tests {
|
||||
// _, store, teardown := MustNewTestStore(t, true, true)
|
||||
// defer teardown()
|
||||
|
||||
tests := []struct {
|
||||
version int
|
||||
expectedVersion int
|
||||
}{
|
||||
{version: 17, expectedVersion: portainer.DBVersion},
|
||||
{version: 21, expectedVersion: portainer.DBVersion},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
_, store, teardown := MustNewTestStore(t, true, true)
|
||||
defer teardown()
|
||||
// // Setup data
|
||||
// v := models.Version{SchemaVersion: tc.version}
|
||||
// store.VersionService.UpdateVersion(&v)
|
||||
|
||||
// Setup data
|
||||
store.VersionService.StoreDBVersion(tc.version)
|
||||
// // Required roles by migrations 22.2
|
||||
// store.RoleService.Create(&portainer.Role{ID: 1})
|
||||
// store.RoleService.Create(&portainer.Role{ID: 2})
|
||||
// store.RoleService.Create(&portainer.Role{ID: 3})
|
||||
// store.RoleService.Create(&portainer.Role{ID: 4})
|
||||
|
||||
// Required roles by migrations 22.2
|
||||
store.RoleService.Create(&portainer.Role{ID: 1})
|
||||
store.RoleService.Create(&portainer.Role{ID: 2})
|
||||
store.RoleService.Create(&portainer.Role{ID: 3})
|
||||
store.RoleService.Create(&portainer.Role{ID: 4})
|
||||
// t.Run(fmt.Sprintf("MigrateData for version %s", tc.version), func(t *testing.T) {
|
||||
// store.MigrateData()
|
||||
// testVersion(store, tc.expectedVersion, t)
|
||||
// })
|
||||
|
||||
t.Run(fmt.Sprintf("MigrateData for version %d", tc.version), func(t *testing.T) {
|
||||
store.MigrateData()
|
||||
testVersion(store, tc.expectedVersion, t)
|
||||
})
|
||||
// t.Run(fmt.Sprintf("Restoring DB after migrateData for version %s", tc.version), func(t *testing.T) {
|
||||
// store.Rollback(true)
|
||||
// store.Open()
|
||||
// testVersion(store, tc.version, t)
|
||||
// })
|
||||
// }
|
||||
|
||||
t.Run(fmt.Sprintf("Restoring DB after migrateData for version %d", tc.version), func(t *testing.T) {
|
||||
store.Rollback(true)
|
||||
store.Open()
|
||||
testVersion(store, tc.version, t)
|
||||
})
|
||||
}
|
||||
// t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
|
||||
// _, store, teardown := MustNewTestStore(t, false, true)
|
||||
// defer teardown()
|
||||
|
||||
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(t, false, true)
|
||||
defer teardown()
|
||||
// v := models.Version{SchemaVersion: "1.24.1"}
|
||||
// store.VersionService.UpdateVersion(&v)
|
||||
|
||||
version := 17
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
// store.MigrateData()
|
||||
|
||||
store.MigrateData()
|
||||
// testVersion(store, v.SchemaVersion, t)
|
||||
// })
|
||||
|
||||
testVersion(store, version, t)
|
||||
})
|
||||
// t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
||||
// _, store, teardown := MustNewTestStore(t, false, true)
|
||||
// defer teardown()
|
||||
|
||||
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(t, false, true)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(0)
|
||||
// v := models.Version{SchemaVersion: "0.0.0"}
|
||||
// store.VersionService.UpdateVersion(&v)
|
||||
|
||||
store.MigrateData()
|
||||
// store.MigrateData()
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||
|
||||
if !isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should exist; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
// if !isFileExist(options.BackupPath) {
|
||||
// t.Errorf("Backup file should exist; file=%s", options.BackupPath)
|
||||
// }
|
||||
// })
|
||||
|
||||
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(t, false, true)
|
||||
defer teardown()
|
||||
// t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
|
||||
// _, store, teardown := MustNewTestStore(t, false, true)
|
||||
// defer teardown()
|
||||
|
||||
store.VersionService.StoreIsUpdating(true)
|
||||
// store.VersionService.StoreIsUpdating(true)
|
||||
|
||||
store.MigrateData()
|
||||
// store.MigrateData()
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||
|
||||
if isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
// if isFileExist(options.BackupPath) {
|
||||
// t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
// }
|
||||
// })
|
||||
|
||||
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
||||
_, store, teardown := MustNewTestStore(t, false, true)
|
||||
defer teardown()
|
||||
// t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
||||
// _, store, teardown := MustNewTestStore(t, false, true)
|
||||
// defer teardown()
|
||||
|
||||
store.MigrateData()
|
||||
// store.MigrateData()
|
||||
|
||||
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||
|
||||
if isFileExist(options.BackupPath) {
|
||||
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
}
|
||||
})
|
||||
// if isFileExist(options.BackupPath) {
|
||||
// t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
func Test_getBackupRestoreOptions(t *testing.T) {
|
||||
@@ -179,18 +181,23 @@ func Test_getBackupRestoreOptions(t *testing.T) {
|
||||
|
||||
func TestRollback(t *testing.T) {
|
||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||
version := 21
|
||||
_, store, teardown := MustNewTestStore(t, false, true)
|
||||
version := models.Version{SchemaVersion: "2.4.0"}
|
||||
_, store, teardown := MustNewTestStore(t, true, false)
|
||||
defer teardown()
|
||||
store.VersionService.StoreDBVersion(version)
|
||||
|
||||
_, err := store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||
err := store.VersionService.UpdateVersion(&version)
|
||||
if err != nil {
|
||||
t.Errorf("Failed updating version: %v", err)
|
||||
}
|
||||
|
||||
_, err = store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
|
||||
// Change the current edition
|
||||
err = store.VersionService.StoreDBVersion(version + 10)
|
||||
// Change the current version
|
||||
version2 := models.Version{SchemaVersion: "2.6.0"}
|
||||
err = store.VersionService.UpdateVersion(&version2)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("")
|
||||
}
|
||||
@@ -199,12 +206,17 @@ func TestRollback(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Logf("Rollback failed: %s", err)
|
||||
t.Fail()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
store.Open()
|
||||
testVersion(store, version, t)
|
||||
_, err = store.Open()
|
||||
if err != nil {
|
||||
t.Logf("Open failed: %s", err)
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
testVersion(store, version.SchemaVersion, t)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -220,15 +232,20 @@ func isFileExist(path string) bool {
|
||||
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
|
||||
// parses it into a database, runs a migration on that database, and then
|
||||
// compares it with an expected output database.
|
||||
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
|
||||
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanceId bool) error {
|
||||
srcJSON, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
|
||||
}
|
||||
|
||||
// Parse source json to db.
|
||||
_, store, teardown := MustNewTestStore(t, true, false)
|
||||
defer teardown()
|
||||
// When we create a new test store, it sets its version field automatically to latest.
|
||||
_, store, _ := MustNewTestStore(t, true, false)
|
||||
|
||||
fmt.Println("store.path=", store.GetConnection().GetDatabaseFilePath())
|
||||
store.connection.DeleteObject("version", []byte("VERSION"))
|
||||
|
||||
// defer teardown()
|
||||
err = importJSON(t, bytes.NewReader(srcJSON), store)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -240,6 +257,21 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if overrideInstanceId {
|
||||
// old versions of portainer did not have instance-id. Because this gets generated
|
||||
// we need to override the expected output to match the expected value to pass the test
|
||||
v, err := store.VersionService.Version()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v.InstanceID = "463d5c47-0ea5-4aca-85b1-405ceefee254"
|
||||
err = store.VersionService.UpdateVersion(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that our database connection is using bolt so we can call
|
||||
// exportJson rather than ExportRaw. The exportJson function allows us to
|
||||
// strip out the metadata which we don't want for our tests.
|
||||
@@ -256,7 +288,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
|
||||
// Convert database back to json.
|
||||
databasePath := con.GetDatabaseFilePath()
|
||||
if _, err := os.Stat(databasePath); err != nil {
|
||||
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
|
||||
return fmt.Errorf("stat on %s failed: %w", databasePath, err)
|
||||
}
|
||||
|
||||
gotJSON, err := con.ExportJSON(databasePath, false)
|
||||
@@ -275,7 +307,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
|
||||
|
||||
// Compare the result we got with the one we wanted.
|
||||
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
|
||||
gotPath := filepath.Join(t.TempDir(), "portainer-migrator-test-fail.json")
|
||||
gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
|
||||
os.WriteFile(
|
||||
gotPath,
|
||||
gotJSON,
|
||||
@@ -316,42 +348,65 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
||||
t.Logf("failed casting %s to map[string]interface{}", k)
|
||||
}
|
||||
|
||||
// New format db
|
||||
version, ok := versions["VERSION"]
|
||||
if ok {
|
||||
err := con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("VERSION"),
|
||||
version,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing VERSION in %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
// old format db
|
||||
|
||||
dbVersion, ok := versions["DB_VERSION"]
|
||||
if !ok {
|
||||
t.Logf("failed getting DB_VERSION from %s", k)
|
||||
}
|
||||
if ok {
|
||||
numDBVersion, ok := dbVersion.(json.Number)
|
||||
if !ok {
|
||||
t.Logf("failed parsing DB_VERSION as json number from %s", k)
|
||||
}
|
||||
|
||||
numDBVersion, ok := dbVersion.(json.Number)
|
||||
if !ok {
|
||||
t.Logf("failed parsing DB_VERSION as json number from %s", k)
|
||||
}
|
||||
intDBVersion, err := numDBVersion.Int64()
|
||||
if err != nil {
|
||||
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
|
||||
}
|
||||
|
||||
intDBVersion, err := numDBVersion.Int64()
|
||||
if err != nil {
|
||||
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
|
||||
}
|
||||
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("DB_VERSION"),
|
||||
int(intDBVersion),
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("DB_VERSION"),
|
||||
int(intDBVersion),
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
instanceID, ok := versions["INSTANCE_ID"]
|
||||
if !ok {
|
||||
t.Logf("failed getting INSTANCE_ID from %s", k)
|
||||
if ok {
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("INSTANCE_ID"),
|
||||
instanceID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("INSTANCE_ID"),
|
||||
instanceID,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
|
||||
edition, ok := versions["EDITION"]
|
||||
if ok {
|
||||
err = con.CreateObjectWithStringId(
|
||||
k,
|
||||
[]byte("EDITION"),
|
||||
edition,
|
||||
)
|
||||
if err != nil {
|
||||
t.Logf("failed writing EDITION in %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
case "dockerhub":
|
||||
|
||||
@@ -54,7 +54,6 @@ func TestMigrateSettings(t *testing.T) {
|
||||
}
|
||||
|
||||
m := migrator.NewMigrator(&migrator.MigratorParameters{
|
||||
DatabaseVersion: 29,
|
||||
EndpointGroupService: store.EndpointGroupService,
|
||||
EndpointService: store.EndpointService,
|
||||
EndpointRelationService: store.EndpointRelationService,
|
||||
|
||||
115
api/datastore/migrate_legacyversion.go
Normal file
115
api/datastore/migrate_legacyversion.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/database/models"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
const (
|
||||
bucketName = "version"
|
||||
legacyDBVersionKey = "DB_VERSION"
|
||||
legacyInstanceKey = "INSTANCE_ID"
|
||||
legacyEditionKey = "EDITION"
|
||||
)
|
||||
|
||||
var dbVerToSemVerMap = map[int]string{
|
||||
18: "1.21",
|
||||
19: "1.22",
|
||||
20: "1.22.1",
|
||||
21: "1.22.2",
|
||||
22: "1.23",
|
||||
23: "1.24",
|
||||
24: "1.24.1",
|
||||
25: "2.0",
|
||||
26: "2.1",
|
||||
27: "2.2",
|
||||
28: "2.4",
|
||||
29: "2.4",
|
||||
30: "2.6",
|
||||
31: "2.7",
|
||||
32: "2.9",
|
||||
33: "2.9.1",
|
||||
34: "2.10",
|
||||
35: "2.9.3",
|
||||
36: "2.11",
|
||||
40: "2.13",
|
||||
50: "2.14",
|
||||
51: "2.14.1",
|
||||
52: "2.14.2",
|
||||
60: "2.15",
|
||||
61: "2.15.1",
|
||||
70: "2.16",
|
||||
80: "2.17",
|
||||
}
|
||||
|
||||
func dbVersionToSemanticVersion(dbVersion int) string {
|
||||
if dbVersion < 18 {
|
||||
return "1.0.0"
|
||||
}
|
||||
|
||||
ver, ok := dbVerToSemVerMap[dbVersion]
|
||||
if ok {
|
||||
return ver
|
||||
}
|
||||
|
||||
// We should always return something sensible
|
||||
switch {
|
||||
case dbVersion < 40:
|
||||
return "2.11"
|
||||
case dbVersion < 50:
|
||||
return "2.13"
|
||||
case dbVersion < 60:
|
||||
return "2.14.2"
|
||||
case dbVersion < 70:
|
||||
return "2.15.1"
|
||||
}
|
||||
|
||||
return "2.16.0"
|
||||
}
|
||||
|
||||
// getOrMigrateLegacyVersion to new Version struct
|
||||
func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
|
||||
// Very old versions of portainer did not have a version bucket, lets set some defaults
|
||||
dbVersion := 24
|
||||
edition := int(portaineree.PortainerCE)
|
||||
instanceId := ""
|
||||
|
||||
// If we already have a version key, we don't need to migrate
|
||||
v, err := store.VersionService.Version()
|
||||
if err == nil || !dataservices.IsErrObjectNotFound(err) {
|
||||
return v, err
|
||||
}
|
||||
|
||||
err = store.connection.GetObject(bucketName, []byte(legacyDBVersionKey), &dbVersion)
|
||||
if err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = store.connection.GetObject(bucketName, []byte(legacyEditionKey), &edition)
|
||||
if err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = store.connection.GetObject(bucketName, []byte(legacyInstanceKey), &instanceId)
|
||||
if err != nil && !dataservices.IsErrObjectNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.Version{
|
||||
SchemaVersion: dbVersionToSemanticVersion(dbVersion),
|
||||
Edition: edition,
|
||||
InstanceID: string(instanceId),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// finishMigrateLegacyVersion writes the new version to the DB and removes the old version keys from the DB
|
||||
func (store *Store) finishMigrateLegacyVersion(versionToWrite *models.Version) error {
|
||||
err := store.VersionService.UpdateVersion(versionToWrite)
|
||||
|
||||
// Remove legacy keys if present
|
||||
store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey))
|
||||
store.connection.DeleteObject(bucketName, []byte(legacyEditionKey))
|
||||
store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
|
||||
return err
|
||||
}
|
||||
117
api/datastore/migrate_post_init.go
Normal file
117
api/datastore/migrate_post_init.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type PostInitMigrator struct {
|
||||
kubeFactory *cli.ClientFactory
|
||||
dockerFactory *dockerclient.ClientFactory
|
||||
dataStore dataservices.DataStore
|
||||
}
|
||||
|
||||
func NewPostInitMigrator(kubeFactory *cli.ClientFactory, dockerFactory *dockerclient.ClientFactory, dataStore dataservices.DataStore) *PostInitMigrator {
|
||||
return &PostInitMigrator{
|
||||
kubeFactory: kubeFactory,
|
||||
dockerFactory: dockerFactory,
|
||||
dataStore: dataStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (migrator *PostInitMigrator) PostInitMigrate() error {
|
||||
if err := migrator.PostInitMigrateIngresses(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrator.PostInitMigrateGPUs()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (migrator *PostInitMigrator) PostInitMigrateIngresses() error {
|
||||
endpoints, err := migrator.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
// Early exit if we do not need to migrate!
|
||||
if !endpoints[i].PostInitMigrations.MigrateIngresses {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := migrator.kubeFactory.MigrateEndpointIngresses(&endpoints[i])
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Msg("failure migrating endpoint ingresses")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostInitMigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
|
||||
// If there's an error getting the containers, we'll log it and move on
|
||||
func (migrator *PostInitMigrator) PostInitMigrateGPUs() {
|
||||
environments, err := migrator.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failure getting endpoints")
|
||||
return
|
||||
}
|
||||
|
||||
for i := range environments {
|
||||
if environments[i].Type == portainer.DockerEnvironment {
|
||||
// // Early exit if we do not need to migrate!
|
||||
if !environments[i].PostInitMigrations.MigrateGPUs {
|
||||
return
|
||||
}
|
||||
|
||||
// set the MigrateGPUs flag to false so we don't run this again
|
||||
environments[i].PostInitMigrations.MigrateGPUs = false
|
||||
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
|
||||
|
||||
// create a docker client
|
||||
dockerClient, err := migrator.dockerFactory.CreateClient(&environments[i], "", nil)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failure creating docker client for environment: " + environments[i].Name)
|
||||
return
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
// get all containers
|
||||
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to list containers")
|
||||
return
|
||||
}
|
||||
|
||||
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole endpoint
|
||||
containersLoop:
|
||||
for _, container := range containers {
|
||||
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
|
||||
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to inspect container")
|
||||
return
|
||||
}
|
||||
|
||||
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
|
||||
for _, deviceRequest := range deviceRequests {
|
||||
if deviceRequest.Driver == "nvidia" {
|
||||
environments[i].EnableGPUManagement = true
|
||||
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
|
||||
|
||||
break containersLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,140 +4,122 @@ import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type migration struct {
|
||||
dbversion int
|
||||
migrate func() error
|
||||
}
|
||||
|
||||
func migrationError(err error, context string) error {
|
||||
return errors.Wrap(err, "failed in "+context)
|
||||
}
|
||||
|
||||
func newMigration(dbversion int, migrate func() error) migration {
|
||||
return migration{
|
||||
dbversion: dbversion,
|
||||
migrate: migrate,
|
||||
}
|
||||
}
|
||||
|
||||
func dbTooOldError() error {
|
||||
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
|
||||
}
|
||||
|
||||
func GetFunctionName(i interface{}) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
// set DB to updating status
|
||||
err := m.versionService.StoreIsUpdating(true)
|
||||
version, err := m.versionService.Version()
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreIsUpdating")
|
||||
return migrationError(err, "get version service")
|
||||
}
|
||||
|
||||
migrations := []migration{
|
||||
// Portainer < 1.21.0
|
||||
newMigration(17, dbTooOldError),
|
||||
|
||||
// Portainer 1.21.0
|
||||
newMigration(18, m.updateUsersToDBVersion18),
|
||||
newMigration(18, m.updateEndpointsToDBVersion18),
|
||||
newMigration(18, m.updateEndpointGroupsToDBVersion18),
|
||||
newMigration(18, m.updateRegistriesToDBVersion18),
|
||||
|
||||
// 1.22.0
|
||||
newMigration(19, m.updateSettingsToDBVersion19),
|
||||
|
||||
// 1.22.1
|
||||
newMigration(20, m.updateUsersToDBVersion20),
|
||||
newMigration(20, m.updateSettingsToDBVersion20),
|
||||
newMigration(20, m.updateSchedulesToDBVersion20),
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
newMigration(22, m.updateResourceControlsToDBVersion22),
|
||||
newMigration(22, m.updateUsersAndRolesToDBVersion22),
|
||||
|
||||
// Portainer 1.24.0
|
||||
newMigration(23, m.updateTagsToDBVersion23),
|
||||
newMigration(23, m.updateEndpointsAndEndpointGroupsToDBVersion23),
|
||||
|
||||
// Portainer 1.24.1
|
||||
newMigration(24, m.updateSettingsToDB24),
|
||||
|
||||
// Portainer 2.0.0
|
||||
newMigration(25, m.updateSettingsToDB25),
|
||||
newMigration(25, m.updateStacksToDB24), // yes this looks odd. Don't be tempted to move it
|
||||
|
||||
// Portainer 2.1.0
|
||||
newMigration(26, m.updateEndpointSettingsToDB25),
|
||||
|
||||
// Portainer 2.2.0
|
||||
newMigration(27, m.updateStackResourceControlToDB27),
|
||||
|
||||
// Portainer 2.6.0
|
||||
newMigration(30, m.migrateDBVersionToDB30),
|
||||
|
||||
// Portainer 2.9.0
|
||||
newMigration(32, m.migrateDBVersionToDB32),
|
||||
|
||||
// Portainer 2.9.1, 2.9.2
|
||||
newMigration(33, m.migrateDBVersionToDB33),
|
||||
|
||||
// Portainer 2.10
|
||||
newMigration(34, m.migrateDBVersionToDB34),
|
||||
|
||||
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
|
||||
newMigration(35, m.migrateDBVersionToDB35),
|
||||
|
||||
newMigration(36, m.migrateDBVersionToDB36),
|
||||
|
||||
// Portainer 2.13
|
||||
newMigration(40, m.migrateDBVersionToDB40),
|
||||
|
||||
// Portainer 2.14
|
||||
newMigration(50, m.migrateDBVersionToDB50),
|
||||
|
||||
// Portainer 2.15
|
||||
newMigration(60, m.migrateDBVersionToDB60),
|
||||
|
||||
// Portainer 2.16
|
||||
newMigration(70, m.migrateDBVersionToDB70),
|
||||
schemaVersion, err := semver.NewVersion(version.SchemaVersion)
|
||||
if err != nil {
|
||||
return migrationError(err, "invalid db schema version")
|
||||
}
|
||||
|
||||
var lastDbVersion int
|
||||
for _, migration := range migrations {
|
||||
if m.currentDBVersion < migration.dbversion {
|
||||
newMigratorCount := 0
|
||||
apiVersion := semver.MustParse(portainer.APIVersion)
|
||||
if schemaVersion.Equal(apiVersion) {
|
||||
// detect and run migrations when the versions are the same.
|
||||
// e.g. development builds
|
||||
latestMigrations := m.LatestMigrations()
|
||||
if latestMigrations.Version.Equal(schemaVersion) &&
|
||||
version.MigratorCount != len(latestMigrations.MigrationFuncs) {
|
||||
err := runMigrations(latestMigrations.MigrationFuncs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newMigratorCount = len(latestMigrations.MigrationFuncs)
|
||||
}
|
||||
} else {
|
||||
// regular path when major/minor/patch versions differ
|
||||
for _, migration := range m.migrations {
|
||||
if schemaVersion.LessThan(migration.Version) {
|
||||
|
||||
// Print the next line only when the version changes
|
||||
if migration.dbversion > lastDbVersion {
|
||||
log.Info().Int("to_version", migration.dbversion).Msg("migrating DB")
|
||||
log.Info().Msgf("migrating data to %s", migration.Version.String())
|
||||
err := runMigrations(migration.MigrationFuncs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := migration.migrate()
|
||||
if err != nil {
|
||||
return migrationError(err, GetFunctionName(migration.migrate))
|
||||
if apiVersion.Equal(migration.Version) {
|
||||
newMigratorCount = len(migration.MigrationFuncs)
|
||||
}
|
||||
}
|
||||
lastDbVersion = migration.dbversion
|
||||
}
|
||||
|
||||
log.Info().Int("version", portainer.DBVersion).Msg("setting DB version")
|
||||
err = m.Always()
|
||||
if err != nil {
|
||||
return migrationError(err, "Always migrations returned error")
|
||||
}
|
||||
|
||||
err = m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
version.SchemaVersion = portainer.APIVersion
|
||||
version.MigratorCount = newMigratorCount
|
||||
|
||||
err = m.versionService.UpdateVersion(version)
|
||||
if err != nil {
|
||||
return migrationError(err, "StoreDBVersion")
|
||||
}
|
||||
|
||||
log.Info().Int("version", portainer.DBVersion).Msg("updated DB version")
|
||||
log.Info().Msgf("db migrated to %s", portainer.APIVersion)
|
||||
|
||||
// reset DB updating status
|
||||
return m.versionService.StoreIsUpdating(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMigrations(migrationFuncs []func() error) error {
|
||||
for _, migrationFunc := range migrationFuncs {
|
||||
err := migrationFunc()
|
||||
if err != nil {
|
||||
return migrationError(err, GetFunctionName(migrationFunc))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) NeedsMigration() bool {
|
||||
// we need to migrate if anything changes with the version in the DB vs what our software version is.
|
||||
// If the version matches, then it's all down to the number of migration funcs we have for the current version
|
||||
// i.e. the MigratorCount
|
||||
|
||||
// In this particular instance we should log a fatal error
|
||||
if m.CurrentDBEdition() != portainer.PortainerCE {
|
||||
log.Fatal().Msg("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
|
||||
return false
|
||||
}
|
||||
|
||||
if m.CurrentSemanticDBVersion().LessThan(semver.MustParse(portainer.APIVersion)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if we have any migrations for the current version
|
||||
latestMigrations := m.LatestMigrations()
|
||||
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
|
||||
if m.currentDBVersion.MigratorCount != len(latestMigrations.MigrationFuncs) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// One remaining possibility if we get here. If our migrator count > 0 and we have no migration funcs
|
||||
// for the current version (i.e. they were deleted during development). Then we we need to migrate.
|
||||
// This is to reset the migrator count back to 0
|
||||
if m.currentDBVersion.MigratorCount > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
29
api/datastore/migrator/migrate_dbversion100.go
Normal file
29
api/datastore/migrator/migrate_dbversion100.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDockerDesktopExtentionSetting() error {
|
||||
log.Info().Msg("updating docker desktop extention flag in settings")
|
||||
|
||||
isDDExtension := false
|
||||
if _, ok := os.LookupEnv("DOCKER_EXTENSION"); ok {
|
||||
isDDExtension = true
|
||||
}
|
||||
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings.IsDockerDesktopExtension = isDDExtension
|
||||
err = m.settingsService.UpdateSettings(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -25,7 +25,7 @@ func (m *Migrator) updateStackResourceControlToDB27() error {
|
||||
|
||||
stack, err := m.stackService.StackByName(stackName)
|
||||
if err != nil {
|
||||
if err == errors.ErrObjectNotFound {
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices/errors"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
|
||||
|
||||
@@ -86,7 +86,7 @@ func (m *Migrator) updateDockerhubToDB32() error {
|
||||
log.Info().Msg("updating dockerhub")
|
||||
|
||||
dockerhub, err := m.dockerhubService.DockerHub()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
|
||||
@@ -12,7 +12,7 @@ func (m *Migrator) migrateDBVersionToDB34() error {
|
||||
return MigrateStackEntryPoint(m.stackService)
|
||||
}
|
||||
|
||||
// MigrateStackEntryPoint exported for testing (blah.)
|
||||
// MigrateStackEntryPoint exported for testing
|
||||
func MigrateStackEntryPoint(stackService dataservices.StackService) error {
|
||||
stacks, err := stackService.Stacks()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
package migrator
|
||||
|
||||
import "github.com/rs/zerolog/log"
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB35() error {
|
||||
// These should have been migrated already, but due to an earlier bug and a bunch of duplicates,
|
||||
// calling it again will now fix the issue as the function has been repaired.
|
||||
|
||||
log.Info().Msg("updating dockerhub registries")
|
||||
|
||||
return m.updateDockerhubToDB32()
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package migrator
|
||||
|
||||
const (
|
||||
db35TestFile = "portainer-mig-35.db"
|
||||
username = "portainer"
|
||||
password = "password"
|
||||
)
|
||||
|
||||
// TODO: this is exactly the kind of reaching into the internals of the store we should not do
|
||||
// func setupDB35Test(t *testing.T) *Migrator {
|
||||
// is := assert.New(t)
|
||||
// dbConn, err := bolt.Open(path.Join(t.TempDir(), db35TestFile), 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
// is.NoError(err, "failed to init testing DB connection")
|
||||
|
||||
// // Create an old style dockerhub authenticated account
|
||||
// dockerhubService, err := dockerhub.NewService(&database.DbConnection{DB: dbConn})
|
||||
// is.NoError(err, "failed to init testing registry service")
|
||||
// err = dockerhubService.UpdateDockerHub(&portainer.DockerHub{true, username, password})
|
||||
// is.NoError(err, "failed to create dockerhub account")
|
||||
|
||||
// registryService, err := registry.NewService(&database.DbConnection{DB: dbConn})
|
||||
// is.NoError(err, "failed to init testing registry service")
|
||||
|
||||
// endpointService, err := endpoint.NewService(&database.DbConnection{DB: dbConn})
|
||||
// is.NoError(err, "failed to init endpoint service")
|
||||
|
||||
// m := &Migrator{
|
||||
// db: dbConn,
|
||||
// dockerhubService: dockerhubService,
|
||||
// registryService: registryService,
|
||||
// endpointService: endpointService,
|
||||
// }
|
||||
|
||||
// return m
|
||||
// }
|
||||
|
||||
// // TestUpdateDockerhubToDB32 tests a normal upgrade
|
||||
// func TestUpdateDockerhubToDB32(t *testing.T) {
|
||||
// is := assert.New(t)
|
||||
// m := setupDB35Test(t)
|
||||
// defer m.db.Close()
|
||||
// defer os.Remove(db35TestFile)
|
||||
|
||||
// if err := m.updateDockerhubToDB32(); err != nil {
|
||||
// t.Errorf("failed to update settings: %v", err)
|
||||
// }
|
||||
|
||||
// // Verify we have a single registry were created
|
||||
// registries, err := m.registryService.Registries()
|
||||
// is.NoError(err, "failed to read registries from the RegistryService")
|
||||
// is.Equal(len(registries), 1, "only one migrated registry expected")
|
||||
// }
|
||||
|
||||
// // TestUpdateDockerhubToDB32_with_duplicate_migrations tests an upgrade where in earlier versions a broken migration
|
||||
// // created a large number of duplicate "dockerhub migrated" registry entries.
|
||||
// func TestUpdateDockerhubToDB32_with_duplicate_migrations(t *testing.T) {
|
||||
// is := assert.New(t)
|
||||
// m := setupDB35Test(t)
|
||||
// defer m.db.Close()
|
||||
// defer os.Remove(db35TestFile)
|
||||
|
||||
// // Create lots of duplicate entries...
|
||||
// registry := &portainer.Registry{
|
||||
// Type: portainer.DockerHubRegistry,
|
||||
// Name: "Dockerhub (authenticated - migrated)",
|
||||
// URL: "docker.io",
|
||||
// Authentication: true,
|
||||
// Username: "portainer",
|
||||
// Password: "password",
|
||||
// RegistryAccesses: portainer.RegistryAccesses{},
|
||||
// }
|
||||
|
||||
// for i := 1; i < 150; i++ {
|
||||
// err := m.registryService.CreateRegistry(registry)
|
||||
// assert.NoError(t, err, "create registry failed")
|
||||
// }
|
||||
|
||||
// // Verify they were created
|
||||
// registries, err := m.registryService.Registries()
|
||||
// is.NoError(err, "failed to read registries from the RegistryService")
|
||||
// is.Condition(func() bool {
|
||||
// return len(registries) > 1
|
||||
// }, "expected multiple duplicate registry entries")
|
||||
|
||||
// // Now run the migrator
|
||||
// if err := m.updateDockerhubToDB32(); err != nil {
|
||||
// t.Errorf("failed to update settings: %v", err)
|
||||
// }
|
||||
|
||||
// // Verify we have a single registry were created
|
||||
// registries, err = m.registryService.Registries()
|
||||
// is.NoError(err, "failed to read registries from the RegistryService")
|
||||
// is.Equal(len(registries), 1, "only one migrated registry expected")
|
||||
// }
|
||||
@@ -19,12 +19,13 @@ func (m *Migrator) addGpuInputFieldDB60() error {
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint.Gpus = []portainer.Pair{}
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
if endpoint.Gpus == nil {
|
||||
endpoint.Gpus = []portainer.Pair{}
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB70() error {
|
||||
log.Info().Msg("- add IngressAvailabilityPerNamespace field")
|
||||
if err := m.addIngressAvailabilityPerNamespaceFieldDB70(); err != nil {
|
||||
log.Info().Msg("add IngressAvailabilityPerNamespace field")
|
||||
if err := m.updateIngressFieldsForEnvDB70(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (m *Migrator) migrateDBVersionToDB70() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error {
|
||||
func (m *Migrator) updateIngressFieldsForEnvDB70() error {
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -59,6 +59,8 @@ func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error {
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = true
|
||||
endpoint.Kubernetes.Configuration.AllowNoneIngressClass = false
|
||||
endpoint.PostInitMigrations.MigrateIngresses = true
|
||||
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
|
||||
36
api/datastore/migrator/migrate_dbversion71.go
Normal file
36
api/datastore/migrator/migrate_dbversion71.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB71() error {
|
||||
log.Info().Msg("removing orphaned snapshots")
|
||||
|
||||
snapshots, err := m.snapshotService.Snapshots()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range snapshots {
|
||||
_, err := m.endpointService.Endpoint(s.EndpointID)
|
||||
if err == nil {
|
||||
log.Debug().Int("endpoint_id", int(s.EndpointID)).Msg("keeping snapshot")
|
||||
continue
|
||||
} else if !dataservices.IsErrObjectNotFound(err) {
|
||||
log.Debug().Int("endpoint_id", int(s.EndpointID)).Err(err).Msg("database error")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug().Int("endpoint_id", int(s.EndpointID)).Msg("removing snapshot")
|
||||
|
||||
err = m.snapshotService.DeleteSnapshot(s.EndpointID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
102
api/datastore/migrator/migrate_dbversion80.go
Normal file
102
api/datastore/migrator/migrate_dbversion80.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB80() error {
|
||||
if err := m.updateEdgeStackStatusForDB80(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.updateExistingEndpointsToNotDetectMetricsAPIForDB80(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.updateExistingEndpointsToNotDetectStorageAPIForDB80(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateExistingEndpointsToNotDetectMetricsAPIForDB80() error {
|
||||
log.Info().Msg("updating existing endpoints to not detect metrics API for existing endpoints (k8s)")
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateExistingEndpointsToNotDetectStorageAPIForDB80() error {
|
||||
log.Info().Msg("updating existing endpoints to not detect metrics API for existing endpoints (k8s)")
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpointutils.IsKubernetesEndpoint(&endpoint) {
|
||||
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEdgeStackStatusForDB80() error {
|
||||
log.Info().Msg("transfer type field to details field for edge stack status")
|
||||
|
||||
edgeStacks, err := m.edgeStackService.EdgeStacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, edgeStack := range edgeStacks {
|
||||
for endpointId, status := range edgeStack.Status {
|
||||
switch status.Type {
|
||||
case portainer.EdgeStackStatusPending:
|
||||
status.Details.Pending = true
|
||||
case portainer.EdgeStackStatusOk:
|
||||
status.Details.Ok = true
|
||||
case portainer.EdgeStackStatusError:
|
||||
status.Details.Error = true
|
||||
case portainer.EdgeStackStatusAcknowledged:
|
||||
status.Details.Acknowledged = true
|
||||
case portainer.EdgeStackStatusRemove:
|
||||
status.Details.Remove = true
|
||||
case portainer.EdgeStackStatusRemoteUpdateSuccess:
|
||||
status.Details.RemoteUpdateSuccess = true
|
||||
}
|
||||
|
||||
edgeStack.Status[endpointId] = status
|
||||
}
|
||||
|
||||
err = m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user