Compare commits
550 Commits
fix/EE-621
...
test-versi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ababd63d97 | ||
|
|
b40d22dc74 | ||
|
|
a257696c25 | ||
|
|
f742937359 | ||
|
|
c0db48b29d | ||
|
|
ea228c3d6d | ||
|
|
da010f3d08 | ||
|
|
32e94d4e4e | ||
|
|
db616bc8a5 | ||
|
|
b8b46ec129 | ||
|
|
7d0b79a546 | ||
|
|
fd26565b14 | ||
|
|
e0b6f2283a | ||
|
|
d3d3d50569 | ||
|
|
cee997e0b3 | ||
|
|
80f53ed6ec | ||
|
|
6f84317e7a | ||
|
|
3cb484f06a | ||
|
|
61353cbe8a | ||
|
|
d647980c3a | ||
|
|
5740abe31b | ||
|
|
5fd4f52e35 | ||
|
|
dbe7cd16d4 | ||
|
|
2b630ca2dd | ||
|
|
2ede22646b | ||
|
|
994b6bb471 | ||
|
|
92f338e0cd | ||
|
|
7a176cf284 | ||
|
|
80e607ab30 | ||
|
|
6cff21477e | ||
|
|
4bb5a7f480 | ||
|
|
9a88511d00 | ||
|
|
48cd614948 | ||
|
|
2fe252d62b | ||
|
|
8fae7f8438 | ||
|
|
e4e55157e8 | ||
|
|
a5e246cc16 | ||
|
|
d28dc59584 | ||
|
|
5353570721 | ||
|
|
eb3e367ba8 | ||
|
|
3c1441d462 | ||
|
|
33ce841040 | ||
|
|
9797201c2a | ||
|
|
6e14ac583b | ||
|
|
0b37b677c1 | ||
|
|
f59dd34154 | ||
|
|
e8ec648886 | ||
|
|
10767a06df | ||
|
|
59b3375b59 | ||
|
|
4408fd0cd3 | ||
|
|
975a9517b9 | ||
|
|
89c92b7834 | ||
|
|
747cea8084 | ||
|
|
f016b31388 | ||
|
|
8cd53a4b7a | ||
|
|
a39abe61c2 | ||
|
|
054898f821 | ||
|
|
13d9b12a2e | ||
|
|
aaec856282 | ||
|
|
009eec9475 | ||
|
|
8d14535fd5 | ||
|
|
cc7f14951c | ||
|
|
b67ff87f35 | ||
|
|
f55ef6e691 | ||
|
|
560a1a00ca | ||
|
|
3b5ce1b053 | ||
|
|
03e8d05f18 | ||
|
|
bedb7fb255 | ||
|
|
4d586f7a85 | ||
|
|
6486a5d971 | ||
|
|
e3364457c4 | ||
|
|
66119a8b57 | ||
|
|
6eb9e906af | ||
|
|
1900fb695d | ||
|
|
a62aac296b | ||
|
|
5294aa2810 | ||
|
|
31bdb948a8 | ||
|
|
468c12c75b | ||
|
|
220fe28830 | ||
|
|
7fd1a644a6 | ||
|
|
6e7a42727a | ||
|
|
ac4b129195 | ||
|
|
85bc14e470 | ||
|
|
6e791a2cfe | ||
|
|
340830d121 | ||
|
|
faca64442f | ||
|
|
854474478c | ||
|
|
4adce14485 | ||
|
|
dc62604ed8 | ||
|
|
f0d43f941f | ||
|
|
9c4935286f | ||
|
|
e1648425ea | ||
|
|
19fa40286a | ||
|
|
1a3db327c7 | ||
|
|
1170004097 | ||
|
|
d2b0eacbf5 | ||
|
|
ca9f85a1ff | ||
|
|
9ee092aa5e | ||
|
|
39bdfa4512 | ||
|
|
e828615467 | ||
|
|
ba4526985a | ||
|
|
607feb183e | ||
|
|
9994ed157a | ||
|
|
bfa27d9103 | ||
|
|
be9d3285e1 | ||
|
|
0f5988af49 | ||
|
|
a28bd349ae | ||
|
|
51f9977885 | ||
|
|
27865981df | ||
|
|
ac3f1cd5c3 | ||
|
|
7549b6cf3f | ||
|
|
dd372ee122 | ||
|
|
6a8e6734f3 | ||
|
|
4ba16f1b04 | ||
|
|
90a19cec5c | ||
|
|
8e480c9fab | ||
|
|
b0e3afa0b6 | ||
|
|
eb6d251a73 | ||
|
|
62c2bf86aa | ||
|
|
4a7f96caf6 | ||
|
|
9c70a43ac3 | ||
|
|
b7cde35c3d | ||
|
|
02fbdfec36 | ||
|
|
94c91035a7 | ||
|
|
5c6c66f010 | ||
|
|
0c870bf37b | ||
|
|
9e0e0a12fa | ||
|
|
c5a1d7e051 | ||
|
|
aaab2fa9d8 | ||
|
|
ef4beef2ea | ||
|
|
1261887c9e | ||
|
|
84fe3cf2a2 | ||
|
|
50fd7c6286 | ||
|
|
d7b412eccc | ||
|
|
d283c63a33 | ||
|
|
d15e2cdc0c | ||
|
|
9cef912c44 | ||
|
|
659abe553d | ||
|
|
014a590704 | ||
|
|
2669a44d79 | ||
|
|
db8f9c6f6c | ||
|
|
2b01136d03 | ||
|
|
fbbf550730 | ||
|
|
3924d0f081 | ||
|
|
00ab9e949a | ||
|
|
42d9dfba36 | ||
|
|
a808f83e7d | ||
|
|
413b9c3b04 | ||
|
|
7edce528d6 | ||
|
|
836df78181 | ||
|
|
a80aa2b45c | ||
|
|
9dd9ffdb3b | ||
|
|
b6daee2850 | ||
|
|
1ba4b590f4 | ||
|
|
e73b1aa49c | ||
|
|
6b5a402962 | ||
|
|
55667a878a | ||
|
|
a0ab82b866 | ||
|
|
6a51b6b41e | ||
|
|
b4e829e8c6 | ||
|
|
06ef12d0ff | ||
|
|
cd5f342da0 | ||
|
|
27e309754e | ||
|
|
6ae0a972d4 | ||
|
|
014c491205 | ||
|
|
4ef71f4aca | ||
|
|
5a5a10821d | ||
|
|
9685e260ea | ||
|
|
f8871fcd2a | ||
|
|
6d17d8bc64 | ||
|
|
46c6a0700f | ||
|
|
5f8fd99fe8 | ||
|
|
8a81d95253 | ||
|
|
f22aed34b5 | ||
|
|
e75e6cb7f7 | ||
|
|
14a365045d | ||
|
|
9b6779515e | ||
|
|
88ee1b5d19 | ||
|
|
a45ec9a7b4 | ||
|
|
51605c6442 | ||
|
|
2fe213d864 | ||
|
|
439f13af19 | ||
|
|
2b5ecd3a57 | ||
|
|
a9ead542b3 | ||
|
|
7479302043 | ||
|
|
10d20e5963 | ||
|
|
5a2e6d0e50 | ||
|
|
9068cfd892 | ||
|
|
5560a444e5 | ||
|
|
505a2d5523 | ||
|
|
2463648161 | ||
|
|
48cf27a3b8 | ||
|
|
39fce3e29b | ||
|
|
4f4c685085 | ||
|
|
d177a70c54 | ||
|
|
cf8ec631dd | ||
|
|
ea61f36e5d | ||
|
|
ffc66647f8 | ||
|
|
6623475035 | ||
|
|
0dd12a218b | ||
|
|
5f89d70fd8 | ||
|
|
3ccbd40232 | ||
|
|
7e9dd01265 | ||
|
|
0fb3555a70 | ||
|
|
73ce754316 | ||
|
|
d304f330e8 | ||
|
|
7333598dba | ||
|
|
bb61e73464 | ||
|
|
c15789eb73 | ||
|
|
e7a2b6268e | ||
|
|
688fa3aa78 | ||
|
|
48bc7d0d92 | ||
|
|
d9df58e93a | ||
|
|
37bba18c81 | ||
|
|
40498d8ddd | ||
|
|
b265810b95 | ||
|
|
09837769d7 | ||
|
|
cf1fd17626 | ||
|
|
785f021898 | ||
|
|
80cc9f18b5 | ||
|
|
5e7e91dd6d | ||
|
|
1032b462b4 | ||
|
|
104307b2b2 | ||
|
|
f8c66a31d9 | ||
|
|
2100155ab5 | ||
|
|
de473fc10e | ||
|
|
76e49ed9a8 | ||
|
|
e9ebef15a0 | ||
|
|
6ff4fd3db2 | ||
|
|
d38085a560 | ||
|
|
3cad13388c | ||
|
|
0b62456236 | ||
|
|
c22d280491 | ||
|
|
960d18998f | ||
|
|
3f3db75d85 | ||
|
|
48aab77058 | ||
|
|
7e53d01d0f | ||
|
|
bd271ec5a1 | ||
|
|
8913e75484 | ||
|
|
c95ffa9e2d | ||
|
|
ddb89f71b4 | ||
|
|
45be6c2b45 | ||
|
|
a00cb951bc | ||
|
|
f584bf3830 | ||
|
|
9600eb6fa1 | ||
|
|
d88ef03ddb | ||
|
|
dc9d7ae3f1 | ||
|
|
a3c7eb0ce0 | ||
|
|
d1ba484be1 | ||
|
|
521eb5f114 | ||
|
|
66770bebd4 | ||
|
|
86c4b3059e | ||
|
|
e3a8853212 | ||
|
|
194b6e491d | ||
|
|
a439695248 | ||
|
|
86f1b8df6e | ||
|
|
a5faddc56c | ||
|
|
9c68c6c9f3 | ||
|
|
d99486ee72 | ||
|
|
946166319f | ||
|
|
26bb028ace | ||
|
|
da615afc92 | ||
|
|
2b53bebcb3 | ||
|
|
d336a14e50 | ||
|
|
4ca6292805 | ||
|
|
44ef5bb12a | ||
|
|
bf600f8b11 | ||
|
|
d6d7afddbc | ||
|
|
61642b8df6 | ||
|
|
07de1b2c06 | ||
|
|
bd3440bf3c | ||
|
|
573f003226 | ||
|
|
6e169662c2 | ||
|
|
31658d4028 | ||
|
|
bb02c69d14 | ||
|
|
73307e164b | ||
|
|
9ea5efb6ba | ||
|
|
3cd58cac54 | ||
|
|
1303a08f5a | ||
|
|
3b1d853090 | ||
|
|
a2a4c85f2d | ||
|
|
506ee389e3 | ||
|
|
8635bc9b9c | ||
|
|
447f497506 | ||
|
|
71292a60b1 | ||
|
|
51449490fa | ||
|
|
ae4970f0ed | ||
|
|
e96d5c245d | ||
|
|
f8e3d75797 | ||
|
|
27aaf322b2 | ||
|
|
b77132dbb1 | ||
|
|
c35473f308 | ||
|
|
a570073d12 | ||
|
|
0ad4826fab | ||
|
|
6db7d31554 | ||
|
|
21d67a971d | ||
|
|
8dfa5efa71 | ||
|
|
529750fa21 | ||
|
|
96b1d36280 | ||
|
|
31c5a82749 | ||
|
|
82516620e7 | ||
|
|
d26d5840f1 | ||
|
|
ebd26316bf | ||
|
|
18dbad232e | ||
|
|
ebcc98d5c5 | ||
|
|
e919da3771 | ||
|
|
eda2dd20ee | ||
|
|
385fd95779 | ||
|
|
88185d7f6d | ||
|
|
253cda8cef | ||
|
|
b34afba7cd | ||
|
|
6c70049ecc | ||
|
|
42c2a52a6b | ||
|
|
19a6a5c608 | ||
|
|
d8e374fb76 | ||
|
|
84ca6185dc | ||
|
|
5088634a41 | ||
|
|
f6beedf0d5 | ||
|
|
3caf1ddb7d | ||
|
|
c622f6da4e | ||
|
|
9ec7394124 | ||
|
|
af8fde66b0 | ||
|
|
709315dde5 | ||
|
|
8856bae5c6 | ||
|
|
90451bfd47 | ||
|
|
0c05539dee | ||
|
|
a2a2c6cf3e | ||
|
|
76aa086d79 | ||
|
|
76fdfeaafc | ||
|
|
5932c78b88 | ||
|
|
68f5ca249f | ||
|
|
2d87a8d8c3 | ||
|
|
988d4103d4 | ||
|
|
ce3a1b8ba5 | ||
|
|
6c89d3c0c9 | ||
|
|
6b91fbf7f4 | ||
|
|
4f3f5e57b6 | ||
|
|
6b3f30e32f | ||
|
|
bdeedb4018 | ||
|
|
50946e087c | ||
|
|
7b89b04667 | ||
|
|
f5f84c5fa4 | ||
|
|
437831fa80 | ||
|
|
31f5b42962 | ||
|
|
7a6c872948 | ||
|
|
4bf18b1d65 | ||
|
|
2d25bf4afa | ||
|
|
56ae19c5ab | ||
|
|
cdf9197274 | ||
|
|
901549e8dd | ||
|
|
80b1cd19cb | ||
|
|
c4942de89b | ||
|
|
80d02f9cd1 | ||
|
|
671b22b5d6 | ||
|
|
43e56bf1c0 | ||
|
|
a175619623 | ||
|
|
63c11d9310 | ||
|
|
4c00b72ae3 | ||
|
|
f4db09a534 | ||
|
|
01cd64037f | ||
|
|
a93344386c | ||
|
|
a2195caa10 | ||
|
|
9ad78753bc | ||
|
|
517190e28b | ||
|
|
5ee6efb145 | ||
|
|
a618ee78e4 | ||
|
|
9a1604e775 | ||
|
|
9615e678e6 | ||
|
|
e39c19bcca | ||
|
|
16ae4f8681 | ||
|
|
70deba50ba | ||
|
|
89359dae8c | ||
|
|
97d227be2a | ||
|
|
8a98704111 | ||
|
|
46b2175729 | ||
|
|
1561814fe5 | ||
|
|
2826a4ce39 | ||
|
|
441a8bbbbf | ||
|
|
2248ce0173 | ||
|
|
b640b58371 | ||
|
|
249b6bc628 | ||
|
|
4a10c2bb07 | ||
|
|
52db4cba0e | ||
|
|
079bade139 | ||
|
|
26e52a0f00 | ||
|
|
3ccc764d40 | ||
|
|
dd068473d2 | ||
|
|
fe47318e26 | ||
|
|
fc7d9ca2cd | ||
|
|
7bf346bd2d | ||
|
|
8f0f9d7aaa | ||
|
|
69c06bc756 | ||
|
|
4a19871fcc | ||
|
|
d5080b6884 | ||
|
|
f7840e0407 | ||
|
|
85ae705833 | ||
|
|
77c38306b2 | ||
|
|
b81babe682 | ||
|
|
4c0049edbe | ||
|
|
7cba02226e | ||
|
|
a15b7cf39a | ||
|
|
36ab4dfb1a | ||
|
|
7b6e106606 | ||
|
|
5f040bf788 | ||
|
|
a4739f1701 | ||
|
|
59f642ea56 | ||
|
|
fa63432695 | ||
|
|
1676fefd97 | ||
|
|
bf66b6c5f3 | ||
|
|
115b01cee3 | ||
|
|
a305fe9e4c | ||
|
|
a58b4f479b | ||
|
|
93593e1379 | ||
|
|
51ae2198f6 | ||
|
|
ccc97e6f78 | ||
|
|
3f28d56bfc | ||
|
|
3103d498cf | ||
|
|
47f29002f0 | ||
|
|
787c7ec4cc | ||
|
|
a8e53a4510 | ||
|
|
752be47fcc | ||
|
|
95474b7dc5 | ||
|
|
7a04d1d4ea | ||
|
|
211fff5ed4 | ||
|
|
2f2cfad722 | ||
|
|
380c16c8dd | ||
|
|
bbf1900677 | ||
|
|
fcc5736d61 | ||
|
|
ae6333bf7c | ||
|
|
3a959208a8 | ||
|
|
b3b7cfa77f | ||
|
|
6d71a28584 | ||
|
|
488fcc7cc5 | ||
|
|
d750389c67 | ||
|
|
cb7efd8601 | ||
|
|
55f66f161e | ||
|
|
067a7d148f | ||
|
|
cf88570c39 | ||
|
|
0e6a175bf6 | ||
|
|
bb680ef20a | ||
|
|
c6505a6647 | ||
|
|
4e7d1c7088 | ||
|
|
0b9cebc685 | ||
|
|
d0b9e3a732 | ||
|
|
b7635feff0 | ||
|
|
7528cabf5a | ||
|
|
39eb37d5e5 | ||
|
|
dbd2e609d7 | ||
|
|
236e669332 | ||
|
|
e142939929 | ||
|
|
98157350b6 | ||
|
|
317eec2790 | ||
|
|
7a1893f864 | ||
|
|
c7125266f6 | ||
|
|
69271c9d59 | ||
|
|
717f0978d9 | ||
|
|
abf517de28 | ||
|
|
7a4314032a | ||
|
|
791c21f643 | ||
|
|
eb5975a400 | ||
|
|
400a80c07d | ||
|
|
ecd603db8c | ||
|
|
95358c204b | ||
|
|
9fc7187e24 | ||
|
|
2d77e71085 | ||
|
|
6da71661d5 | ||
|
|
58da51f767 | ||
|
|
947ba4940b | ||
|
|
e07ee05ee7 | ||
|
|
7a2412b1be | ||
|
|
391b85da41 | ||
|
|
e412958dcc | ||
|
|
488393007f | ||
|
|
6228314e3c | ||
|
|
ba19aab8dc | ||
|
|
3ae430bdd8 | ||
|
|
faa7180536 | ||
|
|
a1519ba737 | ||
|
|
4c226d7a17 | ||
|
|
82951093b5 | ||
|
|
2e15cad048 | ||
|
|
27e997fe0d | ||
|
|
6a4cfc8d7c | ||
|
|
ebac0b9da2 | ||
|
|
e3c5cd063b | ||
|
|
2b73116284 | ||
|
|
d2ccb10972 | ||
|
|
6ede9f8cc3 | ||
|
|
6b07c874fc | ||
|
|
e84dd27e88 | ||
|
|
5f1f797281 | ||
|
|
52fe09d0b1 | ||
|
|
e687cee608 | ||
|
|
8396ff068d | ||
|
|
d98fc1238e | ||
|
|
0ddf84638f | ||
|
|
0b9407f0a6 | ||
|
|
e4d71d858d | ||
|
|
25741e8c4c | ||
|
|
32d8dc311b | ||
|
|
6ff6fd7f75 | ||
|
|
41b73fe2ae | ||
|
|
fb3b00de41 | ||
|
|
0f9b91a15f | ||
|
|
79f3e1b04b | ||
|
|
56022ab7b1 | ||
|
|
4e8b371fb7 | ||
|
|
a2d6d6002c | ||
|
|
dabcf4f7db | ||
|
|
bd5ba7b5d0 | ||
|
|
1d279428a7 | ||
|
|
8ee0c0cf27 | ||
|
|
2a18c9f215 | ||
|
|
974378c9b5 | ||
|
|
eb23818f83 | ||
|
|
8f4d6e7e27 | ||
|
|
5c7f6aab66 | ||
|
|
3cf36b0e93 | ||
|
|
7a9436dad7 | ||
|
|
5c59c53e91 | ||
|
|
e3a995d515 | ||
|
|
87b486b798 | ||
|
|
92c18843b2 | ||
|
|
450c167461 | ||
|
|
bdcb003a32 | ||
|
|
c40931b31c | ||
|
|
db46dc553f | ||
|
|
76bcdfa2b8 | ||
|
|
140ac5d17c | ||
|
|
2fe965942a | ||
|
|
dc574af734 | ||
|
|
1bcbfb8213 | ||
|
|
6bec4cdecc | ||
|
|
04c1c7d8fb | ||
|
|
2f91315ac7 | ||
|
|
a4b17d2548 | ||
|
|
26953d0b15 | ||
|
|
13d1fc63ff | ||
|
|
a4926e5237 | ||
|
|
936a71ee00 | ||
|
|
4096bb562d | ||
|
|
57ed6ae6a6 | ||
|
|
ad5a17ac34 | ||
|
|
436da01bce | ||
|
|
ecce501cf3 | ||
|
|
2c032f1739 | ||
|
|
fffc7b364e | ||
|
|
0b5b8971b1 | ||
|
|
be09c5e346 | ||
|
|
d089dfbca0 |
@@ -10,6 +10,7 @@ globals:
|
|||||||
extends:
|
extends:
|
||||||
- 'eslint:recommended'
|
- 'eslint:recommended'
|
||||||
- 'plugin:storybook/recommended'
|
- 'plugin:storybook/recommended'
|
||||||
|
- 'plugin:import/typescript'
|
||||||
- prettier
|
- prettier
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
@@ -23,12 +24,13 @@ parserOptions:
|
|||||||
modules: true
|
modules: true
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
no-console: warn
|
no-console: error
|
||||||
no-alert: error
|
no-alert: error
|
||||||
no-control-regex: 'off'
|
no-control-regex: 'off'
|
||||||
no-empty: warn
|
no-empty: warn
|
||||||
no-empty-function: warn
|
no-empty-function: warn
|
||||||
no-useless-escape: 'off'
|
no-useless-escape: 'off'
|
||||||
|
import/named: error
|
||||||
import/order:
|
import/order:
|
||||||
[
|
[
|
||||||
'error',
|
'error',
|
||||||
@@ -43,6 +45,12 @@ rules:
|
|||||||
pathGroupsExcludedImportTypes: ['internal'],
|
pathGroupsExcludedImportTypes: ['internal'],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
no-restricted-imports:
|
||||||
|
- error
|
||||||
|
- patterns:
|
||||||
|
- group:
|
||||||
|
- '@/react/test-utils/*'
|
||||||
|
message: 'These utils are just for test files'
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
'import/resolver':
|
'import/resolver':
|
||||||
@@ -51,6 +59,8 @@ settings:
|
|||||||
- ['@@', './app/react/components']
|
- ['@@', './app/react/components']
|
||||||
- ['@', './app']
|
- ['@', './app']
|
||||||
extensions: ['.js', '.ts', '.tsx']
|
extensions: ['.js', '.ts', '.tsx']
|
||||||
|
typescript: true
|
||||||
|
node: true
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
- files:
|
- files:
|
||||||
@@ -75,7 +85,9 @@ overrides:
|
|||||||
settings:
|
settings:
|
||||||
react:
|
react:
|
||||||
version: 'detect'
|
version: 'detect'
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
|
no-console: error
|
||||||
import/order:
|
import/order:
|
||||||
[
|
[
|
||||||
'error',
|
'error',
|
||||||
@@ -108,6 +120,12 @@ overrides:
|
|||||||
'no-await-in-loop': 'off'
|
'no-await-in-loop': 'off'
|
||||||
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
|
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
|
||||||
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
|
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
|
||||||
|
'@typescript-eslint/no-restricted-imports':
|
||||||
|
- error
|
||||||
|
- patterns:
|
||||||
|
- group:
|
||||||
|
- '@/react/test-utils/*'
|
||||||
|
message: 'These utils are just for test files'
|
||||||
overrides: # allow props spreading for hoc files
|
overrides: # allow props spreading for hoc files
|
||||||
- files:
|
- files:
|
||||||
- app/**/with*.ts{,x}
|
- app/**/with*.ts{,x}
|
||||||
@@ -116,13 +134,18 @@ overrides:
|
|||||||
- files:
|
- files:
|
||||||
- app/**/*.test.*
|
- app/**/*.test.*
|
||||||
extends:
|
extends:
|
||||||
- 'plugin:jest/recommended'
|
- 'plugin:vitest/recommended'
|
||||||
- 'plugin:jest/style'
|
|
||||||
env:
|
env:
|
||||||
'jest/globals': true
|
'vitest/env': true
|
||||||
rules:
|
rules:
|
||||||
'react/jsx-no-constructed-context-values': off
|
'react/jsx-no-constructed-context-values': off
|
||||||
|
'@typescript-eslint/no-restricted-imports': off
|
||||||
|
no-restricted-imports: off
|
||||||
|
'react/jsx-props-no-spreading': off
|
||||||
- files:
|
- files:
|
||||||
- app/**/*.stories.*
|
- app/**/*.stories.*
|
||||||
rules:
|
rules:
|
||||||
'no-alert': off
|
'no-alert': off
|
||||||
|
'@typescript-eslint/no-restricted-imports': off
|
||||||
|
no-restricted-imports: off
|
||||||
|
'react/jsx-props-no-spreading': off
|
||||||
|
|||||||
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -93,6 +93,17 @@ body:
|
|||||||
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
|
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
|
||||||
multiple: false
|
multiple: false
|
||||||
options:
|
options:
|
||||||
|
- '2.22.0'
|
||||||
|
- '2.21.2'
|
||||||
|
- '2.21.1'
|
||||||
|
- '2.21.0'
|
||||||
|
- '2.20.3'
|
||||||
|
- '2.20.2'
|
||||||
|
- '2.20.1'
|
||||||
|
- '2.20.0'
|
||||||
|
- '2.19.5'
|
||||||
|
- '2.19.4'
|
||||||
|
- '2.19.3'
|
||||||
- '2.19.2'
|
- '2.19.2'
|
||||||
- '2.19.1'
|
- '2.19.1'
|
||||||
- '2.19.0'
|
- '2.19.0'
|
||||||
|
|||||||
132
.github/workflows/ci.yaml
vendored
132
.github/workflows/ci.yaml
vendored
@@ -5,7 +5,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'develop'
|
- 'develop'
|
||||||
- '!release/*'
|
- 'release/*'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- 'develop'
|
- 'develop'
|
||||||
@@ -13,11 +13,15 @@ on:
|
|||||||
- 'feat/*'
|
- 'feat/*'
|
||||||
- 'fix/*'
|
- 'fix/*'
|
||||||
- 'refactor/*'
|
- 'refactor/*'
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOCKER_HUB_REPO: portainerci/portainer
|
DOCKER_HUB_REPO: portainerci/portainer-ce
|
||||||
NODE_ENV: testing
|
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
|
||||||
GO_VERSION: 1.21.3
|
|
||||||
NODE_VERSION: 18.x
|
NODE_VERSION: 18.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -25,85 +29,71 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
config:
|
config:
|
||||||
- { platform: linux, arch: amd64 }
|
- { platform: linux, arch: amd64, version: "" }
|
||||||
- { platform: linux, arch: arm64 }
|
- { platform: linux, arch: arm64, version: "" }
|
||||||
|
- { platform: linux, arch: arm, version: "" }
|
||||||
|
- { platform: linux, arch: ppc64le, version: "" }
|
||||||
- { platform: windows, arch: amd64, version: 1809 }
|
- { platform: windows, arch: amd64, version: 1809 }
|
||||||
- { platform: windows, arch: amd64, version: ltsc2022 }
|
- { platform: windows, arch: amd64, version: ltsc2022 }
|
||||||
runs-on: arc-runner-set
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
steps:
|
steps:
|
||||||
- name: '[preparation] checkout the current branch'
|
- name: '[preparation] checkout the current branch'
|
||||||
uses: actions/checkout@v3.5.3
|
uses: actions/checkout@v4.1.1
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.branch }}
|
ref: ${{ github.event.inputs.branch }}
|
||||||
- name: '[preparation] set up golang'
|
- name: '[preparation] set up golang'
|
||||||
uses: actions/setup-go@v4.0.1
|
uses: actions/setup-go@v5.0.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version-file: go.mod
|
||||||
cache: false
|
|
||||||
- name: '[preparation] cache paths'
|
|
||||||
id: cache-dir-path
|
|
||||||
run: |
|
|
||||||
echo "yarn-cache-dir=$(yarn cache dir)" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "go-build-dir=$(go env GOCACHE)" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "go-mod-dir=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: '[preparation] cache go'
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ steps.cache-dir-path.outputs.go-build-dir }}
|
|
||||||
${{ steps.cache-dir-path.outputs.go-mod-dir }}
|
|
||||||
key: ${{ matrix.config.platform }}-${{ matrix.config.arch }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ matrix.config.platform }}-${{ matrix.config.arch }}-go-
|
|
||||||
enableCrossOsArchive: true
|
|
||||||
- name: '[preparation] set up node.js'
|
- name: '[preparation] set up node.js'
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4.0.1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: ''
|
cache: 'yarn'
|
||||||
- name: '[preparation] cache yarn'
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
**/node_modules
|
|
||||||
${{ steps.cache-dir-path.outputs.yarn-cache-dir }}
|
|
||||||
key: ${{ matrix.config.platform }}-${{ matrix.config.arch }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ matrix.config.platform }}-${{ matrix.config.arch }}-yarn-
|
|
||||||
enableCrossOsArchive: true
|
|
||||||
- name: '[preparation] set up qemu'
|
- name: '[preparation] set up qemu'
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3.0.0
|
||||||
- name: '[preparation] set up docker context for buildx'
|
- name: '[preparation] set up docker context for buildx'
|
||||||
run: docker context create builders
|
run: docker context create builders
|
||||||
- name: '[preparation] set up docker buildx'
|
- name: '[preparation] set up docker buildx'
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
endpoint: builders
|
endpoint: builders
|
||||||
- name: '[preparation] docker login'
|
- name: '[preparation] docker login'
|
||||||
uses: docker/login-action@v2.2.0
|
uses: docker/login-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||||
- name: '[preparation] set the container image tag'
|
- name: '[preparation] set the container image tag'
|
||||||
run: |
|
run: |
|
||||||
if [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||||
|
# use the release branch name as the tag for release branches
|
||||||
|
# for instance, release/2.19 becomes 2.19
|
||||||
|
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
|
||||||
|
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
||||||
|
# use pr${{ github.event.number }} as the tag for pull requests
|
||||||
|
# for instance, pr123
|
||||||
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
|
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
|
||||||
else
|
else
|
||||||
|
# replace / with - in the branch name
|
||||||
|
# for instance, feature/1.0.0 -> feature-1.0.0
|
||||||
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
|
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${{ matrix.config.platform }}" == "windows" ]; then
|
echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}" >> $GITHUB_ENV
|
||||||
CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}"
|
|
||||||
else
|
|
||||||
CONTAINER_IMAGE_TAG="${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}-${{ matrix.config.arch }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}" >> $GITHUB_ENV
|
|
||||||
- name: '[execution] build linux & windows portainer binaries'
|
- name: '[execution] build linux & windows portainer binaries'
|
||||||
run: |
|
run: |
|
||||||
export YARN_VERSION=$(yarn --version)
|
export YARN_VERSION=$(yarn --version)
|
||||||
export WEBPACK_VERSION=$(yarn list webpack --depth=0 | grep webpack | awk -F@ '{print $2}')
|
export WEBPACK_VERSION=$(yarn list webpack --depth=0 | grep webpack | awk -F@ '{print $2}')
|
||||||
export BUILDNUMBER=${GITHUB_RUN_NUMBER}
|
export BUILDNUMBER=${GITHUB_RUN_NUMBER}
|
||||||
|
GIT_COMMIT_HASH_LONG=${{ github.sha }}
|
||||||
|
export GIT_COMMIT_HASH_SHORT={GIT_COMMIT_HASH_LONG:0:7}
|
||||||
|
|
||||||
|
NODE_ENV="testing"
|
||||||
|
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||||
|
NODE_ENV="production"
|
||||||
|
fi
|
||||||
|
|
||||||
make build-all PLATFORM=${{ matrix.config.platform }} ARCH=${{ matrix.config.arch }} ENV=${NODE_ENV}
|
make build-all PLATFORM=${{ matrix.config.platform }} ARCH=${{ matrix.config.arch }} ENV=${NODE_ENV}
|
||||||
env:
|
env:
|
||||||
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
|
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
|
||||||
@@ -111,38 +101,66 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [ "${{ matrix.config.platform }}" == "windows" ]; then
|
if [ "${{ matrix.config.platform }}" == "windows" ]; then
|
||||||
mv dist/portainer dist/portainer.exe
|
mv dist/portainer dist/portainer.exe
|
||||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
||||||
else
|
else
|
||||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
||||||
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
|
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
|
||||||
|
|
||||||
|
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||||
|
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
|
||||||
|
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
|
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
|
||||||
build_manifests:
|
build_manifests:
|
||||||
runs-on: arc-runner-set
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
needs: [build_images]
|
needs: [build_images]
|
||||||
steps:
|
steps:
|
||||||
- name: '[preparation] docker login'
|
- name: '[preparation] docker login'
|
||||||
uses: docker/login-action@v2.2.0
|
uses: docker/login-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||||
- name: '[preparation] set up docker context for buildx'
|
- name: '[preparation] set up docker context for buildx'
|
||||||
run: docker version && docker context create builders
|
run: docker version && docker context create builders
|
||||||
- name: '[preparation] set up docker buildx'
|
- name: '[preparation] set up docker buildx'
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
endpoint: builders
|
endpoint: builders
|
||||||
- name: '[execution] build and push manifests'
|
- name: '[execution] build and push manifests'
|
||||||
run: |
|
run: |
|
||||||
if [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||||
|
# use the release branch name as the tag for release branches
|
||||||
|
# for instance, release/2.19 becomes 2.19
|
||||||
|
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
|
||||||
|
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
|
||||||
|
# use pr${{ github.event.number }} as the tag for pull requests
|
||||||
|
# for instance, pr123
|
||||||
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
|
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
|
||||||
else
|
else
|
||||||
|
# replace / with - in the branch name
|
||||||
|
# for instance, feature/1.0.0 -> feature-1.0.0
|
||||||
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
|
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
|
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
|
||||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
|
||||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
|
||||||
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
|
||||||
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
|
||||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
|
||||||
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
|
||||||
|
|
||||||
|
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
|
||||||
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
|
||||||
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
|
||||||
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine" \
|
||||||
|
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le-alpine"
|
||||||
|
|
||||||
|
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
|
||||||
|
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
|
||||||
|
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
|
||||||
|
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64"
|
||||||
|
fi
|
||||||
|
|||||||
14
.github/workflows/lint.yml
vendored
14
.github/workflows/lint.yml
vendored
@@ -11,20 +11,27 @@ on:
|
|||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
- release/*
|
- release/*
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.21.3
|
GO_VERSION: 1.22.5
|
||||||
|
NODE_VERSION: 18.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-linters:
|
run-linters:
|
||||||
name: Run linters
|
name: Run linters
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
@@ -44,6 +51,5 @@ jobs:
|
|||||||
- name: GolangCI-Lint
|
- name: GolangCI-Lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
with:
|
with:
|
||||||
version: v1.54.1
|
version: v1.59.1
|
||||||
working-directory: api
|
|
||||||
args: --timeout=10m -c .golangci.yaml
|
args: --timeout=10m -c .golangci.yaml
|
||||||
|
|||||||
8
.github/workflows/nightly-security-scan.yml
vendored
8
.github/workflows/nightly-security-scan.yml
vendored
@@ -6,7 +6,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.21.3
|
GO_VERSION: 1.22.5
|
||||||
|
DOCKER_HUB_REPO: portainerci/portainer-ce
|
||||||
|
DOCKER_HUB_IMAGE_TAG: develop
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
client-dependencies:
|
client-dependencies:
|
||||||
@@ -112,7 +114,7 @@ jobs:
|
|||||||
uses: docker://docker.io/aquasec/trivy:latest
|
uses: docker://docker.io/aquasec/trivy:latest
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop
|
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress ${{ env.DOCKER_HUB_REPO }}:${{ env.DOCKER_HUB_IMAGE_TAG }}
|
||||||
|
|
||||||
- name: upload Trivy image security scan result as artifact
|
- name: upload Trivy image security scan result as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -141,7 +143,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
command: cves
|
command: cves
|
||||||
image: portainerci/portainer:develop
|
image: ${{ env.DOCKER_HUB_REPO }}:${{ env.DOCKER_HUB_IMAGE_TAG }}
|
||||||
sarif-file: image-docker-scout.json
|
sarif-file: image-docker-scout.json
|
||||||
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
|
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||||
|
|||||||
14
.github/workflows/pr-security.yml
vendored
14
.github/workflows/pr-security.yml
vendored
@@ -14,7 +14,7 @@ on:
|
|||||||
- '.github/workflows/pr-security.yml'
|
- '.github/workflows/pr-security.yml'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.21.3
|
GO_VERSION: 1.22.5
|
||||||
NODE_VERSION: 18.x
|
NODE_VERSION: 18.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -23,7 +23,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request &&
|
github.event.pull_request &&
|
||||||
github.event.review.body == '/scan'
|
github.event.review.body == '/scan' &&
|
||||||
|
github.event.pull_request.draft == false
|
||||||
outputs:
|
outputs:
|
||||||
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
||||||
steps:
|
steps:
|
||||||
@@ -77,7 +78,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request &&
|
github.event.pull_request &&
|
||||||
github.event.review.body == '/scan'
|
github.event.review.body == '/scan' &&
|
||||||
|
github.event.pull_request.draft == false
|
||||||
outputs:
|
outputs:
|
||||||
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
||||||
steps:
|
steps:
|
||||||
@@ -139,7 +141,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request &&
|
github.event.pull_request &&
|
||||||
github.event.review.body == '/scan'
|
github.event.review.body == '/scan' &&
|
||||||
|
github.event.pull_request.draft == false
|
||||||
outputs:
|
outputs:
|
||||||
imagediff-trivy: ${{ steps.set-diff-trivy-matrix.outputs.image_diff_trivy_result }}
|
imagediff-trivy: ${{ steps.set-diff-trivy-matrix.outputs.image_diff_trivy_result }}
|
||||||
imagediff-docker-scout: ${{ steps.set-diff-docker-scout-matrix.outputs.image_diff_docker_scout_result }}
|
imagediff-docker-scout: ${{ steps.set-diff-docker-scout-matrix.outputs.image_diff_docker_scout_result }}
|
||||||
@@ -268,7 +271,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request &&
|
github.event.pull_request &&
|
||||||
github.event.review.body == '/scan'
|
github.event.review.body == '/scan' &&
|
||||||
|
github.event.pull_request.draft == false
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
|
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
|
||||||
|
|||||||
56
.github/workflows/test.yaml
vendored
56
.github/workflows/test.yaml
vendored
@@ -1,25 +1,49 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: push
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.21.3
|
GO_VERSION: 1.22.5
|
||||||
NODE_VERSION: 18.x
|
NODE_VERSION: 18.x
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- release/*
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- release/*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-client:
|
test-client:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: 'checkout the current branch'
|
||||||
- uses: actions/setup-node@v2
|
uses: actions/checkout@v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.branch }}
|
||||||
|
|
||||||
|
- name: 'set up node.js'
|
||||||
|
uses: actions/setup-node@v4.0.1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test-client ARGS="--maxWorkers=2"
|
run: make test-client ARGS="--maxWorkers=2 --minWorkers=1"
|
||||||
|
|
||||||
test-server:
|
test-server:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -29,10 +53,24 @@ jobs:
|
|||||||
- { platform: windows, arch: amd64, version: 1809 }
|
- { platform: windows, arch: amd64, version: 1809 }
|
||||||
- { platform: windows, arch: amd64, version: ltsc2022 }
|
- { platform: windows, arch: amd64, version: ltsc2022 }
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- name: 'checkout the current branch'
|
||||||
- uses: actions/setup-go@v3
|
uses: actions/checkout@v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.branch }}
|
||||||
|
|
||||||
|
- name: 'set up golang'
|
||||||
|
uses: actions/setup-go@v5.0.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Run tests
|
|
||||||
|
- name: 'install dependencies'
|
||||||
|
run: make test-deps PLATFORM=linux ARCH=amd64
|
||||||
|
|
||||||
|
- name: 'update $PATH'
|
||||||
|
run: echo "$(pwd)/dist" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: 'run tests'
|
||||||
run: make test-server
|
run: make test-server
|
||||||
|
|||||||
8
.github/workflows/validate-openapi-spec.yaml
vendored
8
.github/workflows/validate-openapi-spec.yaml
vendored
@@ -6,14 +6,20 @@ on:
|
|||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
- 'release/*'
|
- 'release/*'
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.21.3
|
GO_VERSION: 1.22.5
|
||||||
NODE_VERSION: 18.x
|
NODE_VERSION: 18.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
openapi-spec:
|
openapi-spec:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ linters:
|
|||||||
|
|
||||||
# Enable these for now
|
# Enable these for now
|
||||||
enable:
|
enable:
|
||||||
|
- unused
|
||||||
- depguard
|
- depguard
|
||||||
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- errorlint
|
- errorlint
|
||||||
- exportloopref
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
depguard:
|
depguard:
|
||||||
rules:
|
rules:
|
||||||
@@ -3,6 +3,7 @@ import { StorybookConfig } from '@storybook/react-webpack5';
|
|||||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
||||||
import { Configuration } from 'webpack';
|
import { Configuration } from 'webpack';
|
||||||
import postcss from 'postcss';
|
import postcss from 'postcss';
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: ['../app/**/*.stories.@(ts|tsx)'],
|
stories: ['../app/**/*.stories.@(ts|tsx)'],
|
||||||
addons: [
|
addons: [
|
||||||
@@ -87,9 +88,6 @@ const config: StorybookConfig = {
|
|||||||
name: '@storybook/react-webpack5',
|
name: '@storybook/react-webpack5',
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
docs: {
|
|
||||||
autodocs: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import '../app/assets/css';
|
import '../app/assets/css';
|
||||||
|
import React from 'react';
|
||||||
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
|
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
|
||||||
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
|
import { initialize as initMSW, mswLoader } from 'msw-storybook-addon';
|
||||||
import { handlers } from '@/setup-tests/server-handlers';
|
import { handlers } from '../app/setup-tests/server-handlers';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
// Initialize MSW
|
initMSW(
|
||||||
initMSW({
|
{
|
||||||
onUnhandledRequest: ({ method, url }) => {
|
onUnhandledRequest: ({ method, url }) => {
|
||||||
if (url.pathname.startsWith('/api')) {
|
if (url.startsWith('/api')) {
|
||||||
console.error(`Unhandled ${method} request to ${url}.
|
console.error(`Unhandled ${method} request to ${url}.
|
||||||
|
|
||||||
This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories.
|
This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories.
|
||||||
@@ -17,7 +17,9 @@ initMSW({
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
handlers
|
||||||
|
);
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||||
@@ -44,5 +46,6 @@ export const decorators = [
|
|||||||
</UIRouter>
|
</UIRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
),
|
),
|
||||||
mswDecorator,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const loaders = [mswLoader];
|
||||||
@@ -2,22 +2,22 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock Service Worker (0.36.3).
|
* Mock Service Worker (2.0.11).
|
||||||
* @see https://github.com/mswjs/msw
|
* @see https://github.com/mswjs/msw
|
||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
* - Please do NOT serve this file on production.
|
* - Please do NOT serve this file on production.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929';
|
const INTEGRITY_CHECKSUM = 'c5f7f8e188b673ea4e677df7ea3c5a39';
|
||||||
const bypassHeaderName = 'x-msw-bypass';
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse');
|
||||||
const activeClientIds = new Set();
|
const activeClientIds = new Set();
|
||||||
|
|
||||||
self.addEventListener('install', function () {
|
self.addEventListener('install', function () {
|
||||||
return self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', async function (event) {
|
self.addEventListener('activate', function (event) {
|
||||||
return self.clients.claim();
|
event.waitUntil(self.clients.claim());
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('message', async function (event) {
|
self.addEventListener('message', async function (event) {
|
||||||
@@ -33,7 +33,9 @@ self.addEventListener('message', async function (event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allClients = await self.clients.matchAll();
|
const allClients = await self.clients.matchAll({
|
||||||
|
type: 'window',
|
||||||
|
});
|
||||||
|
|
||||||
switch (event.data) {
|
switch (event.data) {
|
||||||
case 'KEEPALIVE_REQUEST': {
|
case 'KEEPALIVE_REQUEST': {
|
||||||
@@ -83,165 +85,8 @@ self.addEventListener('message', async function (event) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resolve the "main" client for the given event.
|
|
||||||
// Client that issues a request doesn't necessarily equal the client
|
|
||||||
// that registered the worker. It's with the latter the worker should
|
|
||||||
// communicate with during the response resolving phase.
|
|
||||||
async function resolveMainClient(event) {
|
|
||||||
const client = await self.clients.get(event.clientId);
|
|
||||||
|
|
||||||
if (client.frameType === 'top-level') {
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allClients = await self.clients.matchAll();
|
|
||||||
|
|
||||||
return allClients
|
|
||||||
.filter((client) => {
|
|
||||||
// Get only those clients that are currently visible.
|
|
||||||
return client.visibilityState === 'visible';
|
|
||||||
})
|
|
||||||
.find((client) => {
|
|
||||||
// Find the client ID that's recorded in the
|
|
||||||
// set of clients that have registered the worker.
|
|
||||||
return activeClientIds.has(client.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRequest(event, requestId) {
|
|
||||||
const client = await resolveMainClient(event);
|
|
||||||
const response = await getResponse(event, client, requestId);
|
|
||||||
|
|
||||||
// Send back the response clone for the "response:*" life-cycle events.
|
|
||||||
// Ensure MSW is active and ready to handle the message, otherwise
|
|
||||||
// this message will pend indefinitely.
|
|
||||||
if (client && activeClientIds.has(client.id)) {
|
|
||||||
(async function () {
|
|
||||||
const clonedResponse = response.clone();
|
|
||||||
sendToClient(client, {
|
|
||||||
type: 'RESPONSE',
|
|
||||||
payload: {
|
|
||||||
requestId,
|
|
||||||
type: clonedResponse.type,
|
|
||||||
ok: clonedResponse.ok,
|
|
||||||
status: clonedResponse.status,
|
|
||||||
statusText: clonedResponse.statusText,
|
|
||||||
body: clonedResponse.body === null ? null : await clonedResponse.text(),
|
|
||||||
headers: serializeHeaders(clonedResponse.headers),
|
|
||||||
redirected: clonedResponse.redirected,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getResponse(event, client, requestId) {
|
|
||||||
const { request } = event;
|
|
||||||
const requestClone = request.clone();
|
|
||||||
const getOriginalResponse = () => fetch(requestClone);
|
|
||||||
|
|
||||||
// Bypass mocking when the request client is not active.
|
|
||||||
if (!client) {
|
|
||||||
return getOriginalResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bypass initial page load requests (i.e. static assets).
|
|
||||||
// The absence of the immediate/parent client in the map of the active clients
|
|
||||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
|
||||||
// and is not ready to handle requests.
|
|
||||||
if (!activeClientIds.has(client.id)) {
|
|
||||||
return await getOriginalResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bypass requests with the explicit bypass header
|
|
||||||
if (requestClone.headers.get(bypassHeaderName) === 'true') {
|
|
||||||
const cleanRequestHeaders = serializeHeaders(requestClone.headers);
|
|
||||||
|
|
||||||
// Remove the bypass header to comply with the CORS preflight check.
|
|
||||||
delete cleanRequestHeaders[bypassHeaderName];
|
|
||||||
|
|
||||||
const originalRequest = new Request(requestClone, {
|
|
||||||
headers: new Headers(cleanRequestHeaders),
|
|
||||||
});
|
|
||||||
|
|
||||||
return fetch(originalRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the request to the client-side MSW.
|
|
||||||
const reqHeaders = serializeHeaders(request.headers);
|
|
||||||
const body = await request.text();
|
|
||||||
|
|
||||||
const clientMessage = await sendToClient(client, {
|
|
||||||
type: 'REQUEST',
|
|
||||||
payload: {
|
|
||||||
id: requestId,
|
|
||||||
url: request.url,
|
|
||||||
method: request.method,
|
|
||||||
headers: reqHeaders,
|
|
||||||
cache: request.cache,
|
|
||||||
mode: request.mode,
|
|
||||||
credentials: request.credentials,
|
|
||||||
destination: request.destination,
|
|
||||||
integrity: request.integrity,
|
|
||||||
redirect: request.redirect,
|
|
||||||
referrer: request.referrer,
|
|
||||||
referrerPolicy: request.referrerPolicy,
|
|
||||||
body,
|
|
||||||
bodyUsed: request.bodyUsed,
|
|
||||||
keepalive: request.keepalive,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (clientMessage.type) {
|
|
||||||
case 'MOCK_SUCCESS': {
|
|
||||||
return delayPromise(() => respondWithMock(clientMessage), clientMessage.payload.delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'MOCK_NOT_FOUND': {
|
|
||||||
return getOriginalResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'NETWORK_ERROR': {
|
|
||||||
const { name, message } = clientMessage.payload;
|
|
||||||
const networkError = new Error(message);
|
|
||||||
networkError.name = name;
|
|
||||||
|
|
||||||
// Rejecting a request Promise emulates a network error.
|
|
||||||
throw networkError;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'INTERNAL_ERROR': {
|
|
||||||
const parsedBody = JSON.parse(clientMessage.payload.body);
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
`\
|
|
||||||
[MSW] Uncaught exception in the request handler for "%s %s":
|
|
||||||
|
|
||||||
${parsedBody.location}
|
|
||||||
|
|
||||||
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
|
|
||||||
`,
|
|
||||||
request.method,
|
|
||||||
request.url
|
|
||||||
);
|
|
||||||
|
|
||||||
return respondWithMock(clientMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return getOriginalResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.addEventListener('fetch', function (event) {
|
self.addEventListener('fetch', function (event) {
|
||||||
const { request } = event;
|
const { request } = event;
|
||||||
const accept = request.headers.get('accept') || '';
|
|
||||||
|
|
||||||
// Bypass server-sent events.
|
|
||||||
if (accept.includes('text/event-stream')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bypass navigation requests.
|
// Bypass navigation requests.
|
||||||
if (request.mode === 'navigate') {
|
if (request.mode === 'navigate') {
|
||||||
@@ -261,36 +106,149 @@ self.addEventListener('fetch', function (event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = uuidv4();
|
// Generate unique request ID.
|
||||||
|
const requestId = crypto.randomUUID();
|
||||||
return event.respondWith(
|
event.respondWith(handleRequest(event, requestId));
|
||||||
handleRequest(event, requestId).catch((error) => {
|
|
||||||
if (error.name === 'NetworkError') {
|
|
||||||
console.warn('[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, request.url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, any exception indicates an issue with the original request/response.
|
|
||||||
console.error(
|
|
||||||
`\
|
|
||||||
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
|
|
||||||
request.method,
|
|
||||||
request.url,
|
|
||||||
`${error.name}: ${error.message}`
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function serializeHeaders(headers) {
|
async function handleRequest(event, requestId) {
|
||||||
const reqHeaders = {};
|
const client = await resolveMainClient(event);
|
||||||
headers.forEach((value, name) => {
|
const response = await getResponse(event, client, requestId);
|
||||||
reqHeaders[name] = reqHeaders[name] ? [].concat(reqHeaders[name]).concat(value) : value;
|
|
||||||
});
|
// Send back the response clone for the "response:*" life-cycle events.
|
||||||
return reqHeaders;
|
// Ensure MSW is active and ready to handle the message, otherwise
|
||||||
|
// this message will pend indefinitely.
|
||||||
|
if (client && activeClientIds.has(client.id)) {
|
||||||
|
(async function () {
|
||||||
|
const responseClone = response.clone();
|
||||||
|
|
||||||
|
sendToClient(
|
||||||
|
client,
|
||||||
|
{
|
||||||
|
type: 'RESPONSE',
|
||||||
|
payload: {
|
||||||
|
requestId,
|
||||||
|
isMockedResponse: IS_MOCKED_RESPONSE in response,
|
||||||
|
type: responseClone.type,
|
||||||
|
status: responseClone.status,
|
||||||
|
statusText: responseClone.statusText,
|
||||||
|
body: responseClone.body,
|
||||||
|
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[responseClone.body]
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendToClient(client, message) {
|
// Resolve the main client for the given event.
|
||||||
|
// Client that issues a request doesn't necessarily equal the client
|
||||||
|
// that registered the worker. It's with the latter the worker should
|
||||||
|
// communicate with during the response resolving phase.
|
||||||
|
async function resolveMainClient(event) {
|
||||||
|
const client = await self.clients.get(event.clientId);
|
||||||
|
|
||||||
|
if (client?.frameType === 'top-level') {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allClients = await self.clients.matchAll({
|
||||||
|
type: 'window',
|
||||||
|
});
|
||||||
|
|
||||||
|
return allClients
|
||||||
|
.filter((client) => {
|
||||||
|
// Get only those clients that are currently visible.
|
||||||
|
return client.visibilityState === 'visible';
|
||||||
|
})
|
||||||
|
.find((client) => {
|
||||||
|
// Find the client ID that's recorded in the
|
||||||
|
// set of clients that have registered the worker.
|
||||||
|
return activeClientIds.has(client.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getResponse(event, client, requestId) {
|
||||||
|
const { request } = event;
|
||||||
|
|
||||||
|
// Clone the request because it might've been already used
|
||||||
|
// (i.e. its body has been read and sent to the client).
|
||||||
|
const requestClone = request.clone();
|
||||||
|
|
||||||
|
function passthrough() {
|
||||||
|
const headers = Object.fromEntries(requestClone.headers.entries());
|
||||||
|
|
||||||
|
// Remove internal MSW request header so the passthrough request
|
||||||
|
// complies with any potential CORS preflight checks on the server.
|
||||||
|
// Some servers forbid unknown request headers.
|
||||||
|
delete headers['x-msw-intention'];
|
||||||
|
|
||||||
|
return fetch(requestClone, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass mocking when the client is not active.
|
||||||
|
if (!client) {
|
||||||
|
return passthrough();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass initial page load requests (i.e. static assets).
|
||||||
|
// The absence of the immediate/parent client in the map of the active clients
|
||||||
|
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||||
|
// and is not ready to handle requests.
|
||||||
|
if (!activeClientIds.has(client.id)) {
|
||||||
|
return passthrough();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass requests with the explicit bypass header.
|
||||||
|
// Such requests can be issued by "ctx.fetch()".
|
||||||
|
const mswIntention = request.headers.get('x-msw-intention');
|
||||||
|
if (['bypass', 'passthrough'].includes(mswIntention)) {
|
||||||
|
return passthrough();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the client that a request has been intercepted.
|
||||||
|
const requestBuffer = await request.arrayBuffer();
|
||||||
|
const clientMessage = await sendToClient(
|
||||||
|
client,
|
||||||
|
{
|
||||||
|
type: 'REQUEST',
|
||||||
|
payload: {
|
||||||
|
id: requestId,
|
||||||
|
url: request.url,
|
||||||
|
mode: request.mode,
|
||||||
|
method: request.method,
|
||||||
|
headers: Object.fromEntries(request.headers.entries()),
|
||||||
|
cache: request.cache,
|
||||||
|
credentials: request.credentials,
|
||||||
|
destination: request.destination,
|
||||||
|
integrity: request.integrity,
|
||||||
|
redirect: request.redirect,
|
||||||
|
referrer: request.referrer,
|
||||||
|
referrerPolicy: request.referrerPolicy,
|
||||||
|
body: requestBuffer,
|
||||||
|
keepalive: request.keepalive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[requestBuffer]
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (clientMessage.type) {
|
||||||
|
case 'MOCK_RESPONSE': {
|
||||||
|
return respondWithMock(clientMessage.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOCK_NOT_FOUND': {
|
||||||
|
return passthrough();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return passthrough();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToClient(client, message, transferrables = []) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const channel = new MessageChannel();
|
const channel = new MessageChannel();
|
||||||
|
|
||||||
@@ -302,27 +260,25 @@ function sendToClient(client, message) {
|
|||||||
resolve(event.data);
|
resolve(event.data);
|
||||||
};
|
};
|
||||||
|
|
||||||
client.postMessage(JSON.stringify(message), [channel.port2]);
|
client.postMessage(message, [channel.port2].concat(transferrables.filter(Boolean)));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function delayPromise(cb, duration) {
|
async function respondWithMock(response) {
|
||||||
return new Promise((resolve) => {
|
// Setting response status code to 0 is a no-op.
|
||||||
setTimeout(() => resolve(cb()), duration);
|
// However, when responding with a "Response.error()", the produced Response
|
||||||
});
|
// instance will have status code set to 0. Since it's not possible to create
|
||||||
}
|
// a Response instance with status code 0, handle that use-case separately.
|
||||||
|
if (response.status === 0) {
|
||||||
|
return Response.error();
|
||||||
|
}
|
||||||
|
|
||||||
function respondWithMock(clientMessage) {
|
const mockedResponse = new Response(response.body, response);
|
||||||
return new Response(clientMessage.payload.body, {
|
|
||||||
...clientMessage.payload,
|
|
||||||
headers: clientMessage.payload.headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function uuidv4() {
|
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
value: true,
|
||||||
const r = (Math.random() * 16) | 0;
|
enumerable: true,
|
||||||
const v = c == 'x' ? r : (r & 0x3) | 0x8;
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mockedResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
19
Makefile
19
Makefile
@@ -7,9 +7,9 @@ ARCH=$(shell go env GOARCH)
|
|||||||
# build target, can be one of "production", "testing", "development"
|
# build target, can be one of "production", "testing", "development"
|
||||||
ENV=development
|
ENV=development
|
||||||
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
||||||
TAG=latest
|
TAG=local
|
||||||
|
|
||||||
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.8.11
|
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
|
||||||
GOTESTSUM=go run gotest.tools/gotestsum@latest
|
GOTESTSUM=go run gotest.tools/gotestsum@latest
|
||||||
|
|
||||||
# Don't change anything below this line unless you know what you're doing
|
# Don't change anything below this line unless you know what you're doing
|
||||||
@@ -30,7 +30,7 @@ build-server: init-dist ## Build the server binary
|
|||||||
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
|
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
|
||||||
|
|
||||||
build-image: build-all ## Build the Portainer image locally
|
build-image: build-all ## Build the Portainer image locally
|
||||||
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
|
docker buildx build --load -t portainerci/portainer-ce:$(TAG) -f build/linux/Dockerfile .
|
||||||
|
|
||||||
build-storybook: ## Build and serve the storybook files
|
build-storybook: ## Build and serve the storybook files
|
||||||
yarn storybook:build
|
yarn storybook:build
|
||||||
@@ -64,11 +64,14 @@ clean: ## Remove all build and download artifacts
|
|||||||
.PHONY: test test-client test-server
|
.PHONY: test test-client test-server
|
||||||
test: test-server test-client ## Run all tests
|
test: test-server test-client ## Run all tests
|
||||||
|
|
||||||
|
test-deps: init-dist
|
||||||
|
./build/download_docker_compose_binary.sh $(PLATFORM) $(ARCH) $(shell jq -r '.dockerCompose' < "./binary-version.json")
|
||||||
|
|
||||||
test-client: ## Run client tests
|
test-client: ## Run client tests
|
||||||
yarn test $(ARGS)
|
yarn test $(ARGS)
|
||||||
|
|
||||||
test-server: ## Run server tests
|
test-server: ## Run server tests
|
||||||
cd api && $(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
|
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
|
||||||
|
|
||||||
##@ Dev
|
##@ Dev
|
||||||
.PHONY: dev dev-client dev-server
|
.PHONY: dev dev-client dev-server
|
||||||
@@ -82,6 +85,8 @@ dev-client: ## Run the client in development mode
|
|||||||
dev-server: build-server ## Run the server in development mode
|
dev-server: build-server ## Run the server in development mode
|
||||||
@./dev/run_container.sh
|
@./dev/run_container.sh
|
||||||
|
|
||||||
|
dev-server-podman: build-server ## Run the server in development mode
|
||||||
|
@./dev/run_container_podman.sh
|
||||||
|
|
||||||
##@ Format
|
##@ Format
|
||||||
.PHONY: format format-client format-server
|
.PHONY: format format-client format-server
|
||||||
@@ -92,7 +97,7 @@ format-client: ## Format client code
|
|||||||
yarn format
|
yarn format
|
||||||
|
|
||||||
format-server: ## Format server code
|
format-server: ## Format server code
|
||||||
cd api && go fmt ./...
|
go fmt ./...
|
||||||
|
|
||||||
##@ Lint
|
##@ Lint
|
||||||
.PHONY: lint lint-client lint-server
|
.PHONY: lint lint-client lint-server
|
||||||
@@ -102,7 +107,7 @@ lint-client: ## Lint client code
|
|||||||
yarn lint
|
yarn lint
|
||||||
|
|
||||||
lint-server: ## Lint server code
|
lint-server: ## Lint server code
|
||||||
cd api && go vet ./...
|
golangci-lint run --timeout=10m -c .golangci.yaml
|
||||||
|
|
||||||
|
|
||||||
##@ Extension
|
##@ Extension
|
||||||
@@ -114,7 +119,7 @@ dev-extension: build-server build-client ## Run the extension in development mod
|
|||||||
##@ Docs
|
##@ Docs
|
||||||
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
|
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
|
||||||
docs-build: init-dist ## Build docs
|
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 ./
|
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
|
||||||
|
|
||||||
docs-validate: docs-build ## Validate docs
|
docs-validate: docs-build ## Validate docs
|
||||||
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
|
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/internal/url"
|
"github.com/portainer/portainer/api/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAgentVersionAndPlatform returns the agent version and platform
|
// GetAgentVersionAndPlatform returns the agent version and platform
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import (
|
|||||||
|
|
||||||
// APIKeyService represents a service for managing API keys.
|
// APIKeyService represents a service for managing API keys.
|
||||||
type APIKeyService interface {
|
type APIKeyService interface {
|
||||||
HashRaw(rawKey string) []byte
|
HashRaw(rawKey string) string
|
||||||
GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error)
|
GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error)
|
||||||
GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error)
|
GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error)
|
||||||
GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error)
|
GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error)
|
||||||
GetDigestUserAndKey(digest []byte) (portainer.User, portainer.APIKey, error)
|
GetDigestUserAndKey(digest string) (portainer.User, portainer.APIKey, error)
|
||||||
UpdateAPIKey(apiKey *portainer.APIKey) error
|
UpdateAPIKey(apiKey *portainer.APIKey) error
|
||||||
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
|
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
|
||||||
InvalidateUserKeyCache(userId portainer.UserID) bool
|
InvalidateUserKeyCache(userId portainer.UserID) bool
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package apikey
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/internal/securecookie"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,17 +33,19 @@ func Test_generateRandomKey(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := securecookie.GenerateRandomKey(tt.wantLenth)
|
got := GenerateRandomKey(tt.wantLenth)
|
||||||
is.Equal(tt.wantLenth, len(got))
|
is.Equal(tt.wantLenth, len(got))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Generated keys are unique", func(t *testing.T) {
|
t.Run("Generated keys are unique", func(t *testing.T) {
|
||||||
keys := make(map[string]bool)
|
keys := make(map[string]bool)
|
||||||
for i := 0; i < 100; i++ {
|
|
||||||
key := securecookie.GenerateRandomKey(8)
|
for range 100 {
|
||||||
|
key := GenerateRandomKey(8)
|
||||||
_, ok := keys[string(key)]
|
_, ok := keys[string(key)]
|
||||||
is.False(ok)
|
is.False(ok)
|
||||||
|
|
||||||
keys[string(key)] = true
|
keys[string(key)] = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,69 +1,79 @@
|
|||||||
package apikey
|
package apikey
|
||||||
|
|
||||||
import (
|
import (
|
||||||
lru "github.com/hashicorp/golang-lru"
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
lru "github.com/hashicorp/golang-lru"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultAPIKeyCacheSize = 1024
|
const DefaultAPIKeyCacheSize = 1024
|
||||||
|
|
||||||
// entry is a tuple containing the user and API key associated to an API key digest
|
// entry is a tuple containing the user and API key associated to an API key digest
|
||||||
type entry struct {
|
type entry[T any] struct {
|
||||||
user portainer.User
|
user T
|
||||||
apiKey portainer.APIKey
|
apiKey portainer.APIKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// apiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
|
type UserCompareFn[T any] func(T, portainer.UserID) bool
|
||||||
|
|
||||||
|
// ApiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
|
||||||
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
|
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
|
||||||
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
|
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
|
||||||
// digest value must be mapped to a portainer user (and respective key data) for validation.
|
// digest value must be mapped to a portainer user (and respective key data) for validation.
|
||||||
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
|
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
|
||||||
type apiKeyCache struct {
|
type ApiKeyCache[T any] struct {
|
||||||
// cache type [string]entry cache (key: string(digest), value: user/key entry)
|
// cache type [string]entry cache (key: string(digest), value: user/key entry)
|
||||||
// note: []byte keys are not supported by golang-lru Cache
|
// note: []byte keys are not supported by golang-lru Cache
|
||||||
cache *lru.Cache
|
cache *lru.Cache
|
||||||
|
userCmpFn UserCompareFn[T]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAPIKeyCache creates a new cache for API keys
|
// NewAPIKeyCache creates a new cache for API keys
|
||||||
func NewAPIKeyCache(cacheSize int) *apiKeyCache {
|
func NewAPIKeyCache[T any](cacheSize int, userCompareFn UserCompareFn[T]) *ApiKeyCache[T] {
|
||||||
cache, _ := lru.New(cacheSize)
|
cache, _ := lru.New(cacheSize)
|
||||||
return &apiKeyCache{cache: cache}
|
|
||||||
|
return &ApiKeyCache[T]{cache: cache, userCmpFn: userCompareFn}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the user/key associated to an api-key's digest
|
// Get returns the user/key associated to an api-key's digest
|
||||||
// This is required because HTTP requests will contain the digest of the API key in header,
|
// This is required because HTTP requests will contain the digest of the API key in header,
|
||||||
// the digest value must be mapped to a portainer user.
|
// the digest value must be mapped to a portainer user.
|
||||||
func (c *apiKeyCache) Get(digest []byte) (portainer.User, portainer.APIKey, bool) {
|
func (c *ApiKeyCache[T]) Get(digest string) (T, portainer.APIKey, bool) {
|
||||||
val, ok := c.cache.Get(string(digest))
|
val, ok := c.cache.Get(digest)
|
||||||
if !ok {
|
if !ok {
|
||||||
return portainer.User{}, portainer.APIKey{}, false
|
var t T
|
||||||
|
|
||||||
|
return t, portainer.APIKey{}, false
|
||||||
}
|
}
|
||||||
tuple := val.(entry)
|
|
||||||
|
tuple := val.(entry[T])
|
||||||
|
|
||||||
return tuple.user, tuple.apiKey, true
|
return tuple.user, tuple.apiKey, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set persists a user/key entry to the cache
|
// Set persists a user/key entry to the cache
|
||||||
func (c *apiKeyCache) Set(digest []byte, user portainer.User, apiKey portainer.APIKey) {
|
func (c *ApiKeyCache[T]) Set(digest string, user T, apiKey portainer.APIKey) {
|
||||||
c.cache.Add(string(digest), entry{
|
c.cache.Add(digest, entry[T]{
|
||||||
user: user,
|
user: user,
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete evicts a digest's user/key entry key from the cache
|
// Delete evicts a digest's user/key entry key from the cache
|
||||||
func (c *apiKeyCache) Delete(digest []byte) {
|
func (c *ApiKeyCache[T]) Delete(digest string) {
|
||||||
c.cache.Remove(string(digest))
|
c.cache.Remove(digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
|
// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
|
||||||
func (c *apiKeyCache) InvalidateUserKeyCache(userId portainer.UserID) bool {
|
func (c *ApiKeyCache[T]) InvalidateUserKeyCache(userId portainer.UserID) bool {
|
||||||
present := false
|
present := false
|
||||||
|
|
||||||
for _, k := range c.cache.Keys() {
|
for _, k := range c.cache.Keys() {
|
||||||
user, _, _ := c.Get([]byte(k.(string)))
|
user, _, _ := c.Get(k.(string))
|
||||||
if user.ID == userId {
|
if c.userCmpFn(user, userId) {
|
||||||
present = c.cache.Remove(k)
|
present = c.cache.Remove(k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return present
|
return present
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,32 +10,32 @@ import (
|
|||||||
func Test_apiKeyCacheGet(t *testing.T) {
|
func Test_apiKeyCacheGet(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
keyCache := NewAPIKeyCache(10)
|
keyCache := NewAPIKeyCache(10, compareUser)
|
||||||
|
|
||||||
// pre-populate cache
|
// pre-populate cache
|
||||||
keyCache.cache.Add(string("foo"), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
|
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{}, apiKey: portainer.APIKey{}})
|
||||||
keyCache.cache.Add(string(""), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
|
keyCache.cache.Add(string(""), entry[portainer.User]{user: portainer.User{}, apiKey: portainer.APIKey{}})
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
digest []byte
|
digest string
|
||||||
found bool
|
found bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
digest: []byte("foo"),
|
digest: "foo",
|
||||||
found: true,
|
found: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
digest: []byte(""),
|
digest: "",
|
||||||
found: true,
|
found: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
digest: []byte("bar"),
|
digest: "bar",
|
||||||
found: false,
|
found: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(string(test.digest), func(t *testing.T) {
|
t.Run(test.digest, func(t *testing.T) {
|
||||||
_, _, found := keyCache.Get(test.digest)
|
_, _, found := keyCache.Get(test.digest)
|
||||||
is.Equal(test.found, found)
|
is.Equal(test.found, found)
|
||||||
})
|
})
|
||||||
@@ -45,43 +45,43 @@ func Test_apiKeyCacheGet(t *testing.T) {
|
|||||||
func Test_apiKeyCacheSet(t *testing.T) {
|
func Test_apiKeyCacheSet(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
keyCache := NewAPIKeyCache(10)
|
keyCache := NewAPIKeyCache(10, compareUser)
|
||||||
|
|
||||||
// pre-populate cache
|
// pre-populate cache
|
||||||
keyCache.Set([]byte("bar"), portainer.User{ID: 2}, portainer.APIKey{})
|
keyCache.Set("bar", portainer.User{ID: 2}, portainer.APIKey{})
|
||||||
keyCache.Set([]byte("foo"), portainer.User{ID: 1}, portainer.APIKey{})
|
keyCache.Set("foo", portainer.User{ID: 1}, portainer.APIKey{})
|
||||||
|
|
||||||
// overwrite existing entry
|
// overwrite existing entry
|
||||||
keyCache.Set([]byte("foo"), portainer.User{ID: 3}, portainer.APIKey{})
|
keyCache.Set("foo", portainer.User{ID: 3}, portainer.APIKey{})
|
||||||
|
|
||||||
val, ok := keyCache.cache.Get(string("bar"))
|
val, ok := keyCache.cache.Get(string("bar"))
|
||||||
is.True(ok)
|
is.True(ok)
|
||||||
|
|
||||||
tuple := val.(entry)
|
tuple := val.(entry[portainer.User])
|
||||||
is.Equal(portainer.User{ID: 2}, tuple.user)
|
is.Equal(portainer.User{ID: 2}, tuple.user)
|
||||||
|
|
||||||
val, ok = keyCache.cache.Get(string("foo"))
|
val, ok = keyCache.cache.Get(string("foo"))
|
||||||
is.True(ok)
|
is.True(ok)
|
||||||
|
|
||||||
tuple = val.(entry)
|
tuple = val.(entry[portainer.User])
|
||||||
is.Equal(portainer.User{ID: 3}, tuple.user)
|
is.Equal(portainer.User{ID: 3}, tuple.user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_apiKeyCacheDelete(t *testing.T) {
|
func Test_apiKeyCacheDelete(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
keyCache := NewAPIKeyCache(10)
|
keyCache := NewAPIKeyCache(10, compareUser)
|
||||||
|
|
||||||
t.Run("Delete an existing entry", func(t *testing.T) {
|
t.Run("Delete an existing entry", func(t *testing.T) {
|
||||||
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
|
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
|
||||||
keyCache.Delete([]byte("foo"))
|
keyCache.Delete("foo")
|
||||||
|
|
||||||
_, ok := keyCache.cache.Get(string("foo"))
|
_, ok := keyCache.cache.Get(string("foo"))
|
||||||
is.False(ok)
|
is.False(ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Delete a non-existing entry", func(t *testing.T) {
|
t.Run("Delete a non-existing entry", func(t *testing.T) {
|
||||||
nonPanicFunc := func() { keyCache.Delete([]byte("non-existent-key")) }
|
nonPanicFunc := func() { keyCache.Delete("non-existent-key") }
|
||||||
is.NotPanics(nonPanicFunc)
|
is.NotPanics(nonPanicFunc)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -128,19 +128,19 @@ func Test_apiKeyCacheLRU(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
keyCache := NewAPIKeyCache(test.cacheLen)
|
keyCache := NewAPIKeyCache(test.cacheLen, compareUser)
|
||||||
|
|
||||||
for _, key := range test.key {
|
for _, key := range test.key {
|
||||||
keyCache.Set([]byte(key), portainer.User{ID: 1}, portainer.APIKey{})
|
keyCache.Set(key, portainer.User{ID: 1}, portainer.APIKey{})
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range test.foundKeys {
|
for _, key := range test.foundKeys {
|
||||||
_, _, found := keyCache.Get([]byte(key))
|
_, _, found := keyCache.Get(key)
|
||||||
is.True(found, "Key %s not found", key)
|
is.True(found, "Key %s not found", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, key := range test.evictedKeys {
|
for _, key := range test.evictedKeys {
|
||||||
_, _, found := keyCache.Get([]byte(key))
|
_, _, found := keyCache.Get(key)
|
||||||
is.False(found, "key %s should have been evicted", key)
|
is.False(found, "key %s should have been evicted", key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -150,10 +150,10 @@ func Test_apiKeyCacheLRU(t *testing.T) {
|
|||||||
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
|
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
keyCache := NewAPIKeyCache(10)
|
keyCache := NewAPIKeyCache(10, compareUser)
|
||||||
|
|
||||||
t.Run("Removes users keys from cache", func(t *testing.T) {
|
t.Run("Removes users keys from cache", func(t *testing.T) {
|
||||||
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
|
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
|
||||||
|
|
||||||
ok := keyCache.InvalidateUserKeyCache(1)
|
ok := keyCache.InvalidateUserKeyCache(1)
|
||||||
is.True(ok)
|
is.True(ok)
|
||||||
@@ -163,8 +163,8 @@ func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Does not affect other keys", func(t *testing.T) {
|
t.Run("Does not affect other keys", func(t *testing.T) {
|
||||||
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
|
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
|
||||||
keyCache.cache.Add(string("bar"), entry{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
|
keyCache.cache.Add(string("bar"), entry[portainer.User]{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
|
||||||
|
|
||||||
ok := keyCache.InvalidateUserKeyCache(1)
|
ok := keyCache.InvalidateUserKeyCache(1)
|
||||||
is.True(ok)
|
is.True(ok)
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package apikey
|
package apikey
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/internal/securecookie"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
@@ -20,30 +21,45 @@ var ErrInvalidAPIKey = errors.New("Invalid API key")
|
|||||||
type apiKeyService struct {
|
type apiKeyService struct {
|
||||||
apiKeyRepository dataservices.APIKeyRepository
|
apiKeyRepository dataservices.APIKeyRepository
|
||||||
userRepository dataservices.UserService
|
userRepository dataservices.UserService
|
||||||
cache *apiKeyCache
|
cache *ApiKeyCache[portainer.User]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRandomKey generates a random key of specified length
|
||||||
|
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
|
||||||
|
func GenerateRandomKey(length int) []byte {
|
||||||
|
k := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareUser(u portainer.User, id portainer.UserID) bool {
|
||||||
|
return u.ID == id
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIKeyService(apiKeyRepository dataservices.APIKeyRepository, userRepository dataservices.UserService) *apiKeyService {
|
func NewAPIKeyService(apiKeyRepository dataservices.APIKeyRepository, userRepository dataservices.UserService) *apiKeyService {
|
||||||
return &apiKeyService{
|
return &apiKeyService{
|
||||||
apiKeyRepository: apiKeyRepository,
|
apiKeyRepository: apiKeyRepository,
|
||||||
userRepository: userRepository,
|
userRepository: userRepository,
|
||||||
cache: NewAPIKeyCache(defaultAPIKeyCacheSize),
|
cache: NewAPIKeyCache(DefaultAPIKeyCacheSize, compareUser),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashRaw computes a hash digest of provided raw API key.
|
// HashRaw computes a hash digest of provided raw API key.
|
||||||
func (a *apiKeyService) HashRaw(rawKey string) []byte {
|
func (a *apiKeyService) HashRaw(rawKey string) string {
|
||||||
hashDigest := sha256.Sum256([]byte(rawKey))
|
hashDigest := sha256.Sum256([]byte(rawKey))
|
||||||
return hashDigest[:]
|
|
||||||
|
return base64.StdEncoding.EncodeToString(hashDigest[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateApiKey generates a raw API key for a user (for one-time display).
|
// GenerateApiKey generates a raw API key for a user (for one-time display).
|
||||||
// The generated API key is stored in the cache and database.
|
// The generated API key is stored in the cache and database.
|
||||||
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
|
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
|
||||||
randKey := securecookie.GenerateRandomKey(32)
|
randKey := GenerateRandomKey(32)
|
||||||
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
|
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
|
||||||
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
|
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
|
||||||
|
|
||||||
hashDigest := a.HashRaw(prefixedAPIKey)
|
hashDigest := a.HashRaw(prefixedAPIKey)
|
||||||
|
|
||||||
apiKey := &portainer.APIKey{
|
apiKey := &portainer.APIKey{
|
||||||
@@ -54,8 +70,7 @@ func (a *apiKeyService) GenerateApiKey(user portainer.User, description string)
|
|||||||
Digest: hashDigest,
|
Digest: hashDigest,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.apiKeyRepository.Create(apiKey)
|
if err := a.apiKeyRepository.Create(apiKey); err != nil {
|
||||||
if err != nil {
|
|
||||||
return "", nil, errors.Wrap(err, "Unable to create API key")
|
return "", nil, errors.Wrap(err, "Unable to create API key")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +92,7 @@ func (a *apiKeyService) GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey,
|
|||||||
|
|
||||||
// GetDigestUserAndKey returns the user and api-key associated to a specified hash digest.
|
// GetDigestUserAndKey returns the user and api-key associated to a specified hash digest.
|
||||||
// A cache lookup is performed first; if the user/api-key is not found in the cache, respective database lookups are performed.
|
// A cache lookup is performed first; if the user/api-key is not found in the cache, respective database lookups are performed.
|
||||||
func (a *apiKeyService) GetDigestUserAndKey(digest []byte) (portainer.User, portainer.APIKey, error) {
|
func (a *apiKeyService) GetDigestUserAndKey(digest string) (portainer.User, portainer.APIKey, error) {
|
||||||
// get api key from cache if possible
|
|
||||||
cachedUser, cachedKey, ok := a.cache.Get(digest)
|
cachedUser, cachedKey, ok := a.cache.Get(digest)
|
||||||
if ok {
|
if ok {
|
||||||
return cachedUser, cachedKey, nil
|
return cachedUser, cachedKey, nil
|
||||||
@@ -106,20 +120,21 @@ func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Unable to retrieve API key")
|
return errors.Wrap(err, "Unable to retrieve API key")
|
||||||
}
|
}
|
||||||
|
|
||||||
a.cache.Set(apiKey.Digest, user, *apiKey)
|
a.cache.Set(apiKey.Digest, user, *apiKey)
|
||||||
|
|
||||||
return a.apiKeyRepository.Update(apiKey.ID, apiKey)
|
return a.apiKeyRepository.Update(apiKey.ID, apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
|
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
|
||||||
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
|
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
|
||||||
// get api-key digest to remove from cache
|
|
||||||
apiKey, err := a.apiKeyRepository.Read(apiKeyID)
|
apiKey, err := a.apiKeyRepository.Read(apiKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
|
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the user/api-key from cache
|
|
||||||
a.cache.Delete(apiKey.Digest)
|
a.cache.Delete(apiKey.Digest)
|
||||||
|
|
||||||
return a.apiKeyRepository.Delete(apiKeyID)
|
return a.apiKeyRepository.Delete(apiKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package apikey
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -68,7 +69,7 @@ func Test_GenerateApiKey(t *testing.T) {
|
|||||||
|
|
||||||
generatedDigest := sha256.Sum256([]byte(rawKey))
|
generatedDigest := sha256.Sum256([]byte(rawKey))
|
||||||
|
|
||||||
is.Equal(apiKey.Digest, generatedDigest[:])
|
is.Equal(apiKey.Digest, base64.StdEncoding.EncodeToString(generatedDigest[:]))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,18 +48,6 @@ func TarGzDir(absolutePath string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addToArchive(tarWriter *tar.Writer, pathInArchive string, path string, info os.FileInfo) error {
|
func addToArchive(tarWriter *tar.Writer, pathInArchive string, path string, info os.FileInfo) error {
|
||||||
header, err := tar.FileInfoHeader(info, info.Name())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
header.Name = pathInArchive // use relative paths in archive
|
|
||||||
|
|
||||||
err = tarWriter.WriteHeader(header)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -68,6 +56,26 @@ func addToArchive(tarWriter *tar.Writer, pathInArchive string, path string, info
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := tar.FileInfoHeader(stat, stat.Name())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
header.Name = pathInArchive // use relative paths in archive
|
||||||
|
|
||||||
|
err = tarWriter.WriteHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stat.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
_, err = io.Copy(tarWriter, file)
|
_, err = io.Copy(tarWriter, file)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -98,7 +106,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
|
|||||||
// skip, dir will be created with a file
|
// skip, dir will be created with a file
|
||||||
case tar.TypeReg:
|
case tar.TypeReg:
|
||||||
p := filepath.Clean(filepath.Join(outputDirPath, header.Name))
|
p := filepath.Clean(filepath.Join(outputDirPath, header.Name))
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0744); err != nil {
|
if err := os.MkdirAll(filepath.Dir(p), 0o744); err != nil {
|
||||||
return fmt.Errorf("Failed to extract dir %s", filepath.Dir(p))
|
return fmt.Errorf("Failed to extract dir %s", filepath.Dir(p))
|
||||||
}
|
}
|
||||||
outFile, err := os.Create(p)
|
outFile, err := os.Create(p)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const rwxr__r__ os.FileMode = 0744
|
const rwxr__r__ os.FileMode = 0o744
|
||||||
|
|
||||||
var filesToBackup = []string{
|
var filesToBackup = []string{
|
||||||
"certs",
|
"certs",
|
||||||
@@ -82,14 +82,9 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
|
|||||||
}
|
}
|
||||||
|
|
||||||
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
|
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
|
||||||
backupWriter, err := os.Create(filepath.Join(backupDirPath, "portainer.db"))
|
dbFileName := datastore.Connection().GetDatabaseFileName()
|
||||||
if err != nil {
|
_, err := datastore.Backup(filepath.Join(backupDirPath, dbFileName))
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
if err = datastore.BackupTo(backupWriter); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return backupWriter.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func encrypt(path string, passphrase string) (string, error) {
|
func encrypt(path string, passphrase string) (string, error) {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
|
|||||||
if password != "" {
|
if password != "" {
|
||||||
archive, err = decrypt(archive, password)
|
archive, err = decrypt(archive, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to decrypt the archive")
|
return errors.Wrap(err, "failed to decrypt the archive. Please ensure the password is correct and try again")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package build
|
package build
|
||||||
|
|
||||||
|
import "runtime"
|
||||||
|
|
||||||
// Variables to be set during the build time
|
// Variables to be set during the build time
|
||||||
var BuildNumber string
|
var BuildNumber string
|
||||||
var ImageTag string
|
var ImageTag string
|
||||||
var NodejsVersion string
|
var NodejsVersion string
|
||||||
var YarnVersion string
|
var YarnVersion string
|
||||||
var WebpackVersion string
|
var WebpackVersion string
|
||||||
var GoVersion string
|
var GoVersion string = runtime.Version()
|
||||||
|
var GitCommit string
|
||||||
|
|||||||
@@ -5,6 +5,17 @@ import (
|
|||||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// EdgeJobs retrieves the edge jobs for the given environment
|
||||||
|
func (service *Service) EdgeJobs(endpointID portainer.EndpointID) []portainer.EdgeJob {
|
||||||
|
service.mu.RLock()
|
||||||
|
defer service.mu.RUnlock()
|
||||||
|
|
||||||
|
return append(
|
||||||
|
make([]portainer.EdgeJob, 0, len(service.edgeJobs[endpointID])),
|
||||||
|
service.edgeJobs[endpointID]...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
|
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
|
||||||
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
|
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
|
||||||
if endpoint.Edge.AsyncMode {
|
if endpoint.Edge.AsyncMode {
|
||||||
@@ -12,10 +23,10 @@ func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portai
|
|||||||
}
|
}
|
||||||
|
|
||||||
service.mu.Lock()
|
service.mu.Lock()
|
||||||
tunnel := service.getTunnelDetails(endpoint.ID)
|
defer service.mu.Unlock()
|
||||||
|
|
||||||
existingJobIndex := -1
|
existingJobIndex := -1
|
||||||
for idx, existingJob := range tunnel.Jobs {
|
for idx, existingJob := range service.edgeJobs[endpoint.ID] {
|
||||||
if existingJob.ID == edgeJob.ID {
|
if existingJob.ID == edgeJob.ID {
|
||||||
existingJobIndex = idx
|
existingJobIndex = idx
|
||||||
|
|
||||||
@@ -24,30 +35,28 @@ func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portai
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existingJobIndex == -1 {
|
if existingJobIndex == -1 {
|
||||||
tunnel.Jobs = append(tunnel.Jobs, *edgeJob)
|
service.edgeJobs[endpoint.ID] = append(service.edgeJobs[endpoint.ID], *edgeJob)
|
||||||
} else {
|
} else {
|
||||||
tunnel.Jobs[existingJobIndex] = *edgeJob
|
service.edgeJobs[endpoint.ID][existingJobIndex] = *edgeJob
|
||||||
}
|
}
|
||||||
|
|
||||||
cache.Del(endpoint.ID)
|
cache.Del(endpoint.ID)
|
||||||
|
|
||||||
service.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
|
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
|
||||||
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
||||||
service.mu.Lock()
|
service.mu.Lock()
|
||||||
|
|
||||||
for endpointID, tunnel := range service.tunnelDetailsMap {
|
for endpointID := range service.edgeJobs {
|
||||||
n := 0
|
n := 0
|
||||||
for _, edgeJob := range tunnel.Jobs {
|
for _, edgeJob := range service.edgeJobs[endpointID] {
|
||||||
if edgeJob.ID != edgeJobID {
|
if edgeJob.ID != edgeJobID {
|
||||||
tunnel.Jobs[n] = edgeJob
|
service.edgeJobs[endpointID][n] = edgeJob
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnel.Jobs = tunnel.Jobs[:n]
|
service.edgeJobs[endpointID] = service.edgeJobs[endpointID][:n]
|
||||||
|
|
||||||
cache.Del(endpointID)
|
cache.Del(endpointID)
|
||||||
}
|
}
|
||||||
@@ -57,19 +66,17 @@ func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
|||||||
|
|
||||||
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
|
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
|
||||||
service.mu.Lock()
|
service.mu.Lock()
|
||||||
tunnel := service.getTunnelDetails(endpointID)
|
defer service.mu.Unlock()
|
||||||
|
|
||||||
n := 0
|
n := 0
|
||||||
for _, edgeJob := range tunnel.Jobs {
|
for _, edgeJob := range service.edgeJobs[endpointID] {
|
||||||
if edgeJob.ID != edgeJobID {
|
if edgeJob.ID != edgeJobID {
|
||||||
tunnel.Jobs[n] = edgeJob
|
service.edgeJobs[endpointID][n] = edgeJob
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tunnel.Jobs = tunnel.Jobs[:n]
|
service.edgeJobs[endpointID] = service.edgeJobs[endpointID][:n]
|
||||||
|
|
||||||
cache.Del(endpointID)
|
cache.Del(endpointID)
|
||||||
|
|
||||||
service.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
tunnelCleanupInterval = 10 * time.Second
|
tunnelCleanupInterval = 10 * time.Second
|
||||||
requiredTimeout = 15 * time.Second
|
|
||||||
activeTimeout = 4*time.Minute + 30*time.Second
|
activeTimeout = 4*time.Minute + 30*time.Second
|
||||||
|
pingTimeout = 3 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service represents a service to manage the state of multiple reverse tunnels.
|
// Service represents a service to manage the state of multiple reverse tunnels.
|
||||||
@@ -29,49 +29,78 @@ const (
|
|||||||
type Service struct {
|
type Service struct {
|
||||||
serverFingerprint string
|
serverFingerprint string
|
||||||
serverPort string
|
serverPort string
|
||||||
tunnelDetailsMap map[portainer.EndpointID]*portainer.TunnelDetails
|
activeTunnels map[portainer.EndpointID]*portainer.TunnelDetails
|
||||||
|
edgeJobs map[portainer.EndpointID][]portainer.EdgeJob
|
||||||
dataStore dataservices.DataStore
|
dataStore dataservices.DataStore
|
||||||
snapshotService portainer.SnapshotService
|
snapshotService portainer.SnapshotService
|
||||||
chiselServer *chserver.Server
|
chiselServer *chserver.Server
|
||||||
shutdownCtx context.Context
|
shutdownCtx context.Context
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
mu sync.Mutex
|
mu sync.RWMutex
|
||||||
fileService portainer.FileService
|
fileService portainer.FileService
|
||||||
|
defaultCheckinInterval int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService returns a pointer to a new instance of Service
|
// NewService returns a pointer to a new instance of Service
|
||||||
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context, fileService portainer.FileService) *Service {
|
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context, fileService portainer.FileService) *Service {
|
||||||
|
defaultCheckinInterval := portainer.DefaultEdgeAgentCheckinIntervalInSeconds
|
||||||
|
|
||||||
|
settings, err := dataStore.Settings().Settings()
|
||||||
|
if err == nil {
|
||||||
|
defaultCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||||
|
} else {
|
||||||
|
log.Error().Err(err).Msg("unable to retrieve the settings from the database")
|
||||||
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
|
activeTunnels: make(map[portainer.EndpointID]*portainer.TunnelDetails),
|
||||||
|
edgeJobs: make(map[portainer.EndpointID][]portainer.EdgeJob),
|
||||||
dataStore: dataStore,
|
dataStore: dataStore,
|
||||||
shutdownCtx: shutdownCtx,
|
shutdownCtx: shutdownCtx,
|
||||||
fileService: fileService,
|
fileService: fileService,
|
||||||
|
defaultCheckinInterval: defaultCheckinInterval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pingAgent ping the given agent so that the agent can keep the tunnel alive
|
// pingAgent ping the given agent so that the agent can keep the tunnel alive
|
||||||
func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
|
func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
|
||||||
tunnel := service.GetTunnelDetails(endpointID)
|
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
|
||||||
requestURL := fmt.Sprintf("http://127.0.0.1:%d/ping", tunnel.Port)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelAddr, err := service.TunnelAddr(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestURL := fmt.Sprintf("http://%s/ping", tunnelAddr)
|
||||||
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
|
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Timeout: 3 * time.Second,
|
Timeout: pingTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := httpClient.Do(req)
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
io.Copy(io.Discard, resp.Body)
|
io.Copy(io.Discard, resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
|
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
|
||||||
func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
|
func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
|
||||||
go func() {
|
go service.keepTunnelAlive(endpointID, ctx, maxAlive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) keepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Int("endpoint_id", int(endpointID)).
|
Int("endpoint_id", int(endpointID)).
|
||||||
Float64("max_alive_minutes", maxAlive.Minutes()).
|
Float64("max_alive_minutes", maxAlive.Minutes()).
|
||||||
@@ -86,9 +115,9 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-pingTicker.C:
|
case <-pingTicker.C:
|
||||||
service.SetTunnelStatusToActive(endpointID)
|
service.UpdateLastActivity(endpointID)
|
||||||
err := service.pingAgent(endpointID)
|
|
||||||
if err != nil {
|
if err := service.pingAgent(endpointID); err != nil {
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Int("endpoint_id", int(endpointID)).
|
Int("endpoint_id", int(endpointID)).
|
||||||
Err(err).
|
Err(err).
|
||||||
@@ -111,7 +140,6 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartTunnelServer starts a tunnel server on the specified addr and port.
|
// StartTunnelServer starts a tunnel server on the specified addr and port.
|
||||||
@@ -121,7 +149,6 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
|||||||
// The snapshotter is used in the tunnel status verification process.
|
// The snapshotter is used in the tunnel status verification process.
|
||||||
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
|
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
|
||||||
privateKeyFile, err := service.retrievePrivateKeyFile()
|
privateKeyFile, err := service.retrievePrivateKeyFile()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -139,21 +166,21 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
|
|||||||
service.serverFingerprint = chiselServer.GetFingerprint()
|
service.serverFingerprint = chiselServer.GetFingerprint()
|
||||||
service.serverPort = port
|
service.serverPort = port
|
||||||
|
|
||||||
err = chiselServer.Start(addr, port)
|
if err := chiselServer.Start(addr, port); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
service.chiselServer = chiselServer
|
service.chiselServer = chiselServer
|
||||||
|
|
||||||
// TODO: work-around Chisel default behavior.
|
// TODO: work-around Chisel default behavior.
|
||||||
// By default, Chisel will allow anyone to connect if no user exists.
|
// By default, Chisel will allow anyone to connect if no user exists.
|
||||||
username, password := generateRandomCredentials()
|
username, password := generateRandomCredentials()
|
||||||
err = service.chiselServer.AddUser(username, password, "127.0.0.1")
|
if err = service.chiselServer.AddUser(username, password, "127.0.0.1"); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
service.snapshotService = snapshotService
|
service.snapshotService = snapshotService
|
||||||
|
|
||||||
go service.startTunnelVerificationLoop()
|
go service.startTunnelVerificationLoop()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -167,36 +194,38 @@ func (service *Service) StopTunnelServer() error {
|
|||||||
func (service *Service) retrievePrivateKeyFile() (string, error) {
|
func (service *Service) retrievePrivateKeyFile() (string, error) {
|
||||||
privateKeyFile := service.fileService.GetDefaultChiselPrivateKeyPath()
|
privateKeyFile := service.fileService.GetDefaultChiselPrivateKeyPath()
|
||||||
|
|
||||||
exist, _ := service.fileService.FileExists(privateKeyFile)
|
if exists, _ := service.fileService.FileExists(privateKeyFile); exists {
|
||||||
if !exist {
|
log.Info().
|
||||||
|
Str("private-key", privateKeyFile).
|
||||||
|
Msg("found Chisel private key file on disk")
|
||||||
|
|
||||||
|
return privateKeyFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Str("private-key", privateKeyFile).
|
Str("private-key", privateKeyFile).
|
||||||
Msg("Chisel private key file does not exist")
|
Msg("chisel private key file does not exist")
|
||||||
|
|
||||||
privateKey, err := ccrypto.GenerateKey("")
|
privateKey, err := ccrypto.GenerateKey("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Failed to generate chisel private key")
|
Msg("failed to generate chisel private key")
|
||||||
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = service.fileService.StoreChiselPrivateKey(privateKey)
|
if err = service.fileService.StoreChiselPrivateKey(privateKey); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Failed to save Chisel private key to disk")
|
Msg("failed to save Chisel private key to disk")
|
||||||
|
|
||||||
return "", err
|
return "", err
|
||||||
} else {
|
}
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("private-key", privateKeyFile).
|
Str("private-key", privateKeyFile).
|
||||||
Msg("Generated a new Chisel private key file")
|
Msg("generated a new Chisel private key file")
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Info().
|
|
||||||
Str("private-key", privateKeyFile).
|
|
||||||
Msg("Found Chisel private key file on disk")
|
|
||||||
}
|
|
||||||
|
|
||||||
return privateKeyFile, nil
|
return privateKeyFile, nil
|
||||||
}
|
}
|
||||||
@@ -225,63 +254,45 @@ func (service *Service) startTunnelVerificationLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkTunnels finds the first tunnel that has not had any activity recently
|
||||||
|
// and attempts to take a snapshot, then closes it and returns
|
||||||
func (service *Service) checkTunnels() {
|
func (service *Service) checkTunnels() {
|
||||||
tunnels := make(map[portainer.EndpointID]portainer.TunnelDetails)
|
service.mu.RLock()
|
||||||
|
|
||||||
service.mu.Lock()
|
for endpointID, tunnel := range service.activeTunnels {
|
||||||
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 {
|
|
||||||
elapsed := time.Since(tunnel.LastActivity)
|
elapsed := time.Since(tunnel.LastActivity)
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Int("endpoint_id", int(endpointID)).
|
Int("endpoint_id", int(endpointID)).
|
||||||
Str("status", tunnel.Status).
|
Float64("last_activity_seconds", elapsed.Seconds()).
|
||||||
Float64("status_time_seconds", elapsed.Seconds()).
|
|
||||||
Msg("environment tunnel monitoring")
|
Msg("environment tunnel monitoring")
|
||||||
|
|
||||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
|
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed < activeTimeout {
|
||||||
log.Debug().
|
continue
|
||||||
Int("endpoint_id", int(endpointID)).
|
|
||||||
Str("status", tunnel.Status).
|
|
||||||
Float64("status_time_seconds", elapsed.Seconds()).
|
|
||||||
Float64("timeout_seconds", requiredTimeout.Seconds()).
|
|
||||||
Msg("REQUIRED state timeout exceeded")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
|
tunnelPort := tunnel.Port
|
||||||
|
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Int("endpoint_id", int(endpointID)).
|
Int("endpoint_id", int(endpointID)).
|
||||||
Str("status", tunnel.Status).
|
Float64("last_activity_seconds", elapsed.Seconds()).
|
||||||
Float64("status_time_seconds", elapsed.Seconds()).
|
|
||||||
Float64("timeout_seconds", activeTimeout.Seconds()).
|
Float64("timeout_seconds", activeTimeout.Seconds()).
|
||||||
Msg("ACTIVE state timeout exceeded")
|
Msg("last activity timeout exceeded")
|
||||||
|
|
||||||
err := service.snapshotEnvironment(endpointID, tunnel.Port)
|
if err := service.snapshotEnvironment(endpointID, tunnelPort); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
log.Error().
|
||||||
Int("endpoint_id", int(endpointID)).
|
Int("endpoint_id", int(endpointID)).
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("unable to snapshot Edge environment")
|
Msg("unable to snapshot Edge environment")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service.close(endpointID)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
service.mu.RUnlock()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {
|
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {
|
||||||
|
|||||||
54
api/chisel/service_test.go
Normal file
54
api/chisel/service_test.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPingAgentPanic(t *testing.T) {
|
||||||
|
endpoint := &portainer.Endpoint{
|
||||||
|
ID: 1,
|
||||||
|
EdgeID: "test-edge-id",
|
||||||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||||
|
UserTrusted: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
|
|
||||||
|
s := NewService(store, nil, nil)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
require.Nil(t, recover())
|
||||||
|
}()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
time.Sleep(pingTimeout + 1*time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
|
ln, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
srv := &http.Server{Handler: mux}
|
||||||
|
|
||||||
|
errCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
errCh <- srv.Serve(ln)
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = s.Open(endpoint)
|
||||||
|
require.NoError(t, err)
|
||||||
|
s.activeTunnels[endpoint.ID].Port = ln.Addr().(*net.TCPAddr).Port
|
||||||
|
|
||||||
|
require.Error(t, s.pingAgent(endpoint.ID))
|
||||||
|
require.NoError(t, srv.Shutdown(context.Background()))
|
||||||
|
require.ErrorIs(t, <-errCh, http.ErrServerClosed)
|
||||||
|
}
|
||||||
@@ -5,14 +5,18 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/internal/edge"
|
||||||
"github.com/portainer/portainer/api/internal/edge/cache"
|
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||||
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
"github.com/portainer/portainer/pkg/libcrypto"
|
"github.com/portainer/portainer/pkg/libcrypto"
|
||||||
|
|
||||||
"github.com/dchest/uniuri"
|
"github.com/dchest/uniuri"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,18 +24,191 @@ const (
|
|||||||
maxAvailablePort = 65535
|
maxAvailablePort = 65535
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNonEdgeEnv = errors.New("cannot open a tunnel for non-edge environments")
|
||||||
|
ErrAsyncEnv = errors.New("cannot open a tunnel for async edge environments")
|
||||||
|
ErrInvalidEnv = errors.New("cannot open a tunnel for an invalid environment")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open will mark the tunnel as REQUIRED so the agent opens it
|
||||||
|
func (s *Service) Open(endpoint *portainer.Endpoint) error {
|
||||||
|
if !endpointutils.IsEdgeEndpoint(endpoint) {
|
||||||
|
return ErrNonEdgeEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.Edge.AsyncMode {
|
||||||
|
return ErrAsyncEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.ID == 0 || endpoint.EdgeID == "" || !endpoint.UserTrusted {
|
||||||
|
return ErrInvalidEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := s.activeTunnels[endpoint.ID]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer cache.Del(endpoint.ID)
|
||||||
|
|
||||||
|
tun := &portainer.TunnelDetails{
|
||||||
|
Status: portainer.EdgeAgentManagementRequired,
|
||||||
|
Port: s.getUnusedPort(),
|
||||||
|
LastActivity: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password := generateRandomCredentials()
|
||||||
|
|
||||||
|
if s.chiselServer != nil {
|
||||||
|
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tun.Port)
|
||||||
|
|
||||||
|
if err := s.chiselServer.AddUser(username, password, authorizedRemote); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tun.Credentials = credentials
|
||||||
|
|
||||||
|
s.activeTunnels[endpoint.ID] = tun
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// close removes the tunnel from the map so the agent will close it
|
||||||
|
func (s *Service) close(endpointID portainer.EndpointID) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
tun, ok := s.activeTunnels[endpointID]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tun.Credentials) > 0 && s.chiselServer != nil {
|
||||||
|
user, _, _ := strings.Cut(tun.Credentials, ":")
|
||||||
|
s.chiselServer.DeleteUser(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ProxyManager != nil {
|
||||||
|
s.ProxyManager.DeleteEndpointProxy(endpointID)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(s.activeTunnels, endpointID)
|
||||||
|
|
||||||
|
cache.Del(endpointID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config returns the tunnel details needed for the agent to connect
|
||||||
|
func (s *Service) Config(endpointID portainer.EndpointID) portainer.TunnelDetails {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
if tun, ok := s.activeTunnels[endpointID]; ok {
|
||||||
|
return *tun
|
||||||
|
}
|
||||||
|
|
||||||
|
return portainer.TunnelDetails{Status: portainer.EdgeAgentIdle}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TunnelAddr returns the address of the local tunnel, including the port, it
|
||||||
|
// will block until the tunnel is ready
|
||||||
|
func (s *Service) TunnelAddr(endpoint *portainer.Endpoint) (string, error) {
|
||||||
|
if err := s.Open(endpoint); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tun := s.Config(endpoint.ID)
|
||||||
|
checkinInterval := time.Duration(s.tryEffectiveCheckinInterval(endpoint)) * time.Second
|
||||||
|
|
||||||
|
for t0 := time.Now(); ; {
|
||||||
|
if time.Since(t0) > 2*checkinInterval {
|
||||||
|
s.close(endpoint.ID)
|
||||||
|
|
||||||
|
return "", errors.New("unable to open the tunnel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the tunnel is established
|
||||||
|
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: tun.Port})
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(checkinInterval / 100)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
s.UpdateLastActivity(endpoint.ID)
|
||||||
|
|
||||||
|
return fmt.Sprintf("127.0.0.1:%d", tun.Port), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryEffectiveCheckinInterval avoids a potential deadlock by returning a
|
||||||
|
// previous known value after a timeout
|
||||||
|
func (s *Service) tryEffectiveCheckinInterval(endpoint *portainer.Endpoint) int {
|
||||||
|
ch := make(chan int, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ch <- edge.EffectiveCheckinInterval(s.dataStore, endpoint)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
return s.defaultCheckinInterval
|
||||||
|
case i := <-ch:
|
||||||
|
s.mu.Lock()
|
||||||
|
s.defaultCheckinInterval = i
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLastActivity sets the current timestamp to avoid the tunnel timeout
|
||||||
|
func (s *Service) UpdateLastActivity(endpointID portainer.EndpointID) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if tun, ok := s.activeTunnels[endpointID]; ok {
|
||||||
|
tun.LastActivity = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: it needs to be called with the lock acquired
|
// NOTE: it needs to be called with the lock acquired
|
||||||
// getUnusedPort is used to generate an unused random port in the dynamic port range.
|
// getUnusedPort is used to generate an unused random port in the dynamic port range.
|
||||||
// Dynamic ports (also called private ports) are 49152 to 65535.
|
// Dynamic ports (also called private ports) are 49152 to 65535.
|
||||||
func (service *Service) getUnusedPort() int {
|
func (service *Service) getUnusedPort() int {
|
||||||
port := randomInt(minAvailablePort, maxAvailablePort)
|
port := randomInt(minAvailablePort, maxAvailablePort)
|
||||||
|
|
||||||
for _, tunnel := range service.tunnelDetailsMap {
|
for _, tunnel := range service.activeTunnels {
|
||||||
if tunnel.Port == port {
|
if tunnel.Port == port {
|
||||||
return service.getUnusedPort()
|
return service.getUnusedPort()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port})
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
log.Debug().
|
||||||
|
Int("port", port).
|
||||||
|
Msg("selected port is in use, trying a different one")
|
||||||
|
|
||||||
|
return service.getUnusedPort()
|
||||||
|
}
|
||||||
|
|
||||||
return port
|
return port
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,152 +216,10 @@ func randomInt(min, max int) int {
|
|||||||
return min + rand.Intn(max-min)
|
return min + rand.Intn(max-min)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: it needs to be called with the lock acquired
|
|
||||||
func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
|
|
||||||
|
|
||||||
if tunnel, ok := service.tunnelDetailsMap[endpointID]; ok {
|
|
||||||
return tunnel
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnel := &portainer.TunnelDetails{
|
|
||||||
Status: portainer.EdgeAgentIdle,
|
|
||||||
}
|
|
||||||
|
|
||||||
service.tunnelDetailsMap[endpointID] = tunnel
|
|
||||||
|
|
||||||
cache.Del(endpointID)
|
|
||||||
|
|
||||||
return tunnel
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTunnelDetails returns information about the tunnel associated to an environment(endpoint).
|
|
||||||
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portainer.TunnelDetails {
|
|
||||||
service.mu.Lock()
|
|
||||||
defer service.mu.Unlock()
|
|
||||||
|
|
||||||
return *service.getTunnelDetails(endpointID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// update the LastActivity
|
|
||||||
service.SetTunnelStatusToActive(endpoint.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
|
|
||||||
err := service.SetTunnelStatusToRequired(endpoint.ID)
|
|
||||||
if err != nil {
|
|
||||||
return portainer.TunnelDetails{}, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoint.EdgeCheckinInterval == 0 {
|
|
||||||
settings, err := service.dataStore.Settings().Settings()
|
|
||||||
if err != nil {
|
|
||||||
return portainer.TunnelDetails{}, fmt.Errorf("failed fetching settings from db: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
return service.GetTunnelDetails(endpoint.ID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
|
|
||||||
// It sets the status to ACTIVE.
|
|
||||||
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
|
||||||
service.mu.Lock()
|
|
||||||
tunnel := service.getTunnelDetails(endpointID)
|
|
||||||
tunnel.Status = portainer.EdgeAgentActive
|
|
||||||
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).
|
|
||||||
// It sets the status to IDLE.
|
|
||||||
// It removes any existing credentials associated to the tunnel.
|
|
||||||
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
|
||||||
service.mu.Lock()
|
|
||||||
|
|
||||||
tunnel := service.getTunnelDetails(endpointID)
|
|
||||||
tunnel.Status = portainer.EdgeAgentIdle
|
|
||||||
tunnel.Port = 0
|
|
||||||
tunnel.LastActivity = time.Now()
|
|
||||||
|
|
||||||
credentials := tunnel.Credentials
|
|
||||||
if credentials != "" {
|
|
||||||
tunnel.Credentials = ""
|
|
||||||
|
|
||||||
if service.chiselServer != nil {
|
|
||||||
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
|
||||||
|
|
||||||
service.mu.Unlock()
|
|
||||||
|
|
||||||
cache.Del(endpointID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
|
||||||
// It sets the status to REQUIRED.
|
|
||||||
// If no port is currently associated to the tunnel, it will associate a random unused port to the tunnel
|
|
||||||
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
|
|
||||||
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
|
|
||||||
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
|
||||||
defer cache.Del(endpointID)
|
|
||||||
|
|
||||||
tunnel := service.getTunnelDetails(endpointID)
|
|
||||||
|
|
||||||
service.mu.Lock()
|
|
||||||
defer service.mu.Unlock()
|
|
||||||
|
|
||||||
if tunnel.Port == 0 {
|
|
||||||
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnel.Status = portainer.EdgeAgentManagementRequired
|
|
||||||
tunnel.Port = service.getUnusedPort()
|
|
||||||
tunnel.LastActivity = time.Now()
|
|
||||||
|
|
||||||
username, password := generateRandomCredentials()
|
|
||||||
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
|
|
||||||
|
|
||||||
if service.chiselServer != nil {
|
|
||||||
err = service.chiselServer.AddUser(username, password, authorizedRemote)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tunnel.Credentials = credentials
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateRandomCredentials() (string, string) {
|
func generateRandomCredentials() (string, string) {
|
||||||
username := uniuri.NewLen(8)
|
username := uniuri.NewLen(8)
|
||||||
password := uniuri.NewLen(8)
|
password := uniuri.NewLen(8)
|
||||||
|
|
||||||
return username, password
|
return username, password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,24 +17,20 @@ import (
|
|||||||
type Service struct{}
|
type Service struct{}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
|
ErrInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
|
||||||
errSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
|
ErrSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
|
||||||
errInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
|
ErrInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
|
||||||
errAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
|
ErrAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseFlags parse the CLI flags and return a portainer.Flags struct
|
func CLIFlags() *portainer.CLIFlags {
|
||||||
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
return &portainer.CLIFlags{
|
||||||
kingpin.Version(version)
|
|
||||||
|
|
||||||
flags := &portainer.CLIFlags{
|
|
||||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||||
AddrHTTPS: kingpin.Flag("bind-https", "Address and port to serve Portainer via https").Default(defaultHTTPSBindAddress).String(),
|
AddrHTTPS: kingpin.Flag("bind-https", "Address and port to serve Portainer via https").Default(defaultHTTPSBindAddress).String(),
|
||||||
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
|
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
|
||||||
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
|
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
|
||||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
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(),
|
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
||||||
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
|
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
|
||||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||||
@@ -49,7 +45,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
|
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
|
||||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
|
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
|
||||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
|
||||||
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
|
Rollback: kingpin.Flag("rollback", "Rollback the database to the previous backup").Bool(),
|
||||||
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
|
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
|
||||||
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
|
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
|
||||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||||
@@ -62,8 +58,15 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||||||
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
|
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(),
|
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"),
|
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"),
|
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFlags parse the CLI flags and return a portainer.Flags struct
|
||||||
|
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
|
kingpin.Version(version)
|
||||||
|
|
||||||
|
flags := CLIFlags()
|
||||||
|
|
||||||
kingpin.Parse()
|
kingpin.Parse()
|
||||||
|
|
||||||
@@ -83,18 +86,16 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||||
displayDeprecationWarnings(flags)
|
displayDeprecationWarnings(flags)
|
||||||
|
|
||||||
err := validateEndpointURL(*flags.EndpointURL)
|
if err := validateEndpointURL(*flags.EndpointURL); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = validateSnapshotInterval(*flags.SnapshotInterval)
|
if err := validateSnapshotInterval(*flags.SnapshotInterval); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
|
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
|
||||||
return errAdminPassExcludeAdminPassFile
|
return ErrAdminPassExcludeAdminPassFile
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -116,15 +117,16 @@ func validateEndpointURL(endpointURL string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
|
||||||
return errInvalidEndpointProtocol
|
return ErrInvalidEndpointProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
|
||||||
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
socketPath := strings.TrimPrefix(endpointURL, "unix://")
|
||||||
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
socketPath = strings.TrimPrefix(socketPath, "npipe://")
|
||||||
|
|
||||||
if _, err := os.Stat(socketPath); err != nil {
|
if _, err := os.Stat(socketPath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return errSocketOrNamedPipeNotFound
|
return ErrSocketOrNamedPipeNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
@@ -139,9 +141,8 @@ func validateSnapshotInterval(snapshotInterval string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := time.ParseDuration(snapshotInterval)
|
if _, err := time.ParseDuration(snapshotInterval); err != nil {
|
||||||
if err != nil {
|
return ErrInvalidSnapshotInterval
|
||||||
return errInvalidSnapshotInterval
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// Confirm starts a rollback db cli application
|
// Confirm starts a rollback db cli application
|
||||||
func Confirm(message string) (bool, error) {
|
func Confirm(message string) (bool, error) {
|
||||||
fmt.Printf("%s [y/N]", message)
|
fmt.Printf("%s [y/N] ", message)
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,19 @@ func setLoggingMode(mode string) {
|
|||||||
TimeFormat: "2006/01/02 03:04PM",
|
TimeFormat: "2006/01/02 03:04PM",
|
||||||
FormatMessage: formatMessage,
|
FormatMessage: formatMessage,
|
||||||
})
|
})
|
||||||
|
case "NOCOLOR":
|
||||||
|
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stderr,
|
||||||
|
TimeFormat: "2006/01/02 03:04PM",
|
||||||
|
FormatMessage: formatMessage,
|
||||||
|
NoColor: true,
|
||||||
|
})
|
||||||
case "JSON":
|
case "JSON":
|
||||||
log.Logger = log.Output(os.Stderr)
|
log.Logger = log.Output(os.Stderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatMessage(i interface{}) string {
|
func formatMessage(i any) string {
|
||||||
if i == nil {
|
if i == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/apikey"
|
"github.com/portainer/portainer/api/apikey"
|
||||||
@@ -21,7 +20,7 @@ import (
|
|||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
"github.com/portainer/portainer/api/datastore/migrator"
|
"github.com/portainer/portainer/api/datastore/migrator"
|
||||||
"github.com/portainer/portainer/api/demo"
|
"github.com/portainer/portainer/api/datastore/postinit"
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/docker"
|
||||||
dockerclient "github.com/portainer/portainer/api/docker/client"
|
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||||
"github.com/portainer/portainer/api/exec"
|
"github.com/portainer/portainer/api/exec"
|
||||||
@@ -44,6 +43,9 @@ import (
|
|||||||
"github.com/portainer/portainer/api/ldap"
|
"github.com/portainer/portainer/api/ldap"
|
||||||
"github.com/portainer/portainer/api/oauth"
|
"github.com/portainer/portainer/api/oauth"
|
||||||
"github.com/portainer/portainer/api/pendingactions"
|
"github.com/portainer/portainer/api/pendingactions"
|
||||||
|
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||||
|
"github.com/portainer/portainer/api/pendingactions/handlers"
|
||||||
|
"github.com/portainer/portainer/api/platform"
|
||||||
"github.com/portainer/portainer/api/scheduler"
|
"github.com/portainer/portainer/api/scheduler"
|
||||||
"github.com/portainer/portainer/api/stacks/deployments"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
"github.com/portainer/portainer/pkg/featureflags"
|
"github.com/portainer/portainer/pkg/featureflags"
|
||||||
@@ -56,14 +58,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func initCLI() *portainer.CLIFlags {
|
func initCLI() *portainer.CLIFlags {
|
||||||
var cliService portainer.CLIService = &cli.Service{}
|
cliService := &cli.Service{}
|
||||||
|
|
||||||
flags, err := cliService.ParseFlags(portainer.APIVersion)
|
flags, err := cliService.ParseFlags(portainer.APIVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed parsing flags")
|
log.Fatal().Err(err).Msg("failed parsing flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cliService.ValidateFlags(flags)
|
if err := cliService.ValidateFlags(flags); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed validating flags")
|
log.Fatal().Err(err).Msg("failed validating flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,14 +96,14 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
|||||||
}
|
}
|
||||||
|
|
||||||
store := datastore.NewStore(*flags.Data, fileService, connection)
|
store := datastore.NewStore(*flags.Data, fileService, connection)
|
||||||
|
|
||||||
isNew, err := store.Open()
|
isNew, err := store.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed opening store")
|
log.Fatal().Err(err).Msg("failed opening store")
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.Rollback {
|
if *flags.Rollback {
|
||||||
err := store.Rollback(false)
|
if err := store.Rollback(false); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed rolling back")
|
log.Fatal().Err(err).Msg("failed rolling back")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +112,7 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init sets some defaults - it's basically a migration
|
// Init sets some defaults - it's basically a migration
|
||||||
err = store.Init()
|
if err := store.Init(); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed initializing data store")
|
log.Fatal().Err(err).Msg("failed initializing data store")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,25 +134,23 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
|||||||
}
|
}
|
||||||
store.VersionService.UpdateVersion(&v)
|
store.VersionService.UpdateVersion(&v)
|
||||||
|
|
||||||
err = updateSettingsFromFlags(store, flags)
|
if err := updateSettingsFromFlags(store, flags); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed updating settings from flags")
|
log.Fatal().Err(err).Msg("failed updating settings from flags")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err = store.MigrateData()
|
if err := store.MigrateData(); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed migration")
|
log.Fatal().Err(err).Msg("failed migration")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = updateSettingsFromFlags(store, flags)
|
if err := updateSettingsFromFlags(store, flags); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed updating settings from flags")
|
log.Fatal().Err(err).Msg("failed updating settings from flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is for the db restore functionality - needs more tests.
|
// this is for the db restore functionality - needs more tests.
|
||||||
go func() {
|
go func() {
|
||||||
<-shutdownCtx.Done()
|
<-shutdownCtx.Done()
|
||||||
|
|
||||||
defer connection.Close()
|
defer connection.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -200,41 +199,21 @@ func initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService {
|
|||||||
return apikey.NewAPIKeyService(datastore.APIKeyRepository(), datastore.User())
|
return apikey.NewAPIKeyService(datastore.APIKeyRepository(), datastore.User())
|
||||||
}
|
}
|
||||||
|
|
||||||
func initJWTService(userSessionTimeout string, dataStore dataservices.DataStore) (dataservices.JWTService, error) {
|
func initJWTService(userSessionTimeout string, dataStore dataservices.DataStore) (portainer.JWTService, error) {
|
||||||
if userSessionTimeout == "" {
|
if userSessionTimeout == "" {
|
||||||
userSessionTimeout = portainer.DefaultUserSessionTimeout
|
userSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtService, err := jwt.NewService(userSessionTimeout, dataStore)
|
return jwt.NewService(userSessionTimeout, dataStore)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwtService, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDigitalSignatureService() portainer.DigitalSignatureService {
|
func initDigitalSignatureService() portainer.DigitalSignatureService {
|
||||||
return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
|
return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func initCryptoService() portainer.CryptoService {
|
|
||||||
return &crypto.Service{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initLDAPService() portainer.LDAPService {
|
|
||||||
return &ldap.Service{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initOAuthService() portainer.OAuthService {
|
|
||||||
return oauth.NewService()
|
|
||||||
}
|
|
||||||
|
|
||||||
func initGitService(ctx context.Context) portainer.GitService {
|
|
||||||
return git.NewService(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
|
func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
|
||||||
slices := strings.Split(addr, ":")
|
slices := strings.Split(addr, ":")
|
||||||
|
|
||||||
host := slices[0]
|
host := slices[0]
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = "0.0.0.0"
|
host = "0.0.0.0"
|
||||||
@@ -242,22 +221,13 @@ func initSSLService(addr, certPath, keyPath string, fileService portainer.FileSe
|
|||||||
|
|
||||||
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
|
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
|
||||||
|
|
||||||
err := sslService.Init(host, certPath, keyPath)
|
if err := sslService.Init(host, certPath, keyPath); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return sslService, nil
|
return sslService, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *dockerclient.ClientFactory {
|
|
||||||
return dockerclient.NewClientFactory(signatureService, reverseTunnelService)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
func initSnapshotService(
|
||||||
snapshotIntervalFromFlag string,
|
snapshotIntervalFromFlag string,
|
||||||
dataStore dataservices.DataStore,
|
dataStore dataservices.DataStore,
|
||||||
@@ -290,34 +260,21 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.SnapshotInterval != "" {
|
settings.SnapshotInterval = *cmp.Or(flags.SnapshotInterval, &settings.SnapshotInterval)
|
||||||
settings.SnapshotInterval = *flags.SnapshotInterval
|
settings.LogoURL = *cmp.Or(flags.Logo, &settings.LogoURL)
|
||||||
}
|
settings.EnableEdgeComputeFeatures = *cmp.Or(flags.EnableEdgeComputeFeatures, &settings.EnableEdgeComputeFeatures)
|
||||||
|
settings.TemplatesURL = *cmp.Or(flags.Templates, &settings.TemplatesURL)
|
||||||
if *flags.Logo != "" {
|
|
||||||
settings.LogoURL = *flags.Logo
|
|
||||||
}
|
|
||||||
|
|
||||||
if *flags.EnableEdgeComputeFeatures {
|
|
||||||
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
|
|
||||||
}
|
|
||||||
|
|
||||||
if *flags.Templates != "" {
|
|
||||||
settings.TemplatesURL = *flags.Templates
|
|
||||||
}
|
|
||||||
|
|
||||||
if *flags.Labels != nil {
|
if *flags.Labels != nil {
|
||||||
settings.BlackListedLabels = *flags.Labels
|
settings.BlackListedLabels = *flags.Labels
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings.AgentSecret = ""
|
||||||
if agentKey, ok := os.LookupEnv("AGENT_SECRET"); ok {
|
if agentKey, ok := os.LookupEnv("AGENT_SECRET"); ok {
|
||||||
settings.AgentSecret = agentKey
|
settings.AgentSecret = agentKey
|
||||||
} else {
|
|
||||||
settings.AgentSecret = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataStore.Settings().UpdateSettings(settings)
|
if err := dataStore.Settings().UpdateSettings(settings); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +297,7 @@ func loadAndParseKeyPair(fileService portainer.FileService, signatureService por
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return signatureService.ParseKeyPair(private, public)
|
return signatureService.ParseKeyPair(private, public)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +306,9 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
privateHeader, publicHeader := signatureService.PEMHeaders()
|
privateHeader, publicHeader := signatureService.PEMHeaders()
|
||||||
|
|
||||||
return fileService.StoreKeyPair(private, public, privateHeader, publicHeader)
|
return fileService.StoreKeyPair(private, public, privateHeader, publicHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,6 +321,7 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D
|
|||||||
if existingKeyPair {
|
if existingKeyPair {
|
||||||
return loadAndParseKeyPair(fileService, signatureService)
|
return loadAndParseKeyPair(fileService, signatureService)
|
||||||
}
|
}
|
||||||
|
|
||||||
return generateAndStoreKeyPair(fileService, signatureService)
|
return generateAndStoreKeyPair(fileService, signatureService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +339,7 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
|
|||||||
|
|
||||||
// return a 32 byte hash of the secret (required for AES)
|
// return a 32 byte hash of the secret (required for AES)
|
||||||
hash := sha256.Sum256(content)
|
hash := sha256.Sum256(content)
|
||||||
|
|
||||||
return hash[:]
|
return hash[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,17 +384,17 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
log.Fatal().Err(err).Msg("failed initializing JWT service")
|
log.Fatal().Err(err).Msg("failed initializing JWT service")
|
||||||
}
|
}
|
||||||
|
|
||||||
ldapService := initLDAPService()
|
ldapService := &ldap.Service{}
|
||||||
|
|
||||||
oauthService := initOAuthService()
|
oauthService := oauth.NewService()
|
||||||
|
|
||||||
gitService := initGitService(shutdownCtx)
|
gitService := git.NewService(shutdownCtx)
|
||||||
|
|
||||||
openAMTService := openamt.NewService()
|
openAMTService := openamt.NewService()
|
||||||
|
|
||||||
cryptoService := initCryptoService()
|
cryptoService := &crypto.Service{}
|
||||||
|
|
||||||
digitalSignatureService := initDigitalSignatureService()
|
signatureService := initDigitalSignatureService()
|
||||||
|
|
||||||
edgeStacksService := edgestacks.NewService(dataStore)
|
edgeStacksService := edgestacks.NewService(dataStore)
|
||||||
|
|
||||||
@@ -446,32 +408,27 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
log.Fatal().Err(err).Msg("failed to get SSL settings")
|
log.Fatal().Err(err).Msg("failed to get SSL settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = initKeyPair(fileService, digitalSignatureService)
|
if err := initKeyPair(fileService, signatureService); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed initializing key pair")
|
log.Fatal().Err(err).Msg("failed initializing key pair")
|
||||||
}
|
}
|
||||||
|
|
||||||
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx, fileService)
|
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx, fileService)
|
||||||
|
|
||||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
dockerClientFactory := dockerclient.NewClientFactory(signatureService, reverseTunnelService)
|
||||||
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
|
|
||||||
|
kubernetesClientFactory, err := kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed initializing Kubernetes Client Factory service")
|
||||||
|
}
|
||||||
|
|
||||||
authorizationService := authorization.NewService(dataStore)
|
authorizationService := authorization.NewService(dataStore)
|
||||||
authorizationService.K8sClientFactory = kubernetesClientFactory
|
authorizationService.K8sClientFactory = kubernetesClientFactory
|
||||||
|
|
||||||
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory, authorizationService, shutdownCtx)
|
|
||||||
|
|
||||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed initializing snapshot service")
|
|
||||||
}
|
|
||||||
snapshotService.Start()
|
|
||||||
|
|
||||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||||
|
|
||||||
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
|
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
|
||||||
|
|
||||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
|
proxyManager := proxy.NewManager(kubernetesClientFactory)
|
||||||
|
|
||||||
reverseTunnelService.ProxyManager = proxyManager
|
reverseTunnelService.ProxyManager = proxyManager
|
||||||
|
|
||||||
@@ -484,39 +441,45 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
|
|
||||||
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
|
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
|
||||||
|
|
||||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
|
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
|
||||||
}
|
}
|
||||||
|
|
||||||
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
|
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, *flags.Assets)
|
||||||
|
|
||||||
|
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory)
|
||||||
|
pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore))
|
||||||
|
pendingActionsService.RegisterHandler(actions.DeletePortainerK8sRegistrySecrets, handlers.NewHandlerDeleteRegistrySecrets(authorizationService, dataStore, kubernetesClientFactory))
|
||||||
|
pendingActionsService.RegisterHandler(actions.PostInitMigrateEnvironment, handlers.NewHandlerPostInitMigrateEnvironment(authorizationService, dataStore, kubernetesClientFactory, dockerClientFactory, *flags.Assets, kubernetesDeployer))
|
||||||
|
|
||||||
|
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed initializing snapshot service")
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshotService.Start()
|
||||||
|
|
||||||
|
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
|
||||||
|
|
||||||
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
|
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed initializing helm package manager")
|
log.Fatal().Err(err).Msg("failed initializing helm package manager")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
|
if err := edge.LoadEdgeJobs(dataStore, reverseTunnelService); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
|
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationStatus := initStatus(instanceID)
|
applicationStatus := initStatus(instanceID)
|
||||||
|
|
||||||
demoService := demo.NewService()
|
|
||||||
if *flags.DemoEnvironment {
|
|
||||||
err := demoService.Init(dataStore, cryptoService)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed initializing demo environment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// channel to control when the admin user is created
|
// channel to control when the admin user is created
|
||||||
adminCreationDone := make(chan struct{}, 1)
|
adminCreationDone := make(chan struct{}, 1)
|
||||||
|
|
||||||
go endpointutils.InitEndpoint(shutdownCtx, adminCreationDone, flags, dataStore, snapshotService)
|
go endpointutils.InitEndpoint(shutdownCtx, adminCreationDone, flags, dataStore, snapshotService)
|
||||||
|
|
||||||
adminPasswordHash := ""
|
adminPasswordHash := ""
|
||||||
|
|
||||||
if *flags.AdminPasswordFile != "" {
|
if *flags.AdminPasswordFile != "" {
|
||||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
|
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -539,14 +502,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
|
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
log.Info().Msg("created admin user with the given password.")
|
log.Info().Msg("created admin user with the given password.")
|
||||||
|
|
||||||
user := &portainer.User{
|
user := &portainer.User{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Role: portainer.AdministratorRole,
|
Role: portainer.AdministratorRole,
|
||||||
Password: adminPasswordHash,
|
Password: adminPasswordHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := dataStore.User().Create(user)
|
if err := dataStore.User().Create(user); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed creating admin user")
|
log.Fatal().Err(err).Msg("failed creating admin user")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,8 +520,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
|
if err := reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("failed starting tunnel server")
|
log.Fatal().Err(err).Msg("failed starting tunnel server")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -571,7 +533,20 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
||||||
}
|
}
|
||||||
|
|
||||||
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
|
platformService, err := platform.NewService(dataStore)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed initializing platform service")
|
||||||
|
}
|
||||||
|
|
||||||
|
upgradeService, err := upgrade.NewService(
|
||||||
|
*flags.Assets,
|
||||||
|
kubernetesClientFactory,
|
||||||
|
dockerClientFactory,
|
||||||
|
composeStackManager,
|
||||||
|
dataStore,
|
||||||
|
fileService,
|
||||||
|
stackDeployer,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||||
}
|
}
|
||||||
@@ -580,10 +555,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
// but some more complex migrations require access to a kubernetes or docker
|
// but some more complex migrations require access to a kubernetes or docker
|
||||||
// client. Therefore we run a separate migration process just before
|
// client. Therefore we run a separate migration process just before
|
||||||
// starting the server.
|
// starting the server.
|
||||||
postInitMigrator := datastore.NewPostInitMigrator(
|
postInitMigrator := postinit.NewPostInitMigrator(
|
||||||
kubernetesClientFactory,
|
kubernetesClientFactory,
|
||||||
dockerClientFactory,
|
dockerClientFactory,
|
||||||
dataStore,
|
dataStore,
|
||||||
|
*flags.Assets,
|
||||||
|
kubernetesDeployer,
|
||||||
)
|
)
|
||||||
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("failure during post init migrations")
|
log.Fatal().Err(err).Msg("failure during post init migrations")
|
||||||
@@ -614,7 +591,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
ProxyManager: proxyManager,
|
ProxyManager: proxyManager,
|
||||||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||||
KubeClusterAccessService: kubeClusterAccessService,
|
KubeClusterAccessService: kubeClusterAccessService,
|
||||||
SignatureService: digitalSignatureService,
|
SignatureService: signatureService,
|
||||||
SnapshotService: snapshotService,
|
SnapshotService: snapshotService,
|
||||||
SSLService: sslService,
|
SSLService: sslService,
|
||||||
DockerClientFactory: dockerClientFactory,
|
DockerClientFactory: dockerClientFactory,
|
||||||
@@ -623,16 +600,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
ShutdownCtx: shutdownCtx,
|
ShutdownCtx: shutdownCtx,
|
||||||
ShutdownTrigger: shutdownTrigger,
|
ShutdownTrigger: shutdownTrigger,
|
||||||
StackDeployer: stackDeployer,
|
StackDeployer: stackDeployer,
|
||||||
DemoService: demoService,
|
|
||||||
UpgradeService: upgradeService,
|
UpgradeService: upgradeService,
|
||||||
AdminCreationDone: adminCreationDone,
|
AdminCreationDone: adminCreationDone,
|
||||||
PendingActionsService: pendingActionsService,
|
PendingActionsService: pendingActionsService,
|
||||||
|
PlatformService: platformService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
|
|
||||||
configureLogger()
|
configureLogger()
|
||||||
setLoggingMode("PRETTY")
|
setLoggingMode("PRETTY")
|
||||||
|
|
||||||
@@ -643,6 +618,7 @@ func main() {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
server := buildServer(flags)
|
server := buildServer(flags)
|
||||||
|
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("version", portainer.APIVersion).
|
Str("version", portainer.APIVersion).
|
||||||
Str("build_number", build.BuildNumber).
|
Str("build_number", build.BuildNumber).
|
||||||
@@ -654,6 +630,7 @@ func main() {
|
|||||||
Msg("starting Portainer")
|
Msg("starting Portainer")
|
||||||
|
|
||||||
err := server.Start()
|
err := server.Start()
|
||||||
|
|
||||||
log.Info().Err(err).Msg("HTTP server exited")
|
log.Info().Err(err).Msg("HTTP server exited")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
148
api/concurrent/concurrent.go
Normal file
148
api/concurrent/concurrent.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// Package concurrent provides utilities for running multiple functions concurrently in Go.
|
||||||
|
// For example, many kubernetes calls can take a while to fulfill. Oftentimes in Portainer
|
||||||
|
// we need to get a list of objects from multiple kubernetes REST APIs. We can often call these
|
||||||
|
// apis concurrently to speed up the response time.
|
||||||
|
// This package provides a clean way to do just that.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// The ConfigMaps and Secrets function converted using concurrent.Run.
|
||||||
|
/*
|
||||||
|
|
||||||
|
// GetConfigMapsAndSecrets gets all the ConfigMaps AND all the Secrets for a
|
||||||
|
// given namespace in a k8s endpoint. The result is a list of both config maps
|
||||||
|
// and secrets. The IsSecret boolean property indicates if a given struct is a
|
||||||
|
// secret or configmap.
|
||||||
|
func (kcl *KubeClient) GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) {
|
||||||
|
|
||||||
|
// use closures to capture the current kube client and namespace by declaring wrapper functions
|
||||||
|
// that match the interface signature for concurrent.Func
|
||||||
|
|
||||||
|
listConfigMaps := func(ctx context.Context) (any, error) {
|
||||||
|
return kcl.cli.CoreV1().ConfigMaps(namespace).List(context.Background(), meta.ListOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
listSecrets := func(ctx context.Context) (any, error) {
|
||||||
|
return kcl.cli.CoreV1().Secrets(namespace).List(context.Background(), meta.ListOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the functions concurrently and wait for results. We can also pass in a context to cancel.
|
||||||
|
// e.g. Deadline timer.
|
||||||
|
results, err := concurrent.Run(context.TODO(), listConfigMaps, listSecrets)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var configMapList *core.ConfigMapList
|
||||||
|
var secretList *core.SecretList
|
||||||
|
for _, r := range results {
|
||||||
|
switch v := r.Result.(type) {
|
||||||
|
case *core.ConfigMapList:
|
||||||
|
configMapList = v
|
||||||
|
case *core.SecretList:
|
||||||
|
secretList = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Applications
|
||||||
|
var combined []models.K8sConfigMapOrSecret
|
||||||
|
for _, m := range configMapList.Items {
|
||||||
|
var cm models.K8sConfigMapOrSecret
|
||||||
|
cm.UID = string(m.UID)
|
||||||
|
cm.Name = m.Name
|
||||||
|
cm.Namespace = m.Namespace
|
||||||
|
cm.Annotations = m.Annotations
|
||||||
|
cm.Data = m.Data
|
||||||
|
cm.CreationDate = m.CreationTimestamp.Time.UTC().Format(time.RFC3339)
|
||||||
|
combined = append(combined, cm)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range secretList.Items {
|
||||||
|
var secret models.K8sConfigMapOrSecret
|
||||||
|
secret.UID = string(s.UID)
|
||||||
|
secret.Name = s.Name
|
||||||
|
secret.Namespace = s.Namespace
|
||||||
|
secret.Annotations = s.Annotations
|
||||||
|
secret.Data = msbToMss(s.Data)
|
||||||
|
secret.CreationDate = s.CreationTimestamp.Time.UTC().Format(time.RFC3339)
|
||||||
|
secret.IsSecret = true
|
||||||
|
secret.SecretType = string(s.Type)
|
||||||
|
combined = append(combined, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
return combined, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
package concurrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result contains the result and any error returned from running a client task function
|
||||||
|
type Result struct {
|
||||||
|
Result any // the result of running the task function
|
||||||
|
Err error // any error that occurred while running the task function
|
||||||
|
}
|
||||||
|
|
||||||
|
// Func is a function returns a result or error
|
||||||
|
type Func func(ctx context.Context) (any, error)
|
||||||
|
|
||||||
|
// Run runs a list of functions returns the results
|
||||||
|
func Run(ctx context.Context, maxConcurrency int, tasks ...Func) ([]Result, error) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
resultsChan := make(chan Result, len(tasks))
|
||||||
|
taskChan := make(chan Func, len(tasks))
|
||||||
|
|
||||||
|
localCtx, cancelCtx := context.WithCancel(ctx)
|
||||||
|
defer cancelCtx()
|
||||||
|
|
||||||
|
runTask := func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for fn := range taskChan {
|
||||||
|
result, err := fn(localCtx)
|
||||||
|
resultsChan <- Result{Result: result, Err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set maxConcurrency to the number of tasks if zero or negative
|
||||||
|
if maxConcurrency <= 0 {
|
||||||
|
maxConcurrency = len(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start worker goroutines
|
||||||
|
for range maxConcurrency {
|
||||||
|
wg.Add(1)
|
||||||
|
go runTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tasks to the task channel
|
||||||
|
for _, fn := range tasks {
|
||||||
|
taskChan <- fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the task channel to signal workers to stop when all tasks are done
|
||||||
|
close(taskChan)
|
||||||
|
|
||||||
|
// Wait for all workers to complete
|
||||||
|
wg.Wait()
|
||||||
|
close(resultsChan)
|
||||||
|
|
||||||
|
// Collect the results and cancel on error
|
||||||
|
results := make([]Result, 0, len(tasks))
|
||||||
|
for r := range resultsChan {
|
||||||
|
if r.Err != nil {
|
||||||
|
cancelCtx()
|
||||||
|
|
||||||
|
return nil, r.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
@@ -5,22 +5,21 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ReadTransaction interface {
|
type ReadTransaction interface {
|
||||||
GetObject(bucketName string, key []byte, object interface{}) error
|
GetObject(bucketName string, key []byte, object any) error
|
||||||
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
GetAll(bucketName string, obj any, append func(o any) (any, error)) error
|
||||||
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, append func(o any) (any, error)) error
|
||||||
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Transaction interface {
|
type Transaction interface {
|
||||||
ReadTransaction
|
ReadTransaction
|
||||||
|
|
||||||
SetServiceName(bucketName string) error
|
SetServiceName(bucketName string) error
|
||||||
UpdateObject(bucketName string, key []byte, object interface{}) error
|
UpdateObject(bucketName string, key []byte, object any) error
|
||||||
DeleteObject(bucketName string, key []byte) error
|
DeleteObject(bucketName string, key []byte) error
|
||||||
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
|
CreateObject(bucketName string, fn func(uint64) (int, any)) error
|
||||||
CreateObjectWithId(bucketName string, id int, obj interface{}) error
|
CreateObjectWithId(bucketName string, id int, obj any) error
|
||||||
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
|
CreateObjectWithStringId(bucketName string, id []byte, obj any) error
|
||||||
DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error
|
DeleteAllObjects(bucketName string, obj any, matching func(o any) (id int, ok bool)) error
|
||||||
GetNextIdentifier(bucketName string) int
|
GetNextIdentifier(bucketName string) int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +45,8 @@ type Connection interface {
|
|||||||
NeedsEncryptionMigration() (bool, error)
|
NeedsEncryptionMigration() (bool, error)
|
||||||
SetEncrypted(encrypted bool)
|
SetEncrypted(encrypted bool)
|
||||||
|
|
||||||
BackupMetadata() (map[string]interface{}, error)
|
BackupMetadata() (map[string]any, error)
|
||||||
RestoreMetadata(s map[string]interface{}) error
|
RestoreMetadata(s map[string]any) error
|
||||||
|
|
||||||
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
|
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
|
||||||
ConvertToKey(v int) []byte
|
ConvertToKey(v int) []byte
|
||||||
|
|||||||
@@ -1,52 +1,216 @@
|
|||||||
package crypto
|
package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
"golang.org/x/crypto/scrypt"
|
"golang.org/x/crypto/scrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NOTE: has to go with what is considered to be a simplistic in that it omits any
|
const (
|
||||||
// authentication of the encrypted data.
|
// AES GCM settings
|
||||||
// Person with better knowledge is welcomed to improve it.
|
aesGcmHeader = "AES256-GCM" // The encrypted file header
|
||||||
// sourced from https://golang.org/src/crypto/cipher/example_test.go
|
aesGcmBlockSize = 1024 * 1024 // 1MB block for aes gcm
|
||||||
|
|
||||||
var emptySalt []byte = make([]byte, 0)
|
// Argon2 settings
|
||||||
|
// Recommded settings lower memory hardware according to current OWASP recommendations
|
||||||
|
// Considering some people run portainer on a NAS I think it's prudent not to assume we're on server grade hardware
|
||||||
|
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
|
||||||
|
argon2MemoryCost = 12 * 1024
|
||||||
|
argon2TimeCost = 3
|
||||||
|
argon2Threads = 1
|
||||||
|
argon2KeyLength = 32
|
||||||
|
)
|
||||||
|
|
||||||
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
|
// AesEncrypt reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key
|
||||||
// passphrase is used to generate an encryption key.
|
|
||||||
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
|
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
|
||||||
// making a 32 bytes key that would correspond to AES-256
|
err := aesEncryptGCM(input, output, passphrase)
|
||||||
// don't necessarily need a salt, so just kept in empty
|
|
||||||
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("error encrypting file: %w", err)
|
||||||
}
|
|
||||||
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the key is unique for each ciphertext, then it's ok to use a zero
|
|
||||||
// IV.
|
|
||||||
var iv [aes.BlockSize]byte
|
|
||||||
stream := cipher.NewOFB(block, iv[:])
|
|
||||||
|
|
||||||
writer := &cipher.StreamWriter{S: stream, W: output}
|
|
||||||
// Copy the input to the output, encrypting as we go.
|
|
||||||
if _, err := io.Copy(writer, input); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
|
// AesDecrypt reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from
|
||||||
// passphrase is used to generate an encryption key.
|
|
||||||
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
|
func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
|
// Read file header to determine how it was encrypted
|
||||||
|
inputReader := bufio.NewReader(input)
|
||||||
|
header, err := inputReader.Peek(len(aesGcmHeader))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading encrypted backup file header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(header) == aesGcmHeader {
|
||||||
|
reader, err := aesDecryptGCM(inputReader, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decrypting file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the previous decryption routine which has no header (to support older archives)
|
||||||
|
reader, err := aesDecryptOFB(inputReader, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decrypting legacy file backup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// aesEncryptGCM reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key.
|
||||||
|
func aesEncryptGCM(input io.Reader, output io.Writer, passphrase []byte) error {
|
||||||
|
// Derive key using argon2 with a random salt
|
||||||
|
salt := make([]byte, 16) // 16 bytes salt
|
||||||
|
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := argon2.IDKey(passphrase, salt, argon2TimeCost, argon2MemoryCost, argon2Threads, 32)
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
aesgcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate nonce
|
||||||
|
nonce, err := NewRandomNonce(aesgcm.NonceSize())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the header
|
||||||
|
if _, err := output.Write([]byte(aesGcmHeader)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write nonce and salt to the output file
|
||||||
|
if _, err := output.Write(salt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := output.Write(nonce.Value()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer for reading plaintext blocks
|
||||||
|
buf := make([]byte, aesGcmBlockSize) // Adjust buffer size as needed
|
||||||
|
ciphertext := make([]byte, len(buf)+aesgcm.Overhead())
|
||||||
|
|
||||||
|
// Encrypt plaintext in blocks
|
||||||
|
for {
|
||||||
|
n, err := io.ReadFull(input, buf)
|
||||||
|
if n == 0 {
|
||||||
|
break // end of plaintext input
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seal encrypts the plaintext using the nonce returning the updated slice.
|
||||||
|
ciphertext = aesgcm.Seal(ciphertext[:0], nonce.Value(), buf[:n], nil)
|
||||||
|
|
||||||
|
_, err = output.Write(ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce.Increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// aesDecryptGCM reads from input, decrypts with AES-256 and returns the reader to read the decrypted content from.
|
||||||
|
func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
|
// Reader & verify header
|
||||||
|
header := make([]byte, len(aesGcmHeader))
|
||||||
|
if _, err := io.ReadFull(input, header); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(header) != aesGcmHeader {
|
||||||
|
return nil, fmt.Errorf("invalid header")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read salt
|
||||||
|
salt := make([]byte, 16) // Salt size
|
||||||
|
if _, err := io.ReadFull(input, salt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := argon2.IDKey(passphrase, salt, argon2TimeCost, argon2MemoryCost, argon2Threads, 32)
|
||||||
|
|
||||||
|
// Initialize AES cipher block
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create GCM mode with the cipher block
|
||||||
|
aesgcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read nonce from the input reader
|
||||||
|
nonce := NewNonce(aesgcm.NonceSize())
|
||||||
|
if err := nonce.Read(input); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a buffer to store decrypted data
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
plaintext := make([]byte, aesGcmBlockSize)
|
||||||
|
|
||||||
|
// Decrypt the ciphertext in blocks
|
||||||
|
for {
|
||||||
|
// Read a block of ciphertext from the input reader
|
||||||
|
ciphertextBlock := make([]byte, aesGcmBlockSize+aesgcm.Overhead()) // Adjust block size as needed
|
||||||
|
n, err := io.ReadFull(input, ciphertextBlock)
|
||||||
|
if n == 0 {
|
||||||
|
break // end of ciphertext
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !(errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the block of ciphertext
|
||||||
|
plaintext, err = aesgcm.Open(plaintext[:0], nonce.Value(), ciphertextBlock[:n], nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = buf.Write(plaintext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce.Increment()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// aesDecryptOFB reads from input, decrypts with AES-256 and returns the reader to a read decrypted content from.
|
||||||
|
// passphrase is used to generate an encryption key.
|
||||||
|
// note: This function used to decrypt files that were encrypted without a header i.e. old archives
|
||||||
|
func aesDecryptOFB(input io.Reader, passphrase []byte) (io.Reader, error) {
|
||||||
|
var emptySalt []byte = make([]byte, 0)
|
||||||
|
|
||||||
// making a 32 bytes key that would correspond to AES-256
|
// making a 32 bytes key that would correspond to AES-256
|
||||||
// don't necessarily need a salt, so just kept in empty
|
// don't necessarily need a salt, so just kept in empty
|
||||||
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
|
key, err := scrypt.Key(passphrase, emptySalt, 32768, 8, 1, 32)
|
||||||
@@ -59,11 +223,9 @@ func AesDecrypt(input io.Reader, passphrase []byte) (io.Reader, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the key is unique for each ciphertext, then it's ok to use a zero
|
// If the key is unique for each ciphertext, then it's ok to use a zero IV.
|
||||||
// IV.
|
|
||||||
var iv [aes.BlockSize]byte
|
var iv [aes.BlockSize]byte
|
||||||
stream := cipher.NewOFB(block, iv[:])
|
stream := cipher.NewOFB(block, iv[:])
|
||||||
|
|
||||||
reader := &cipher.StreamReader{S: stream, R: input}
|
reader := &cipher.StreamReader{S: stream, R: input}
|
||||||
|
|
||||||
return reader, nil
|
return reader, nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package crypto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -9,7 +10,19 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
func randBytes(n int) []byte {
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||||
|
const passphrase = "passphrase"
|
||||||
|
|
||||||
tmpdir := t.TempDir()
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -18,17 +31,99 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
|||||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||||
)
|
)
|
||||||
|
|
||||||
content := []byte("content")
|
content := randBytes(1024*1024*100 + 523)
|
||||||
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
|
originFile, _ := os.Open(originFilePath)
|
||||||
|
defer originFile.Close()
|
||||||
|
|
||||||
|
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||||
|
|
||||||
|
err := AesEncrypt(originFile, encryptedFileWriter, []byte(passphrase))
|
||||||
|
assert.Nil(t, err, "Failed to encrypt a file")
|
||||||
|
encryptedFileWriter.Close()
|
||||||
|
|
||||||
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
|
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||||
|
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||||
|
|
||||||
|
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||||
|
defer encryptedFileReader.Close()
|
||||||
|
|
||||||
|
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||||
|
defer decryptedFileWriter.Close()
|
||||||
|
|
||||||
|
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(passphrase))
|
||||||
|
assert.Nil(t, err, "Failed to decrypt file")
|
||||||
|
|
||||||
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
|
|
||||||
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
|
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_encryptAndDecrypt_withStrongPassphrase(t *testing.T) {
|
||||||
|
const passphrase = "A strong passphrase with special characters: !@#$%^&*()_+"
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
var (
|
||||||
|
originFilePath = filepath.Join(tmpdir, "origin2")
|
||||||
|
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
|
||||||
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
|
||||||
|
)
|
||||||
|
|
||||||
|
content := randBytes(500)
|
||||||
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
|
originFile, _ := os.Open(originFilePath)
|
||||||
|
defer originFile.Close()
|
||||||
|
|
||||||
|
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||||
|
|
||||||
|
err := AesEncrypt(originFile, encryptedFileWriter, []byte(passphrase))
|
||||||
|
assert.Nil(t, err, "Failed to encrypt a file")
|
||||||
|
encryptedFileWriter.Close()
|
||||||
|
|
||||||
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
|
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||||
|
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||||
|
|
||||||
|
encryptedFileReader, _ := os.Open(encryptedFilePath)
|
||||||
|
defer encryptedFileReader.Close()
|
||||||
|
|
||||||
|
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||||
|
defer decryptedFileWriter.Close()
|
||||||
|
|
||||||
|
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte(passphrase))
|
||||||
|
assert.Nil(t, err, "Failed to decrypt file")
|
||||||
|
|
||||||
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
|
|
||||||
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
|
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
|
||||||
|
tmpdir := t.TempDir()
|
||||||
|
|
||||||
|
var (
|
||||||
|
originFilePath = filepath.Join(tmpdir, "origin2")
|
||||||
|
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
|
||||||
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
|
||||||
|
)
|
||||||
|
|
||||||
|
content := randBytes(500)
|
||||||
os.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
defer originFile.Close()
|
defer originFile.Close()
|
||||||
|
|
||||||
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
encryptedFileWriter, _ := os.Create(encryptedFilePath)
|
||||||
defer encryptedFileWriter.Close()
|
|
||||||
|
|
||||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
|
||||||
assert.Nil(t, err, "Failed to encrypt a file")
|
assert.Nil(t, err, "Failed to encrypt a file")
|
||||||
|
encryptedFileWriter.Close()
|
||||||
|
|
||||||
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
assert.Nil(t, err, "Couldn't read encrypted file")
|
||||||
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
|
||||||
@@ -57,7 +152,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
|||||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||||
)
|
)
|
||||||
|
|
||||||
content := []byte("content")
|
content := randBytes(1024 * 50)
|
||||||
os.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
@@ -96,7 +191,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
|||||||
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
|
||||||
)
|
)
|
||||||
|
|
||||||
content := []byte("content")
|
content := randBytes(1034)
|
||||||
os.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
@@ -117,11 +212,6 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
|||||||
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
decryptedFileWriter, _ := os.Create(decryptedFilePath)
|
||||||
defer decryptedFileWriter.Close()
|
defer decryptedFileWriter.Close()
|
||||||
|
|
||||||
decryptedReader, err := AesDecrypt(encryptedFileReader, []byte("garbage"))
|
_, err = AesDecrypt(encryptedFileReader, []byte("garbage"))
|
||||||
assert.Nil(t, err, "Should allow to decrypt with wrong passphrase")
|
assert.NotNil(t, err, "Should not allow decrypt with wrong passphrase")
|
||||||
|
|
||||||
io.Copy(decryptedFileWriter, decryptedReader)
|
|
||||||
|
|
||||||
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
|
||||||
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
|
|
||||||
}
|
}
|
||||||
|
|||||||
61
api/crypto/nonce.go
Normal file
61
api/crypto/nonce.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Nonce struct {
|
||||||
|
val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNonce(size int) *Nonce {
|
||||||
|
return &Nonce{val: make([]byte, size)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRandomNonce generates a new initial nonce with the lower byte set to a random value
|
||||||
|
// This ensures there are plenty of nonce values availble before rolling over
|
||||||
|
// Based on ideas from the Secure Programming Cookbook for C and C++ by John Viega, Matt Messier
|
||||||
|
// https://www.oreilly.com/library/view/secure-programming-cookbook/0596003943/ch04s09.html
|
||||||
|
func NewRandomNonce(size int) (*Nonce, error) {
|
||||||
|
randomBytes := 1
|
||||||
|
if size <= randomBytes {
|
||||||
|
return nil, errors.New("nonce size must be greater than the number of random bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
randomPart := make([]byte, randomBytes)
|
||||||
|
if _, err := rand.Read(randomPart); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroPart := make([]byte, size-randomBytes)
|
||||||
|
nonceVal := append(randomPart, zeroPart...)
|
||||||
|
return &Nonce{val: nonceVal}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nonce) Read(stream io.Reader) error {
|
||||||
|
_, err := io.ReadFull(stream, n.val)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nonce) Value() []byte {
|
||||||
|
return n.val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Nonce) Increment() error {
|
||||||
|
// Start incrementing from the least significant byte
|
||||||
|
for i := len(n.val) - 1; i >= 0; i-- {
|
||||||
|
// Increment the current byte
|
||||||
|
n.val[i]++
|
||||||
|
|
||||||
|
// Check for overflow
|
||||||
|
if n.val[i] != 0 {
|
||||||
|
// No overflow, nonce is successfully incremented
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach here, it means the nonce has overflowed
|
||||||
|
return errors.New("nonce overflow")
|
||||||
|
}
|
||||||
@@ -22,6 +22,12 @@ func CreateTLSConfiguration() *tls.Config {
|
|||||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
@@ -73,7 +74,6 @@ func (connection *DbConnection) IsEncryptedStore() bool {
|
|||||||
// NeedsEncryptionMigration returns true if database encryption is enabled and
|
// NeedsEncryptionMigration returns true if database encryption is enabled and
|
||||||
// we have an un-encrypted DB that requires migration to an encrypted DB
|
// we have an un-encrypted DB that requires migration to an encrypted DB
|
||||||
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
|
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
|
||||||
|
|
||||||
// Cases: Note, we need to check both portainer.db and portainer.edb
|
// Cases: Note, we need to check both portainer.db and portainer.edb
|
||||||
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
|
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
|
||||||
|
|
||||||
@@ -121,11 +121,11 @@ func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
|
|||||||
|
|
||||||
// Open opens and initializes the BoltDB database.
|
// Open opens and initializes the BoltDB database.
|
||||||
func (connection *DbConnection) Open() error {
|
func (connection *DbConnection) Open() error {
|
||||||
|
|
||||||
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
|
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
|
||||||
|
|
||||||
// Now we open the db
|
// Now we open the db
|
||||||
databasePath := connection.GetDatabaseFilePath()
|
databasePath := connection.GetDatabaseFilePath()
|
||||||
|
|
||||||
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
|
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
|
||||||
Timeout: 1 * time.Second,
|
Timeout: 1 * time.Second,
|
||||||
InitialMmapSize: connection.InitialMmapSize,
|
InitialMmapSize: connection.InitialMmapSize,
|
||||||
@@ -144,6 +144,8 @@ func (connection *DbConnection) Open() error {
|
|||||||
// Close closes the BoltDB database.
|
// Close closes the BoltDB database.
|
||||||
// Safe to being called multiple times.
|
// Safe to being called multiple times.
|
||||||
func (connection *DbConnection) Close() error {
|
func (connection *DbConnection) Close() error {
|
||||||
|
log.Info().Msg("closing PortainerDB")
|
||||||
|
|
||||||
if connection.DB != nil {
|
if connection.DB != nil {
|
||||||
return connection.DB.Close()
|
return connection.DB.Close()
|
||||||
}
|
}
|
||||||
@@ -176,6 +178,7 @@ func (connection *DbConnection) ViewTx(fn func(portainer.Transaction) error) err
|
|||||||
func (connection *DbConnection) BackupTo(w io.Writer) error {
|
func (connection *DbConnection) BackupTo(w io.Writer) error {
|
||||||
return connection.View(func(tx *bolt.Tx) error {
|
return connection.View(func(tx *bolt.Tx) error {
|
||||||
_, err := tx.WriteTo(w)
|
_, err := tx.WriteTo(w)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -190,6 +193,7 @@ func (connection *DbConnection) ExportRaw(filename string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(filename, b, 0600)
|
return os.WriteFile(filename, b, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +203,7 @@ func (connection *DbConnection) ExportRaw(filename string) error {
|
|||||||
func (connection *DbConnection) ConvertToKey(v int) []byte {
|
func (connection *DbConnection) ConvertToKey(v int) []byte {
|
||||||
b := make([]byte, 8)
|
b := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(b, uint64(v))
|
binary.BigEndian.PutUint64(b, uint64(v))
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +215,7 @@ func keyToString(b []byte) string {
|
|||||||
|
|
||||||
v := binary.BigEndian.Uint64(b)
|
v := binary.BigEndian.Uint64(b)
|
||||||
if v <= math.MaxInt32 {
|
if v <= math.MaxInt32 {
|
||||||
return fmt.Sprintf("%d", v)
|
return strconv.FormatUint(v, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(b)
|
return string(b)
|
||||||
@@ -224,7 +229,7 @@ func (connection *DbConnection) SetServiceName(bucketName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetObject is a generic function used to retrieve an unmarshalled object from a 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 {
|
func (connection *DbConnection) GetObject(bucketName string, key []byte, object any) error {
|
||||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
return tx.GetObject(bucketName, key, object)
|
return tx.GetObject(bucketName, key, object)
|
||||||
})
|
})
|
||||||
@@ -239,7 +244,7 @@ func (connection *DbConnection) getEncryptionKey() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateObject is a generic function used to update an object inside a 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 {
|
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object any) error {
|
||||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return tx.UpdateObject(bucketName, key, object)
|
return tx.UpdateObject(bucketName, key, object)
|
||||||
})
|
})
|
||||||
@@ -280,7 +285,7 @@ func (connection *DbConnection) DeleteObject(bucketName string, key []byte) erro
|
|||||||
|
|
||||||
// DeleteAllObjects delete all objects where matching() returns (id, ok).
|
// 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"?
|
// TODO: think about how to return the error inside (maybe change ok to type err, and use "notfound"?
|
||||||
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
|
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj any, matching func(o any) (id int, ok bool)) error {
|
||||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return tx.DeleteAllObjects(bucketName, obj, matching)
|
return tx.DeleteAllObjects(bucketName, obj, matching)
|
||||||
})
|
})
|
||||||
@@ -299,71 +304,64 @@ func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateObject creates a new object in the bucket, using the next bucket sequence id
|
// 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 {
|
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, any)) error {
|
||||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return tx.CreateObject(bucketName, fn)
|
return tx.CreateObject(bucketName, fn)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateObjectWithId creates a new object in the bucket, using the specified id
|
// CreateObjectWithId creates a new object in the bucket, using the specified id
|
||||||
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj any) error {
|
||||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return tx.CreateObjectWithId(bucketName, id, obj)
|
return tx.CreateObjectWithId(bucketName, id, obj)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
|
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
|
||||||
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
|
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj any) error {
|
||||||
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return tx.CreateObjectWithStringId(bucketName, id, obj)
|
return tx.CreateObjectWithStringId(bucketName, id, obj)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
func (connection *DbConnection) GetAll(bucketName string, obj any, appendFn func(o any) (any, error)) error {
|
||||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
return tx.GetAll(bucketName, obj, append)
|
return tx.GetAll(bucketName, obj, appendFn)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: decide which Unmarshal to use, and use one...
|
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, appendFn func(o any) (any, error)) error {
|
||||||
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
|
||||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
return tx.GetAllWithJsoniter(bucketName, obj, append)
|
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, appendFn)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
|
||||||
return connection.ViewTx(func(tx portainer.Transaction) error {
|
|
||||||
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, append)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackupMetadata will return a copy of the boltdb sequence numbers for all buckets.
|
// BackupMetadata will return a copy of the boltdb sequence numbers for all buckets.
|
||||||
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
|
func (connection *DbConnection) BackupMetadata() (map[string]any, error) {
|
||||||
buckets := map[string]interface{}{}
|
buckets := map[string]any{}
|
||||||
|
|
||||||
err := connection.View(func(tx *bolt.Tx) error {
|
err := connection.View(func(tx *bolt.Tx) error {
|
||||||
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
return tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||||
bucketName := string(name)
|
bucketName := string(name)
|
||||||
seqId := bucket.Sequence()
|
seqId := bucket.Sequence()
|
||||||
buckets[bucketName] = int(seqId)
|
buckets[bucketName] = int(seqId)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return buckets, err
|
return buckets, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreMetadata will restore the boltdb sequence numbers for all buckets.
|
// RestoreMetadata will restore the boltdb sequence numbers for all buckets.
|
||||||
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
|
func (connection *DbConnection) RestoreMetadata(s map[string]any) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for bucketName, v := range s {
|
for bucketName, v := range s {
|
||||||
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
|
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Error().Str("bucket", bucketName).Msg("failed to restore metadata to bucket, skipped")
|
log.Error().Str("bucket", bucketName).Msg("failed to restore metadata to bucket, skipped")
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,10 +87,7 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
||||||
connection := DbConnection{Path: dir}
|
connection := DbConnection{Path: dir}
|
||||||
|
|
||||||
if tc.dbname == "both" {
|
if tc.dbname == "both" {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
|
func backupMetadata(connection *bolt.DB) (map[string]any, error) {
|
||||||
buckets := map[string]interface{}{}
|
buckets := map[string]any{}
|
||||||
|
|
||||||
err := connection.View(func(tx *bolt.Tx) error {
|
err := connection.View(func(tx *bolt.Tx) error {
|
||||||
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||||
@@ -39,7 +39,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
|
|||||||
}
|
}
|
||||||
defer connection.Close()
|
defer connection.Close()
|
||||||
|
|
||||||
backup := make(map[string]interface{})
|
backup := make(map[string]any)
|
||||||
if metadata {
|
if metadata {
|
||||||
meta, err := backupMetadata(connection)
|
meta, err := backupMetadata(connection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -52,7 +52,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
|
|||||||
err = connection.View(func(tx *bolt.Tx) error {
|
err = connection.View(func(tx *bolt.Tx) error {
|
||||||
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
|
||||||
bucketName := string(name)
|
bucketName := string(name)
|
||||||
var list []interface{}
|
var list []any
|
||||||
version := make(map[string]string)
|
version := make(map[string]string)
|
||||||
cursor := bucket.Cursor()
|
cursor := bucket.Cursor()
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
@@ -60,7 +60,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj interface{}
|
var obj any
|
||||||
err := c.UnmarshalObject(v, &obj)
|
err := c.UnmarshalObject(v, &obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
|
|||||||
@@ -5,17 +5,16 @@ import (
|
|||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errEncryptedStringTooShort = fmt.Errorf("encrypted string too short")
|
var errEncryptedStringTooShort = errors.New("encrypted string too short")
|
||||||
|
|
||||||
// MarshalObject encodes an object to binary format
|
// MarshalObject encodes an object to binary format
|
||||||
func (connection *DbConnection) MarshalObject(object interface{}) ([]byte, error) {
|
func (connection *DbConnection) MarshalObject(object any) ([]byte, error) {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
// Special case for the VERSION bucket. Here we're not using json
|
// Special case for the VERSION bucket. Here we're not using json
|
||||||
@@ -39,7 +38,7 @@ func (connection *DbConnection) MarshalObject(object interface{}) ([]byte, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalObject decodes an object from binary data
|
// UnmarshalObject decodes an object from binary data
|
||||||
func (connection *DbConnection) UnmarshalObject(data []byte, object interface{}) error {
|
func (connection *DbConnection) UnmarshalObject(data []byte, object any) error {
|
||||||
var err error
|
var err error
|
||||||
if connection.getEncryptionKey() != nil {
|
if connection.getEncryptionKey() != nil {
|
||||||
data, err = decrypt(data, connection.getEncryptionKey())
|
data, err = decrypt(data, connection.getEncryptionKey())
|
||||||
@@ -47,8 +46,8 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object interface{})
|
|||||||
return errors.Wrap(err, "Failed decrypting object")
|
return errors.Wrap(err, "Failed decrypting object")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e := json.Unmarshal(data, object)
|
|
||||||
if e != nil {
|
if e := json.Unmarshal(data, object); e != nil {
|
||||||
// Special case for the VERSION bucket. Here we're not using json
|
// Special case for the VERSION bucket. Here we're not using json
|
||||||
// So we need to return it as a string
|
// So we need to return it as a string
|
||||||
s, ok := object.(*string)
|
s, ok := object.(*string)
|
||||||
@@ -58,6 +57,7 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object interface{})
|
|||||||
|
|
||||||
*s = string(data)
|
*s = string(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,22 +70,20 @@ func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return encrypted, err
|
return encrypted, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce := make([]byte, gcm.NonceSize())
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
return encrypted, err
|
return encrypted, err
|
||||||
}
|
}
|
||||||
ciphertextByte := gcm.Seal(
|
|
||||||
nonce,
|
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||||
nonce,
|
|
||||||
plaintext,
|
|
||||||
nil)
|
|
||||||
return ciphertextByte, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
|
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
|
||||||
if string(encrypted) == "false" {
|
if string(encrypted) == "false" {
|
||||||
return []byte("false"), nil
|
return []byte("false"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
block, err := aes.NewCipher(passphrase)
|
block, err := aes.NewCipher(passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return encrypted, errors.Wrap(err, "Error creating cypher block")
|
return encrypted, errors.Wrap(err, "Error creating cypher block")
|
||||||
@@ -102,11 +100,8 @@ func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
|
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
|
||||||
plaintextByte, err = gcm.Open(
|
|
||||||
nil,
|
plaintextByte, err = gcm.Open(nil, nonce, ciphertextByteClean, nil)
|
||||||
nonce,
|
|
||||||
ciphertextByteClean,
|
|
||||||
nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return encrypted, errors.Wrap(err, "Error decrypting text")
|
return encrypted, errors.Wrap(err, "Error decrypting text")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
|
|||||||
uuid := uuid.Must(uuid.NewV4())
|
uuid := uuid.Must(uuid.NewV4())
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
object interface{}
|
object any
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
|
|||||||
expected: uuid.String(),
|
expected: uuid.String(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
object: map[string]interface{}{"key": "value"},
|
object: map[string]any{"key": "value"},
|
||||||
expected: `{"key":"value"}`,
|
expected: `{"key":"value"}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -73,11 +73,11 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
|
|||||||
expected: `["1","2","3"]`,
|
expected: `["1","2","3"]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
object: []map[string]interface{}{{"key1": "value1"}, {"key2": "value2"}},
|
object: []map[string]any{{"key1": "value1"}, {"key2": "value2"}},
|
||||||
expected: `[{"key1":"value1"},{"key2":"value2"}]`,
|
expected: `[{"key1":"value1"},{"key2":"value2"}]`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
object: []interface{}{1, "2", false, map[string]interface{}{"key1": "value1"}},
|
object: []any{1, "2", false, map[string]any{"key1": "value1"}},
|
||||||
expected: `[1,"2",false,{"key1":"value1"}]`,
|
expected: `[1,"2",false,{"key1":"value1"}]`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (tx *DbTransaction) SetServiceName(bucketName string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interface{}) error {
|
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object any) error {
|
||||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
value := bucket.Get(key)
|
value := bucket.Get(key)
|
||||||
@@ -31,7 +31,7 @@ func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interfa
|
|||||||
return tx.conn.UnmarshalObject(value, object)
|
return tx.conn.UnmarshalObject(value, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object any) error {
|
||||||
data, err := tx.conn.MarshalObject(object)
|
data, err := tx.conn.MarshalObject(object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -46,7 +46,7 @@ func (tx *DbTransaction) DeleteObject(bucketName string, key []byte) error {
|
|||||||
return bucket.Delete(key)
|
return bucket.Delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, matchingFn func(o interface{}) (id int, ok bool)) error {
|
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj any, matchingFn func(o any) (id int, ok bool)) error {
|
||||||
var ids []int
|
var ids []int
|
||||||
|
|
||||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
@@ -74,16 +74,18 @@ func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, ma
|
|||||||
|
|
||||||
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
|
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
|
||||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
id, err := bucket.NextSequence()
|
id, err := bucket.NextSequence()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifer")
|
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifier")
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return int(id)
|
return int(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
|
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, any)) error {
|
||||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
seqId, _ := bucket.NextSequence()
|
seqId, _ := bucket.NextSequence()
|
||||||
@@ -97,7 +99,7 @@ func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, i
|
|||||||
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj any) error {
|
||||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
data, err := tx.conn.MarshalObject(obj)
|
data, err := tx.conn.MarshalObject(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -107,7 +109,7 @@ func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj inter
|
|||||||
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
|
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj any) error {
|
||||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
data, err := tx.conn.MarshalObject(obj)
|
data, err := tx.conn.MarshalObject(obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -117,7 +119,7 @@ func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte,
|
|||||||
return bucket.Put(id, data)
|
return bucket.Put(id, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
|
func (tx *DbTransaction) GetAll(bucketName string, obj any, appendFn func(o any) (any, error)) error {
|
||||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
return bucket.ForEach(func(k []byte, v []byte) error {
|
return bucket.ForEach(func(k []byte, v []byte) error {
|
||||||
@@ -130,20 +132,7 @@ func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, appendFn fun
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *DbTransaction) GetAllWithJsoniter(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
|
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, appendFn func(o any) (any, error)) error {
|
||||||
bucket := tx.tx.Bucket([]byte(bucketName))
|
|
||||||
|
|
||||||
return bucket.ForEach(func(k []byte, v []byte) error {
|
|
||||||
err := tx.conn.UnmarshalObject(v, obj)
|
|
||||||
if err == nil {
|
|
||||||
obj, err = appendFn(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
|
|
||||||
cursor := tx.tx.Bucket([]byte(bucketName)).Cursor()
|
cursor := tx.tx.Bucket([]byte(bucketName)).Cursor()
|
||||||
|
|
||||||
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
|
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package apikeyrepository
|
package apikeyrepository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@@ -37,12 +36,12 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
|
|
||||||
// GetAPIKeysByUserID returns a slice containing all the APIKeys a user has access to.
|
// GetAPIKeysByUserID returns a slice containing all the APIKeys a user has access to.
|
||||||
func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error) {
|
func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error) {
|
||||||
var result = make([]portainer.APIKey, 0)
|
result := make([]portainer.APIKey, 0)
|
||||||
|
|
||||||
err := service.Connection.GetAll(
|
err := service.Connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.APIKey{},
|
&portainer.APIKey{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
func(obj any) (any, error) {
|
||||||
record, ok := obj.(*portainer.APIKey)
|
record, ok := obj.(*portainer.APIKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
|
||||||
@@ -61,19 +60,19 @@ func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer
|
|||||||
|
|
||||||
// GetAPIKeyByDigest returns the API key for the associated digest.
|
// GetAPIKeyByDigest returns the API key for the associated digest.
|
||||||
// Note: there is a 1-to-1 mapping of api-key and digest
|
// Note: there is a 1-to-1 mapping of api-key and digest
|
||||||
func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error) {
|
func (service *Service) GetAPIKeyByDigest(digest string) (*portainer.APIKey, error) {
|
||||||
var k *portainer.APIKey
|
var k *portainer.APIKey
|
||||||
stop := fmt.Errorf("ok")
|
stop := fmt.Errorf("ok")
|
||||||
err := service.Connection.GetAll(
|
err := service.Connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.APIKey{},
|
&portainer.APIKey{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
func(obj any) (any, error) {
|
||||||
key, ok := obj.(*portainer.APIKey)
|
key, ok := obj.(*portainer.APIKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
|
||||||
return nil, fmt.Errorf("failed to convert to APIKey object: %s", obj)
|
return nil, fmt.Errorf("failed to convert to APIKey object: %s", obj)
|
||||||
}
|
}
|
||||||
if bytes.Equal(key.Digest, digest) {
|
if key.Digest == digest {
|
||||||
k = key
|
k = key
|
||||||
return nil, stop
|
return nil, stop
|
||||||
}
|
}
|
||||||
@@ -96,7 +95,7 @@ func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, err
|
|||||||
func (service *Service) Create(record *portainer.APIKey) error {
|
func (service *Service) Create(record *portainer.APIKey) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
record.ID = portainer.APIKeyID(id)
|
record.ID = portainer.APIKeyID(id)
|
||||||
|
|
||||||
return int(record.ID), record
|
return int(record.ID), record
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func (service BaseDataServiceTx[T, I]) Read(ID I) (*T, error) {
|
|||||||
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
|
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
|
||||||
var collection = make([]T, 0)
|
var collection = make([]T, 0)
|
||||||
|
|
||||||
return collection, service.Tx.GetAllWithJsoniter(
|
return collection, service.Tx.GetAll(
|
||||||
service.Bucket,
|
service.Bucket,
|
||||||
new(T),
|
new(T),
|
||||||
AppendFn(&collection),
|
AppendFn(&collection),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFun
|
|||||||
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
|
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
|
||||||
return service.Tx.CreateObject(
|
return service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
group.ID = portainer.EdgeGroupID(id)
|
group.ID = portainer.EdgeGroupID(id)
|
||||||
return int(group.ID), group
|
return int(group.ID), group
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func (service ServiceTx) EdgeStacks() ([]portainer.EdgeStack, error) {
|
|||||||
err := service.tx.GetAll(
|
err := service.tx.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.EdgeStack{},
|
&portainer.EdgeStack{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
func(obj any) (any, error) {
|
||||||
stack, ok := obj.(*portainer.EdgeStack)
|
stack, ok := obj.(*portainer.EdgeStack)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
@@ -157,6 +159,7 @@ func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -166,11 +169,13 @@ func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.
|
|||||||
func (service *Service) GetNextIdentifier() int {
|
func (service *Service) GetNextIdentifier() int {
|
||||||
var identifier int
|
var identifier int
|
||||||
|
|
||||||
service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
if err := service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
identifier = service.Tx(tx).GetNextIdentifier()
|
identifier = service.Tx(tx).GetNextIdentifier()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Str("bucket", BucketName).Msg("could not get the next identifier")
|
||||||
|
}
|
||||||
|
|
||||||
return identifier
|
return identifier
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ func (service ServiceTx) BucketName() string {
|
|||||||
// Endpoint returns an environment(endpoint) by ID.
|
// Endpoint returns an environment(endpoint) by ID.
|
||||||
func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||||
var endpoint portainer.Endpoint
|
var endpoint portainer.Endpoint
|
||||||
|
|
||||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
err := service.tx.GetObject(BucketName, identifier, &endpoint)
|
if err := service.tx.GetObject(BucketName, identifier, &endpoint); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +36,7 @@ func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
|
|||||||
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
err := service.tx.UpdateObject(BucketName, identifier, endpoint)
|
if err := service.tx.UpdateObject(BucketName, identifier, endpoint); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +44,7 @@ func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *porta
|
|||||||
if len(endpoint.EdgeID) > 0 {
|
if len(endpoint.EdgeID) > 0 {
|
||||||
service.service.idxEdgeID[endpoint.EdgeID] = ID
|
service.service.idxEdgeID[endpoint.EdgeID] = ID
|
||||||
}
|
}
|
||||||
|
|
||||||
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
|
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
|
||||||
service.service.mu.Unlock()
|
service.service.mu.Unlock()
|
||||||
|
|
||||||
@@ -57,8 +57,7 @@ func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *porta
|
|||||||
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
|
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||||
identifier := service.service.connection.ConvertToKey(int(ID))
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
err := service.tx.DeleteObject(BucketName, identifier)
|
if err := service.tx.DeleteObject(BucketName, identifier); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +69,7 @@ func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.service.heartbeats.Delete(ID)
|
service.service.heartbeats.Delete(ID)
|
||||||
service.service.mu.Unlock()
|
service.service.mu.Unlock()
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
|
|||||||
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
|
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
|
||||||
var endpoints = make([]portainer.Endpoint, 0)
|
var endpoints = make([]portainer.Endpoint, 0)
|
||||||
|
|
||||||
return endpoints, service.tx.GetAllWithJsoniter(
|
return endpoints, service.tx.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.Endpoint{},
|
&portainer.Endpoint{},
|
||||||
dataservices.AppendFn(&endpoints),
|
dataservices.AppendFn(&endpoints),
|
||||||
@@ -107,8 +107,7 @@ func (service ServiceTx) UpdateHeartbeat(endpointID portainer.EndpointID) {
|
|||||||
|
|
||||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||||
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
|
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
|
||||||
err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
|
if err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +115,7 @@ func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
|
|||||||
if len(endpoint.EdgeID) > 0 {
|
if len(endpoint.EdgeID) > 0 {
|
||||||
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
|
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
|
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
|
||||||
service.service.mu.Unlock()
|
service.service.mu.Unlock()
|
||||||
|
|
||||||
@@ -134,6 +134,7 @@ func (service ServiceTx) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
|||||||
func (service *Service) Create(endpointGroup *portainer.EndpointGroup) error {
|
func (service *Service) Create(endpointGroup *portainer.EndpointGroup) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
endpointGroup.ID = portainer.EndpointGroupID(id)
|
endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||||
return int(endpointGroup.ID), endpointGroup
|
return int(endpointGroup.ID), endpointGroup
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type ServiceTx struct {
|
|||||||
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
|
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
|
||||||
return service.Tx.CreateObject(
|
return service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
endpointGroup.ID = portainer.EndpointGroupID(id)
|
endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||||
return int(endpointGroup.ID), endpointGroup
|
return int(endpointGroup.ID), endpointGroup
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ func (service *Service) RegisterUpdateStackFunction(
|
|||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
func NewService(connection portainer.Connection) (*Service, error) {
|
func NewService(connection portainer.Connection) (*Service, error) {
|
||||||
err := connection.SetServiceName(BucketName)
|
if err := connection.SetServiceName(BucketName); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,8 +64,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
|
|||||||
var endpointRelation portainer.EndpointRelation
|
var endpointRelation portainer.EndpointRelation
|
||||||
identifier := service.connection.ConvertToKey(int(endpointID))
|
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||||
|
|
||||||
err := service.connection.GetObject(BucketName, identifier, &endpointRelation)
|
if err := service.connection.GetObject(BucketName, identifier, &endpointRelation); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,8 +159,12 @@ func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationStat
|
|||||||
// list how many time this stack is referenced in all relations
|
// list how many time this stack is referenced in all relations
|
||||||
// in order to update the stack deployments count
|
// in order to update the stack deployments count
|
||||||
for refStackId, refStackEnabled := range stacksToUpdate {
|
for refStackId, refStackEnabled := range stacksToUpdate {
|
||||||
if refStackEnabled {
|
if !refStackEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
numDeployments := 0
|
numDeployments := 0
|
||||||
|
|
||||||
for _, r := range relations {
|
for _, r := range relations {
|
||||||
for sId, enabled := range r.EdgeStacks {
|
for sId, enabled := range r.EdgeStacks {
|
||||||
if enabled && sId == refStackId {
|
if enabled && sId == refStackId {
|
||||||
@@ -171,9 +173,10 @@ func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
|
if err := service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||||
edgeStack.NumDeployments = numDeployments
|
edgeStack.NumDeployments = numDeployments
|
||||||
})
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not update the number of deployments")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,8 +33,7 @@ func (service ServiceTx) EndpointRelation(endpointID portainer.EndpointID) (*por
|
|||||||
var endpointRelation portainer.EndpointRelation
|
var endpointRelation portainer.EndpointRelation
|
||||||
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||||
|
|
||||||
err := service.tx.GetObject(BucketName, identifier, &endpointRelation)
|
if err := service.tx.GetObject(BucketName, identifier, &endpointRelation); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +128,10 @@ func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationSta
|
|||||||
// list how many time this stack is referenced in all relations
|
// list how many time this stack is referenced in all relations
|
||||||
// in order to update the stack deployments count
|
// in order to update the stack deployments count
|
||||||
for refStackId, refStackEnabled := range stacksToUpdate {
|
for refStackId, refStackEnabled := range stacksToUpdate {
|
||||||
if refStackEnabled {
|
if !refStackEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
numDeployments := 0
|
numDeployments := 0
|
||||||
for _, r := range relations {
|
for _, r := range relations {
|
||||||
for sId, enabled := range r.EdgeStacks {
|
for sId, enabled := range r.EdgeStacks {
|
||||||
@@ -139,9 +141,10 @@ func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
|
if err := service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||||
edgeStack.NumDeployments = numDeployments
|
edgeStack.NumDeployments = numDeployments
|
||||||
})
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not update the number of deployments")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package fdoprofile
|
|
||||||
|
|
||||||
import (
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
|
||||||
const BucketName = "fdo_profiles"
|
|
||||||
|
|
||||||
// Service represents a service for managingFDO Profiles data.
|
|
||||||
type Service struct {
|
|
||||||
dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Service{
|
|
||||||
BaseDataService: dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]{
|
|
||||||
Bucket: BucketName,
|
|
||||||
Connection: connection,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create assign an ID to a new FDO Profile and saves it.
|
|
||||||
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
|
|
||||||
return service.Connection.CreateObjectWithId(
|
|
||||||
BucketName,
|
|
||||||
int(FDOProfile.ID),
|
|
||||||
FDOProfile,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNextIdentifier returns the next identifier for a FDO Profile.
|
|
||||||
func (service *Service) GetNextIdentifier() int {
|
|
||||||
return service.Connection.GetNextIdentifier(BucketName)
|
|
||||||
}
|
|
||||||
@@ -45,7 +45,7 @@ func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]p
|
|||||||
func (service *Service) Create(record *portainer.HelmUserRepository) error {
|
func (service *Service) Create(record *portainer.HelmUserRepository) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
record.ID = portainer.HelmUserRepositoryID(id)
|
record.ID = portainer.HelmUserRepositoryID(id)
|
||||||
return int(record.ID), record
|
return int(record.ID), record
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ func IsErrObjectNotFound(e error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AppendFn appends elements to the given collection slice
|
// AppendFn appends elements to the given collection slice
|
||||||
func AppendFn[T any](collection *[]T) func(obj interface{}) (interface{}, error) {
|
func AppendFn[T any](collection *[]T) func(obj any) (any, error) {
|
||||||
return func(obj interface{}) (interface{}, error) {
|
return func(obj any) (any, error) {
|
||||||
element, ok := obj.(*T)
|
element, ok := obj.(*T)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||||
@@ -32,8 +32,8 @@ func AppendFn[T any](collection *[]T) func(obj interface{}) (interface{}, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FilterFn appends elements to the given collection when the predicate is true
|
// FilterFn appends elements to the given collection when the predicate is true
|
||||||
func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
|
func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj any) (any, error) {
|
||||||
return func(obj interface{}) (interface{}, error) {
|
return func(obj any) (any, error) {
|
||||||
element, ok := obj.(*T)
|
element, ok := obj.(*T)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||||
@@ -50,8 +50,8 @@ func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj interface
|
|||||||
|
|
||||||
// FirstFn sets the element to the first one that satisfies the predicate and stops the computation, returns ErrStop on
|
// FirstFn sets the element to the first one that satisfies the predicate and stops the computation, returns ErrStop on
|
||||||
// success
|
// success
|
||||||
func FirstFn[T any](element *T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
|
func FirstFn[T any](element *T, predicate func(T) bool) func(obj any) (any, error) {
|
||||||
return func(obj interface{}) (interface{}, error) {
|
return func(obj any) (any, error) {
|
||||||
e, ok := obj.(*T)
|
e, ok := obj.(*T)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package dataservices
|
package dataservices
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/database/models"
|
"github.com/portainer/portainer/api/database/models"
|
||||||
)
|
)
|
||||||
@@ -18,7 +15,6 @@ type (
|
|||||||
Endpoint() EndpointService
|
Endpoint() EndpointService
|
||||||
EndpointGroup() EndpointGroupService
|
EndpointGroup() EndpointGroupService
|
||||||
EndpointRelation() EndpointRelationService
|
EndpointRelation() EndpointRelationService
|
||||||
FDOProfile() FDOProfileService
|
|
||||||
HelmUserRepository() HelmUserRepositoryService
|
HelmUserRepository() HelmUserRepositoryService
|
||||||
Registry() RegistryService
|
Registry() RegistryService
|
||||||
ResourceControl() ResourceControlService
|
ResourceControl() ResourceControlService
|
||||||
@@ -39,6 +35,7 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
DataStore interface {
|
DataStore interface {
|
||||||
|
Connection() portainer.Connection
|
||||||
Open() (newStore bool, err error)
|
Open() (newStore bool, err error)
|
||||||
Init() error
|
Init() error
|
||||||
Close() error
|
Close() error
|
||||||
@@ -47,7 +44,7 @@ type (
|
|||||||
MigrateData() error
|
MigrateData() error
|
||||||
Rollback(force bool) error
|
Rollback(force bool) error
|
||||||
CheckCurrentEdition() error
|
CheckCurrentEdition() error
|
||||||
BackupTo(w io.Writer) error
|
Backup(path string) (string, error)
|
||||||
Export(filename string) (err error)
|
Export(filename string) (err error)
|
||||||
|
|
||||||
DataStoreTx
|
DataStoreTx
|
||||||
@@ -74,8 +71,9 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
PendingActionsService interface {
|
PendingActionsService interface {
|
||||||
BaseCRUD[portainer.PendingActions, portainer.PendingActionsID]
|
BaseCRUD[portainer.PendingAction, portainer.PendingActionID]
|
||||||
GetNextIdentifier() int
|
GetNextIdentifier() int
|
||||||
|
DeleteByEndpointID(ID portainer.EndpointID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeStackService represents a service to manage Edge stacks
|
// EdgeStackService represents a service to manage Edge stacks
|
||||||
@@ -121,27 +119,12 @@ type (
|
|||||||
BucketName() string
|
BucketName() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// FDOProfileService represents a service to manage FDO Profiles
|
|
||||||
FDOProfileService interface {
|
|
||||||
BaseCRUD[portainer.FDOProfile, portainer.FDOProfileID]
|
|
||||||
GetNextIdentifier() int
|
|
||||||
}
|
|
||||||
|
|
||||||
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
|
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
|
||||||
HelmUserRepositoryService interface {
|
HelmUserRepositoryService interface {
|
||||||
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
||||||
HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error)
|
HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTService represents a service for managing JWT tokens
|
|
||||||
JWTService interface {
|
|
||||||
GenerateToken(data *portainer.TokenData) (string, error)
|
|
||||||
GenerateTokenForOAuth(data *portainer.TokenData, expiryTime *time.Time) (string, error)
|
|
||||||
GenerateTokenForKubeconfig(data *portainer.TokenData) (string, error)
|
|
||||||
ParseAndVerifyToken(token string) (*portainer.TokenData, error)
|
|
||||||
SetUserSessionDuration(userSessionDuration time.Duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegistryService represents a service for managing registry data
|
// RegistryService represents a service for managing registry data
|
||||||
RegistryService interface {
|
RegistryService interface {
|
||||||
BaseCRUD[portainer.Registry, portainer.RegistryID]
|
BaseCRUD[portainer.Registry, portainer.RegistryID]
|
||||||
@@ -162,7 +145,7 @@ type (
|
|||||||
APIKeyRepository interface {
|
APIKeyRepository interface {
|
||||||
BaseCRUD[portainer.APIKey, portainer.APIKeyID]
|
BaseCRUD[portainer.APIKey, portainer.APIKeyID]
|
||||||
GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error)
|
GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error)
|
||||||
GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error)
|
GetAPIKeyByDigest(digest string) (*portainer.APIKey, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SettingsService represents a service for managing application settings
|
// SettingsService represents a service for managing application settings
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package pendingactions
|
package pendingactions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -12,11 +14,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
dataservices.BaseDataService[portainer.PendingActions, portainer.PendingActionsID]
|
dataservices.BaseDataService[portainer.PendingAction, portainer.PendingActionID]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceTx struct {
|
type ServiceTx struct {
|
||||||
dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]
|
dataservices.BaseDataServiceTx[portainer.PendingAction, portainer.PendingActionID]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(connection portainer.Connection) (*Service, error) {
|
func NewService(connection portainer.Connection) (*Service, error) {
|
||||||
@@ -26,28 +28,34 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
BaseDataService: dataservices.BaseDataService[portainer.PendingActions, portainer.PendingActionsID]{
|
BaseDataService: dataservices.BaseDataService[portainer.PendingAction, portainer.PendingActionID]{
|
||||||
Bucket: BucketName,
|
Bucket: BucketName,
|
||||||
Connection: connection,
|
Connection: connection,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) Create(config *portainer.PendingActions) error {
|
func (s Service) Create(config *portainer.PendingAction) error {
|
||||||
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return s.Tx(tx).Create(config)
|
return s.Tx(tx).Create(config)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Service) Update(ID portainer.PendingActionsID, config *portainer.PendingActions) error {
|
func (s Service) Update(ID portainer.PendingActionID, config *portainer.PendingAction) error {
|
||||||
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return s.Tx(tx).Update(ID, config)
|
return s.Tx(tx).Update(ID, config)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s Service) DeleteByEndpointID(ID portainer.EndpointID) error {
|
||||||
|
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return s.Tx(tx).DeleteByEndpointID(ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
return ServiceTx{
|
return ServiceTx{
|
||||||
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]{
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingAction, portainer.PendingActionID]{
|
||||||
Bucket: BucketName,
|
Bucket: BucketName,
|
||||||
Connection: service.Connection,
|
Connection: service.Connection,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
@@ -55,19 +63,42 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s ServiceTx) Create(config *portainer.PendingActions) error {
|
func (s ServiceTx) Create(config *portainer.PendingAction) error {
|
||||||
return s.Tx.CreateObject(BucketName, func(id uint64) (int, interface{}) {
|
return s.Tx.CreateObject(BucketName, func(id uint64) (int, any) {
|
||||||
config.ID = portainer.PendingActionsID(id)
|
config.ID = portainer.PendingActionID(id)
|
||||||
config.CreatedAt = time.Now().Unix()
|
config.CreatedAt = time.Now().Unix()
|
||||||
|
|
||||||
return int(config.ID), config
|
return int(config.ID), config
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s ServiceTx) Update(ID portainer.PendingActionsID, config *portainer.PendingActions) error {
|
func (s ServiceTx) Update(ID portainer.PendingActionID, config *portainer.PendingAction) error {
|
||||||
return s.BaseDataServiceTx.Update(ID, config)
|
return s.BaseDataServiceTx.Update(ID, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s ServiceTx) DeleteByEndpointID(ID portainer.EndpointID) error {
|
||||||
|
log.Debug().Int("endpointId", int(ID)).Msg("deleting pending actions for endpoint")
|
||||||
|
pendingActions, err := s.BaseDataServiceTx.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve pending-actions for endpoint (%d): %w", ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pendingAction := range pendingActions {
|
||||||
|
if pendingAction.EndpointID == ID {
|
||||||
|
err := s.BaseDataServiceTx.Delete(pendingAction.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Int("endpointId", int(ID)).Msgf("failed to delete pending action: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextIdentifier returns the next identifier for a custom template.
|
||||||
|
func (service ServiceTx) GetNextIdentifier() int {
|
||||||
|
return service.Tx.GetNextIdentifier(BucketName)
|
||||||
|
}
|
||||||
|
|
||||||
// GetNextIdentifier returns the next identifier for a custom template.
|
// GetNextIdentifier returns the next identifier for a custom template.
|
||||||
func (service *Service) GetNextIdentifier() int {
|
func (service *Service) GetNextIdentifier() int {
|
||||||
return service.Connection.GetNextIdentifier(BucketName)
|
return service.Connection.GetNextIdentifier(BucketName)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
|||||||
func (service *Service) Create(registry *portainer.Registry) error {
|
func (service *Service) Create(registry *portainer.Registry) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
registry.ID = portainer.RegistryID(id)
|
registry.ID = portainer.RegistryID(id)
|
||||||
return int(registry.ID), registry
|
return int(registry.ID), registry
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type ServiceTx struct {
|
|||||||
func (service ServiceTx) Create(registry *portainer.Registry) error {
|
func (service ServiceTx) Create(registry *portainer.Registry) error {
|
||||||
return service.Tx.CreateObject(
|
return service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
registry.ID = portainer.RegistryID(id)
|
registry.ID = portainer.RegistryID(id)
|
||||||
return int(registry.ID), registry
|
return int(registry.ID), registry
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
|
|||||||
err := service.Connection.GetAll(
|
err := service.Connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.ResourceControl{},
|
&portainer.ResourceControl{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
func(obj any) (any, error) {
|
||||||
rc, ok := obj.(*portainer.ResourceControl)
|
rc, ok := obj.(*portainer.ResourceControl)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||||
@@ -84,7 +84,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
|
|||||||
func (service *Service) Create(resourceControl *portainer.ResourceControl) error {
|
func (service *Service) Create(resourceControl *portainer.ResourceControl) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
resourceControl.ID = portainer.ResourceControlID(id)
|
resourceControl.ID = portainer.ResourceControlID(id)
|
||||||
return int(resourceControl.ID), resourceControl
|
return int(resourceControl.ID), resourceControl
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, r
|
|||||||
err := service.Tx.GetAll(
|
err := service.Tx.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.ResourceControl{},
|
&portainer.ResourceControl{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
func(obj any) (any, error) {
|
||||||
rc, ok := obj.(*portainer.ResourceControl)
|
rc, ok := obj.(*portainer.ResourceControl)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||||
@@ -55,7 +55,7 @@ func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, r
|
|||||||
func (service ServiceTx) Create(resourceControl *portainer.ResourceControl) error {
|
func (service ServiceTx) Create(resourceControl *portainer.ResourceControl) error {
|
||||||
return service.Tx.CreateObject(
|
return service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
resourceControl.ID = portainer.ResourceControlID(id)
|
resourceControl.ID = portainer.ResourceControlID(id)
|
||||||
return int(resourceControl.ID), resourceControl
|
return int(resourceControl.ID), resourceControl
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
|||||||
func (service *Service) Create(role *portainer.Role) error {
|
func (service *Service) Create(role *portainer.Role) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
role.ID = portainer.RoleID(id)
|
role.ID = portainer.RoleID(id)
|
||||||
return int(role.ID), role
|
return int(role.ID), role
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type ServiceTx struct {
|
|||||||
func (service ServiceTx) Create(role *portainer.Role) error {
|
func (service ServiceTx) Create(role *portainer.Role) error {
|
||||||
return service.Tx.CreateObject(
|
return service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
role.ID = portainer.RoleID(id)
|
role.ID = portainer.RoleID(id)
|
||||||
return int(role.ID), role
|
return int(role.ID), role
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TestService_StackByWebhookID(t *testing.T) {
|
|||||||
|
|
||||||
b := stackBuilder{t: t, store: store}
|
b := stackBuilder{t: t, store: store}
|
||||||
b.createNewStack(newGuidString(t))
|
b.createNewStack(newGuidString(t))
|
||||||
for i := 0; i < 10; i++ {
|
for range 10 {
|
||||||
b.createNewStack("")
|
b.createNewStack("")
|
||||||
}
|
}
|
||||||
webhookID := newGuidString(t)
|
webhookID := newGuidString(t)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
|||||||
func (service *Service) Create(tag *portainer.Tag) error {
|
func (service *Service) Create(tag *portainer.Tag) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
tag.ID = portainer.TagID(id)
|
tag.ID = portainer.TagID(id)
|
||||||
return int(tag.ID), tag
|
return int(tag.ID), tag
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type ServiceTx struct {
|
|||||||
func (service ServiceTx) Create(tag *portainer.Tag) error {
|
func (service ServiceTx) Create(tag *portainer.Tag) error {
|
||||||
return service.Tx.CreateObject(
|
return service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
tag.ID = portainer.TagID(id)
|
tag.ID = portainer.TagID(id)
|
||||||
return int(tag.ID), tag
|
return int(tag.ID), tag
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ type Service struct {
|
|||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
func NewService(connection portainer.Connection) (*Service, error) {
|
func NewService(connection portainer.Connection) (*Service, error) {
|
||||||
err := connection.SetServiceName(BucketName)
|
if err := connection.SetServiceName(BucketName); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +31,16 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
|
return ServiceTx{
|
||||||
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Team, portainer.TeamID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: service.Connection,
|
||||||
|
Tx: tx,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TeamByName returns a team by name.
|
// TeamByName returns a team by name.
|
||||||
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
|
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
|
||||||
var t portainer.Team
|
var t portainer.Team
|
||||||
@@ -59,7 +68,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
|
|||||||
func (service *Service) Create(team *portainer.Team) error {
|
func (service *Service) Create(team *portainer.Team) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
team.ID = portainer.TeamID(id)
|
team.ID = portainer.TeamID(id)
|
||||||
return int(team.ID), team
|
return int(team.ID), team
|
||||||
},
|
},
|
||||||
|
|||||||
48
api/dataservices/team/tx.go
Normal file
48
api/dataservices/team/tx.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package team
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
dataservices.BaseDataServiceTx[portainer.Team, portainer.TeamID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamByName returns a team by name.
|
||||||
|
func (service ServiceTx) TeamByName(name string) (*portainer.Team, error) {
|
||||||
|
var t portainer.Team
|
||||||
|
|
||||||
|
err := service.Tx.GetAll(
|
||||||
|
BucketName,
|
||||||
|
&portainer.Team{},
|
||||||
|
dataservices.FirstFn(&t, func(e portainer.Team) bool {
|
||||||
|
return strings.EqualFold(e.Name, name)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors.Is(err, dataservices.ErrStop) {
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil, dserrors.ErrObjectNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTeam creates a new Team.
|
||||||
|
func (service ServiceTx) Create(team *portainer.Team) error {
|
||||||
|
return service.Tx.CreateObject(
|
||||||
|
BucketName,
|
||||||
|
func(id uint64) (int, any) {
|
||||||
|
team.ID = portainer.TeamID(id)
|
||||||
|
return int(team.ID), team
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]port
|
|||||||
func (service *Service) Create(membership *portainer.TeamMembership) error {
|
func (service *Service) Create(membership *portainer.TeamMembership) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
membership.ID = portainer.TeamMembershipID(id)
|
membership.ID = portainer.TeamMembershipID(id)
|
||||||
return int(membership.ID), membership
|
return int(membership.ID), membership
|
||||||
},
|
},
|
||||||
@@ -84,8 +84,8 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
|
|||||||
return service.Connection.DeleteAllObjects(
|
return service.Connection.DeleteAllObjects(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.TeamMembership{},
|
&portainer.TeamMembership{},
|
||||||
func(obj interface{}) (id int, ok bool) {
|
func(obj any) (id int, ok bool) {
|
||||||
membership, ok := obj.(portainer.TeamMembership)
|
membership, ok := obj.(*portainer.TeamMembership)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
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 fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||||
@@ -105,8 +105,8 @@ func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) er
|
|||||||
return service.Connection.DeleteAllObjects(
|
return service.Connection.DeleteAllObjects(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.TeamMembership{},
|
&portainer.TeamMembership{},
|
||||||
func(obj interface{}) (id int, ok bool) {
|
func(obj any) (id int, ok bool) {
|
||||||
membership, ok := obj.(portainer.TeamMembership)
|
membership, ok := obj.(*portainer.TeamMembership)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
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 fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||||
@@ -125,8 +125,8 @@ func (service *Service) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.T
|
|||||||
return service.Connection.DeleteAllObjects(
|
return service.Connection.DeleteAllObjects(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.TeamMembership{},
|
&portainer.TeamMembership{},
|
||||||
func(obj interface{}) (id int, ok bool) {
|
func(obj any) (id int, ok bool) {
|
||||||
membership, ok := obj.(portainer.TeamMembership)
|
membership, ok := obj.(*portainer.TeamMembership)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
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 fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func (service ServiceTx) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]por
|
|||||||
func (service ServiceTx) Create(membership *portainer.TeamMembership) error {
|
func (service ServiceTx) Create(membership *portainer.TeamMembership) error {
|
||||||
return service.Tx.CreateObject(
|
return service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
membership.ID = portainer.TeamMembershipID(id)
|
membership.ID = portainer.TeamMembershipID(id)
|
||||||
return int(membership.ID), membership
|
return int(membership.ID), membership
|
||||||
},
|
},
|
||||||
@@ -55,7 +55,7 @@ func (service ServiceTx) DeleteTeamMembershipByUserID(userID portainer.UserID) e
|
|||||||
return service.Tx.DeleteAllObjects(
|
return service.Tx.DeleteAllObjects(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.TeamMembership{},
|
&portainer.TeamMembership{},
|
||||||
func(obj interface{}) (id int, ok bool) {
|
func(obj any) (id int, ok bool) {
|
||||||
membership, ok := obj.(portainer.TeamMembership)
|
membership, ok := obj.(portainer.TeamMembership)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||||
@@ -76,7 +76,7 @@ func (service ServiceTx) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) e
|
|||||||
return service.Tx.DeleteAllObjects(
|
return service.Tx.DeleteAllObjects(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.TeamMembership{},
|
&portainer.TeamMembership{},
|
||||||
func(obj interface{}) (id int, ok bool) {
|
func(obj any) (id int, ok bool) {
|
||||||
membership, ok := obj.(portainer.TeamMembership)
|
membership, ok := obj.(portainer.TeamMembership)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||||
@@ -96,7 +96,7 @@ func (service ServiceTx) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.
|
|||||||
return service.Tx.DeleteAllObjects(
|
return service.Tx.DeleteAllObjects(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.TeamMembership{},
|
&portainer.TeamMembership{},
|
||||||
func(obj interface{}) (id int, ok bool) {
|
func(obj any) (id int, ok bool) {
|
||||||
membership, ok := obj.(portainer.TeamMembership)
|
membership, ok := obj.(portainer.TeamMembership)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (service ServiceTx) UsersByRole(role portainer.UserRole) ([]portainer.User,
|
|||||||
func (service ServiceTx) Create(user *portainer.User) error {
|
func (service ServiceTx) Create(user *portainer.User) error {
|
||||||
return service.Tx.CreateObject(
|
return service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
user.ID = portainer.UserID(id)
|
user.ID = portainer.UserID(id)
|
||||||
user.Username = strings.ToLower(user.Username)
|
user.Username = strings.ToLower(user.Username)
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
|
|||||||
func (service *Service) Create(user *portainer.User) error {
|
func (service *Service) Create(user *portainer.User) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
user.ID = portainer.UserID(id)
|
user.ID = portainer.UserID(id)
|
||||||
user.Username = strings.ToLower(user.Username)
|
user.Username = strings.ToLower(user.Username)
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
|
|||||||
func (service *Service) Create(webhook *portainer.Webhook) error {
|
func (service *Service) Create(webhook *portainer.Webhook) error {
|
||||||
return service.Connection.CreateObject(
|
return service.Connection.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, interface{}) {
|
func(id uint64) (int, any) {
|
||||||
webhook.ID = portainer.WebhookID(id)
|
webhook.ID = portainer.WebhookID(id)
|
||||||
return int(webhook.ID), webhook
|
return int(webhook.ID), webhook
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,186 +4,89 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/database/models"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var backupDefaults = struct {
|
// Backup takes an optional output path and creates a backup of the database.
|
||||||
backupDir string
|
// The database connection is stopped before running the backup to avoid any
|
||||||
commonDir string
|
// corruption and if a path is not given a default is used.
|
||||||
}{
|
// The path or an error are returned.
|
||||||
"backups",
|
func (store *Store) Backup(path string) (string, error) {
|
||||||
"common",
|
if err := store.createBackupPath(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
backupFilename := store.backupFilename()
|
||||||
|
if path != "" {
|
||||||
|
backupFilename = path
|
||||||
|
}
|
||||||
|
log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msgf("Backing up database")
|
||||||
|
|
||||||
|
// Close the store before backing up
|
||||||
|
err := store.Close()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to close store before backup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.fileService.Copy(store.connection.GetDatabaseFilePath(), backupFilename, true)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create backup file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reopen the store
|
||||||
|
_, err = store.Open()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to reopen store after backup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return backupFilename, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
func (store *Store) Restore() error {
|
||||||
// Backup Helpers
|
backupFilename := store.backupFilename()
|
||||||
//
|
return store.RestoreFromFile(backupFilename)
|
||||||
|
}
|
||||||
|
|
||||||
// createBackupFolders create initial folders for backups
|
func (store *Store) RestoreFromFile(backupFilename string) error {
|
||||||
func (store *Store) createBackupFolders() {
|
store.Close()
|
||||||
// create common dir
|
if err := store.fileService.Copy(backupFilename, store.connection.GetDatabaseFilePath(), true); err != nil {
|
||||||
commonDir := store.commonBackupDir()
|
return fmt.Errorf("unable to restore backup file %q. err: %w", backupFilename, err)
|
||||||
if exists, _ := store.fileService.FileExists(commonDir); !exists {
|
}
|
||||||
if err := os.MkdirAll(commonDir, 0700); err != nil {
|
|
||||||
log.Error().Err(err).Msg("error while creating common backup folder")
|
log.Info().Str("from", backupFilename).Str("to", store.connection.GetDatabaseFilePath()).Msgf("database restored")
|
||||||
|
|
||||||
|
_, err := store.Open()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to determine version of restored portainer backup file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine the db version
|
||||||
|
version, err := store.VersionService.Version()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to determine restored database version. err: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
editionLabel := portainer.SoftwareEdition(version.Edition).GetEditionLabel()
|
||||||
|
log.Info().Msgf("Restored database version: Portainer %s %s", editionLabel, version.SchemaVersion)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *Store) createBackupPath() error {
|
||||||
|
backupDir := path.Join(store.connection.GetStorePath(), "backups")
|
||||||
|
if exists, _ := store.fileService.FileExists(backupDir); !exists {
|
||||||
|
if err := os.MkdirAll(backupDir, 0o700); err != nil {
|
||||||
|
return fmt.Errorf("unable to create backup folder: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *Store) backupFilename() string {
|
||||||
|
return path.Join(store.connection.GetStorePath(), "backups", store.connection.GetDatabaseFileName()+".bak")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) databasePath() string {
|
func (store *Store) databasePath() string {
|
||||||
return store.connection.GetDatabaseFilePath()
|
return store.connection.GetDatabaseFilePath()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) commonBackupDir() string {
|
|
||||||
return path.Join(store.connection.GetStorePath(), backupDefaults.backupDir, backupDefaults.commonDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (store *Store) copyDBFile(from string, to string) error {
|
|
||||||
log.Info().Str("from", from).Str("to", to).Msg("copying DB file")
|
|
||||||
|
|
||||||
err := store.fileService.Copy(from, to, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupOptions provide a helper to inject backup options
|
|
||||||
type BackupOptions struct {
|
|
||||||
Version string
|
|
||||||
BackupDir string
|
|
||||||
BackupFileName string
|
|
||||||
BackupPath string
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBackupRestoreOptions returns options to store db at common backup dir location; used by:
|
|
||||||
// - db backup prior to version upgrade
|
|
||||||
// - db rollback
|
|
||||||
func getBackupRestoreOptions(backupDir string) *BackupOptions {
|
|
||||||
return &BackupOptions{
|
|
||||||
BackupDir: backupDir, //connection.commonBackupDir(),
|
|
||||||
BackupFileName: beforePortainerVersionUpgradeBackup,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup current database with default options
|
|
||||||
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 == "" {
|
|
||||||
v, err := store.VersionService.Version()
|
|
||||||
if err != nil {
|
|
||||||
options.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(), options.Version, time.Now().Format("20060102150405"))
|
|
||||||
}
|
|
||||||
if options.BackupPath == "" {
|
|
||||||
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupWithOptions backup current database with options
|
|
||||||
func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
|
|
||||||
log.Info().Msg("creating DB backup")
|
|
||||||
|
|
||||||
store.createBackupFolders()
|
|
||||||
|
|
||||||
options = store.setupOptions(options)
|
|
||||||
dbPath := store.databasePath()
|
|
||||||
|
|
||||||
if err := store.Close(); err != nil {
|
|
||||||
return options.BackupPath, fmt.Errorf(
|
|
||||||
"error closing datastore before creating backup: %w",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
|
|
||||||
return options.BackupPath, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := store.Open(); err != nil {
|
|
||||||
return options.BackupPath, fmt.Errorf(
|
|
||||||
"error opening datastore after creating backup: %w",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return options.BackupPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
|
||||||
// Restore strategies:
|
|
||||||
// - default: restore latest from current edition
|
|
||||||
// - restore a specific
|
|
||||||
func (store *Store) restoreWithOptions(options *BackupOptions) error {
|
|
||||||
options = store.setupOptions(options)
|
|
||||||
|
|
||||||
// Check if backup file exist before restoring
|
|
||||||
_, err := os.Stat(options.BackupPath)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to restore does not exist %s")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.Close()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("error while closing store before restore")
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msg("restoring DB backup")
|
|
||||||
err = store.copyDBFile(options.BackupPath, store.databasePath())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = store.Open()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveWithOptions removes backup database based on supplied options
|
|
||||||
func (store *Store) removeWithOptions(options *BackupOptions) error {
|
|
||||||
log.Info().Msg("removing DB backup")
|
|
||||||
|
|
||||||
options = store.setupOptions(options)
|
|
||||||
_, err := os.Stat(options.BackupPath)
|
|
||||||
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to remove does not exist")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("path", options.BackupPath).Msg("removing DB file")
|
|
||||||
err = os.Remove(options.BackupPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,106 +2,79 @@ package datastore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/database/models"
|
"github.com/portainer/portainer/api/database/models"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateBackupFolders(t *testing.T) {
|
|
||||||
_, store := MustNewTestStore(t, true, true)
|
|
||||||
|
|
||||||
connection := store.GetConnection()
|
|
||||||
backupPath := path.Join(connection.GetStorePath(), backupDefaults.backupDir)
|
|
||||||
|
|
||||||
if isFileExist(backupPath) {
|
|
||||||
t.Error("Expect backups folder to not exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
store.createBackupFolders()
|
|
||||||
if !isFileExist(backupPath) {
|
|
||||||
t.Error("Expect backups folder to exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreCreation(t *testing.T) {
|
func TestStoreCreation(t *testing.T) {
|
||||||
_, store := MustNewTestStore(t, true, true)
|
_, store := MustNewTestStore(t, true, true)
|
||||||
if store == nil {
|
if store == nil {
|
||||||
t.Error("Expect to create a store")
|
t.Fatal("Expect to create a store")
|
||||||
}
|
}
|
||||||
|
|
||||||
if store.CheckCurrentEdition() != nil {
|
v, err := store.VersionService.Version()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
|
||||||
|
if portainer.SoftwareEdition(v.Edition) != portainer.PortainerCE {
|
||||||
t.Error("Expect to get CE Edition")
|
t.Error("Expect to get CE Edition")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.SchemaVersion != portainer.APIVersion {
|
||||||
|
t.Error("Expect to get APIVersion")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackup(t *testing.T) {
|
func TestBackup(t *testing.T) {
|
||||||
_, store := MustNewTestStore(t, true, true)
|
_, store := MustNewTestStore(t, true, true)
|
||||||
connection := store.GetConnection()
|
backupFileName := store.backupFilename()
|
||||||
|
t.Run(fmt.Sprintf("Backup should create %s", backupFileName), func(t *testing.T) {
|
||||||
t.Run("Backup should create default db backup", func(t *testing.T) {
|
|
||||||
v := models.Version{
|
v := models.Version{
|
||||||
|
Edition: int(portainer.PortainerCE),
|
||||||
SchemaVersion: portainer.APIVersion,
|
SchemaVersion: portainer.APIVersion,
|
||||||
}
|
}
|
||||||
store.VersionService.UpdateVersion(&v)
|
store.VersionService.UpdateVersion(&v)
|
||||||
store.backupWithOptions(nil)
|
store.Backup("")
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) {
|
|
||||||
store.backupWithOptions(&BackupOptions{
|
|
||||||
BackupFileName: beforePortainerVersionUpgradeBackup,
|
|
||||||
BackupDir: store.commonBackupDir(),
|
|
||||||
})
|
|
||||||
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", beforePortainerVersionUpgradeBackup)
|
|
||||||
if !isFileExist(backupFileName) {
|
if !isFileExist(backupFileName) {
|
||||||
t.Errorf("Expect backup file to be created %s", backupFileName)
|
t.Errorf("Expect backup file to be created %s", backupFileName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoveWithOptions(t *testing.T) {
|
func TestRestore(t *testing.T) {
|
||||||
_, store := MustNewTestStore(t, true, true)
|
_, store := MustNewTestStore(t, true, false)
|
||||||
|
|
||||||
t.Run("successfully removes file if existent", func(t *testing.T) {
|
t.Run("Basic Restore", func(t *testing.T) {
|
||||||
store.createBackupFolders()
|
// override and set initial db version and edition
|
||||||
options := &BackupOptions{
|
updateEdition(store, portainer.PortainerCE)
|
||||||
BackupDir: store.commonBackupDir(),
|
updateVersion(store, "2.4")
|
||||||
BackupFileName: "test.txt",
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := path.Join(options.BackupDir, options.BackupFileName)
|
store.Backup("")
|
||||||
f, err := os.Create(filePath)
|
updateVersion(store, "2.16")
|
||||||
if err != nil {
|
testVersion(store, "2.16", t)
|
||||||
t.Fatalf("file should be created; err=%s", err)
|
store.Restore()
|
||||||
}
|
|
||||||
f.Close()
|
|
||||||
|
|
||||||
err = store.removeWithOptions(options)
|
// check if the restore is successful and the version is correct
|
||||||
if err != nil {
|
testVersion(store, "2.4", t)
|
||||||
t.Errorf("RemoveWithOptions should successfully remove file; err=%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isFileExist(f.Name()) {
|
|
||||||
t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name())
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fails to removes file if non-existent", func(t *testing.T) {
|
t.Run("Basic Restore After Multiple Backups", func(t *testing.T) {
|
||||||
options := &BackupOptions{
|
// override and set initial db version and edition
|
||||||
BackupDir: store.commonBackupDir(),
|
updateEdition(store, portainer.PortainerCE)
|
||||||
BackupFileName: "test.txt",
|
updateVersion(store, "2.4")
|
||||||
}
|
store.Backup("")
|
||||||
|
updateVersion(store, "2.14")
|
||||||
|
updateVersion(store, "2.16")
|
||||||
|
testVersion(store, "2.16", t)
|
||||||
|
store.Restore()
|
||||||
|
|
||||||
err := store.removeWithOptions(options)
|
// check if the restore is successful and the version is correct
|
||||||
if err == nil {
|
testVersion(store, "2.4", t)
|
||||||
t.Error("RemoveWithOptions should fail for non-existent file")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,14 @@ func (store *Store) Open() (newStore bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if encryptionReq {
|
if encryptionReq {
|
||||||
|
backupFilename, err := store.Backup("")
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to backup database prior to encrypting: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
err = store.encryptDB()
|
err = store.encryptDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
store.RestoreFromFile(backupFilename) // restore from backup if encryption fails
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
api/datastore/helpers_test.go
Normal file
58
api/datastore/helpers_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isFileExist is helper function to check for file existence
|
||||||
|
func isFileExist(path string) bool {
|
||||||
|
matches, err := filepath.Glob(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return len(matches) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateVersion(store *Store, v string) {
|
||||||
|
version, err := store.VersionService.Version()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
|
||||||
|
version.SchemaVersion = v
|
||||||
|
|
||||||
|
err = store.VersionService.UpdateVersion(version)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateEdition(store *Store, edition portainer.SoftwareEdition) {
|
||||||
|
version, err := store.VersionService.Version()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
|
||||||
|
version.Edition = int(edition)
|
||||||
|
|
||||||
|
err = store.VersionService.UpdateVersion(version)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testVersion is a helper which tests current store version against wanted version
|
||||||
|
func testVersion(store *Store, versionWant string, t *testing.T) {
|
||||||
|
v, err := store.VersionService.Version()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
if v.SchemaVersion != versionWant {
|
||||||
|
t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package datastore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
@@ -15,8 +16,6 @@ import (
|
|||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
|
|
||||||
|
|
||||||
func (store *Store) MigrateData() error {
|
func (store *Store) MigrateData() error {
|
||||||
updating, err := store.VersionService.IsUpdating()
|
updating, err := store.VersionService.IsUpdating()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -41,7 +40,7 @@ func (store *Store) MigrateData() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// before we alter anything in the DB, create a backup
|
// before we alter anything in the DB, create a backup
|
||||||
backupPath, err := store.Backup(version)
|
_, err = store.Backup("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "while backing up database")
|
return errors.Wrap(err, "while backing up database")
|
||||||
}
|
}
|
||||||
@@ -51,9 +50,9 @@ func (store *Store) MigrateData() error {
|
|||||||
err = errors.Wrap(err, "failed to migrate database")
|
err = errors.Wrap(err, "failed to migrate database")
|
||||||
|
|
||||||
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
|
log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
|
||||||
restorErr := store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
|
restoreErr := store.Restore()
|
||||||
if restorErr != nil {
|
if restoreErr != nil {
|
||||||
return errors.Wrap(restorErr, "failed to restore database")
|
return errors.Wrap(restoreErr, "failed to restore database")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Msg("database restored to previous version")
|
log.Info().Msg("database restored to previous version")
|
||||||
@@ -87,6 +86,7 @@ func (store *Store) newMigratorParameters(version *models.Version) *migrator.Mig
|
|||||||
EdgeStackService: store.EdgeStackService,
|
EdgeStackService: store.EdgeStackService,
|
||||||
EdgeJobService: store.EdgeJobService,
|
EdgeJobService: store.EdgeJobService,
|
||||||
TunnelServerService: store.TunnelServerService,
|
TunnelServerService: store.TunnelServerService,
|
||||||
|
PendingActionsService: store.PendingActionsService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +117,11 @@ func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special test code to simulate a failure (used by migrate_data_test.go). Do not remove...
|
||||||
|
if os.Getenv("PORTAINER_TEST_MIGRATE_FAIL") == "FAIL" {
|
||||||
|
panic("test migration failure")
|
||||||
|
}
|
||||||
|
|
||||||
err = store.VersionService.StoreIsUpdating(false)
|
err = store.VersionService.StoreIsUpdating(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to update the store")
|
return errors.Wrap(err, "failed to update the store")
|
||||||
@@ -127,7 +132,6 @@ func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models
|
|||||||
|
|
||||||
// Rollback to a pre-upgrade backup copy/snapshot of portainer.db
|
// Rollback to a pre-upgrade backup copy/snapshot of portainer.db
|
||||||
func (store *Store) connectionRollback(force bool) error {
|
func (store *Store) connectionRollback(force bool) error {
|
||||||
|
|
||||||
if !force {
|
if !force {
|
||||||
confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
|
confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
|
||||||
if err != nil || !confirmed {
|
if err != nil || !confirmed {
|
||||||
@@ -135,9 +139,7 @@ func (store *Store) connectionRollback(force bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
options := getBackupRestoreOptions(store.commonBackupDir())
|
err := store.Restore()
|
||||||
|
|
||||||
err := store.restoreWithOptions(options)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,25 @@ package datastore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/database/boltdb"
|
"github.com/portainer/portainer/api/database/boltdb"
|
||||||
"github.com/portainer/portainer/api/database/models"
|
"github.com/portainer/portainer/api/database/models"
|
||||||
|
"github.com/portainer/portainer/api/datastore/migrator"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/segmentio/encoding/json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// testVersion is a helper which tests current store version against wanted version
|
|
||||||
func testVersion(store *Store, versionWant string, t *testing.T) {
|
|
||||||
v, err := store.VersionService.Version()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expect store version to be %s but was %s with error: %s", versionWant, v.SchemaVersion, err)
|
|
||||||
}
|
|
||||||
if v.SchemaVersion != versionWant {
|
|
||||||
t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMigrateData(t *testing.T) {
|
func TestMigrateData(t *testing.T) {
|
||||||
snapshotTests := []struct {
|
tests := []struct {
|
||||||
testName string
|
testName string
|
||||||
srcPath string
|
srcPath string
|
||||||
wantPath string
|
wantPath string
|
||||||
@@ -42,7 +33,7 @@ func TestMigrateData(t *testing.T) {
|
|||||||
overrideInstanceId: true,
|
overrideInstanceId: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range snapshotTests {
|
for _, test := range tests {
|
||||||
t.Run(test.testName, func(t *testing.T) {
|
t.Run(test.testName, func(t *testing.T) {
|
||||||
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
|
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,147 +46,133 @@ func TestMigrateData(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
|
||||||
// newStore, store, teardown := MustNewTestStore(t, true, false)
|
newStore, store := MustNewTestStore(t, true, false)
|
||||||
// defer teardown()
|
if !newStore {
|
||||||
|
t.Error("Expect a new DB")
|
||||||
// if !newStore {
|
|
||||||
// t.Error("Expect a new DB")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// testVersion(store, portainer.APIVersion, t)
|
|
||||||
// store.Close()
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
// // Setup data
|
|
||||||
// v := models.Version{SchemaVersion: tc.version}
|
|
||||||
// store.VersionService.UpdateVersion(&v)
|
|
||||||
|
|
||||||
// // 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("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("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)
|
|
||||||
|
|
||||||
// store.MigrateData()
|
|
||||||
|
|
||||||
// testVersion(store, v.SchemaVersion, t)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
|
||||||
// _, store, teardown := MustNewTestStore(t, false, true)
|
|
||||||
// defer teardown()
|
|
||||||
|
|
||||||
// v := models.Version{SchemaVersion: "0.0.0"}
|
|
||||||
// store.VersionService.UpdateVersion(&v)
|
|
||||||
|
|
||||||
// store.MigrateData()
|
|
||||||
|
|
||||||
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
// store.VersionService.StoreIsUpdating(true)
|
|
||||||
|
|
||||||
// store.MigrateData()
|
|
||||||
|
|
||||||
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
|
|
||||||
// store.MigrateData()
|
|
||||||
|
|
||||||
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
|
||||||
|
|
||||||
// if isFileExist(options.BackupPath) {
|
|
||||||
// t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_getBackupRestoreOptions(t *testing.T) {
|
|
||||||
_, store := MustNewTestStore(t, false, true)
|
|
||||||
|
|
||||||
options := getBackupRestoreOptions(store.commonBackupDir())
|
|
||||||
|
|
||||||
wantDir := store.commonBackupDir()
|
|
||||||
if !strings.HasSuffix(options.BackupDir, wantDir) {
|
|
||||||
log.Fatal().Str("got", options.BackupDir).Str("want", wantDir).Msg("incorrect backup dir")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wantFilename := "portainer.db.bak"
|
testVersion(store, portainer.APIVersion, t)
|
||||||
if options.BackupFileName != wantFilename {
|
store.Close()
|
||||||
log.Fatal().Str("got", options.BackupFileName).Str("want", wantFilename).Msg("incorrect backup file")
|
|
||||||
|
newStore, _ = store.Open()
|
||||||
|
if newStore {
|
||||||
|
t.Error("Expect store to NOT be new DB")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
|
||||||
|
_, store := MustNewTestStore(t, true, false)
|
||||||
|
store.VersionService.UpdateVersion(&models.Version{SchemaVersion: "1.0", Edition: int(portainer.PortainerCE)})
|
||||||
|
store.MigrateData()
|
||||||
|
|
||||||
|
backupfilename := store.backupFilename()
|
||||||
|
if exists, _ := store.fileService.FileExists(backupfilename); !exists {
|
||||||
|
t.Errorf("Expect backup file to be created %s", backupfilename)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MigrateData should recover and restore backup during migration critical failure", func(t *testing.T) {
|
||||||
|
os.Setenv("PORTAINER_TEST_MIGRATE_FAIL", "FAIL")
|
||||||
|
|
||||||
|
version := "2.15"
|
||||||
|
_, store := MustNewTestStore(t, true, false)
|
||||||
|
store.VersionService.UpdateVersion(&models.Version{SchemaVersion: version, Edition: int(portainer.PortainerCE)})
|
||||||
|
store.MigrateData()
|
||||||
|
|
||||||
|
store.Open()
|
||||||
|
testVersion(store, version, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
|
||||||
|
_, store := MustNewTestStore(t, true, false)
|
||||||
|
store.VersionService.StoreIsUpdating(true)
|
||||||
|
store.MigrateData()
|
||||||
|
|
||||||
|
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
|
||||||
|
// If the backup file is not blank, then it means a backup was created. We don't want that because we
|
||||||
|
// only create a backup when the version changes.
|
||||||
|
backupfilename := store.backupFilename()
|
||||||
|
if exists, _ := store.fileService.FileExists(backupfilename); exists {
|
||||||
|
t.Errorf("Backup file should not exist for dirty database")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
|
||||||
|
_, store := MustNewTestStore(t, true, false)
|
||||||
|
|
||||||
|
// Set migrator the count to match our migrations array (simulate no changes).
|
||||||
|
// Should not create a backup
|
||||||
|
v, err := store.VersionService.Version()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to read version from db: %s", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
migratorParams := store.newMigratorParameters(v)
|
||||||
|
m := migrator.NewMigrator(migratorParams)
|
||||||
|
latestMigrations := m.LatestMigrations()
|
||||||
|
|
||||||
|
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
|
||||||
|
v.MigratorCount = len(latestMigrations.MigrationFuncs)
|
||||||
|
store.VersionService.UpdateVersion(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.MigrateData()
|
||||||
|
|
||||||
|
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
|
||||||
|
// If the backup file is not blank, then it means a backup was created. We don't want that because we
|
||||||
|
// only create a backup when the version changes.
|
||||||
|
backupfilename := store.backupFilename()
|
||||||
|
if exists, _ := store.fileService.FileExists(backupfilename); exists {
|
||||||
|
t.Errorf("Backup file should not exist for dirty database")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MigrateData should create backup on startup if portainer version matches db and migrationFuncs counts differ", func(t *testing.T) {
|
||||||
|
_, store := MustNewTestStore(t, true, false)
|
||||||
|
|
||||||
|
// Set migrator count very large to simulate changes
|
||||||
|
// Should not create a backup
|
||||||
|
v, err := store.VersionService.Version()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unable to read version from db: %s", err)
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
v.MigratorCount = 1000
|
||||||
|
store.VersionService.UpdateVersion(v)
|
||||||
|
store.MigrateData()
|
||||||
|
|
||||||
|
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
|
||||||
|
// If the backup file is not blank, then it means a backup was created. We don't want that because we
|
||||||
|
// only create a backup when the version changes.
|
||||||
|
backupfilename := store.backupFilename()
|
||||||
|
if exists, _ := store.fileService.FileExists(backupfilename); !exists {
|
||||||
|
t.Errorf("DB backup should exist and there should be no error")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRollback(t *testing.T) {
|
func TestRollback(t *testing.T) {
|
||||||
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||||
version := models.Version{SchemaVersion: "2.4.0"}
|
version := "2.11"
|
||||||
_, store := MustNewTestStore(t, true, false)
|
|
||||||
|
|
||||||
err := store.VersionService.UpdateVersion(&version)
|
v := models.Version{
|
||||||
if err != nil {
|
SchemaVersion: version,
|
||||||
t.Errorf("Failed updating version: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
|
_, store := MustNewTestStore(t, false, false)
|
||||||
|
store.VersionService.UpdateVersion(&v)
|
||||||
|
|
||||||
|
_, err := store.Backup("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("")
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the current version
|
v.SchemaVersion = "2.14"
|
||||||
version2 := models.Version{SchemaVersion: "2.6.0"}
|
// Change the current edition
|
||||||
err = store.VersionService.UpdateVersion(&version2)
|
err = store.VersionService.UpdateVersion(&v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("")
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
}
|
||||||
@@ -207,26 +184,45 @@ func TestRollback(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = store.Open()
|
store.Open()
|
||||||
|
testVersion(store, version, t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
|
||||||
|
version := "2.15"
|
||||||
|
|
||||||
|
v := models.Version{
|
||||||
|
SchemaVersion: version,
|
||||||
|
Edition: int(portainer.PortainerCE),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, store := MustNewTestStore(t, true, false)
|
||||||
|
store.VersionService.UpdateVersion(&v)
|
||||||
|
|
||||||
|
_, err := store.Backup("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("Open failed: %s", err)
|
log.Fatal().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
|
||||||
|
v.SchemaVersion = "2.14"
|
||||||
|
// Change the current edition
|
||||||
|
err = store.VersionService.UpdateVersion(&v)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.Rollback(true)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Rollback failed: %s", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
testVersion(store, version.SchemaVersion, t)
|
store.Open()
|
||||||
|
testVersion(store, version, t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// isFileExist is helper function to check for file existence
|
|
||||||
func isFileExist(path string) bool {
|
|
||||||
matches, err := filepath.Glob(path)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return len(matches) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
|
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
|
||||||
// parses it into a database, runs a migration on that database, and then
|
// parses it into a database, runs a migration on that database, and then
|
||||||
// compares it with an expected output database.
|
// compares it with an expected output database.
|
||||||
@@ -309,7 +305,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanc
|
|||||||
os.WriteFile(
|
os.WriteFile(
|
||||||
gotPath,
|
gotPath,
|
||||||
gotJSON,
|
gotJSON,
|
||||||
0600,
|
0o600,
|
||||||
)
|
)
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
|
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
|
||||||
@@ -325,7 +321,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanc
|
|||||||
// importJSON reads input JSON and commits it to a portainer datastore.Store.
|
// importJSON reads input JSON and commits it to a portainer datastore.Store.
|
||||||
// Errors are logged with the testing package.
|
// Errors are logged with the testing package.
|
||||||
func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
||||||
objects := make(map[string]interface{})
|
objects := make(map[string]any)
|
||||||
|
|
||||||
// Parse json into map of objects.
|
// Parse json into map of objects.
|
||||||
d := json.NewDecoder(r)
|
d := json.NewDecoder(r)
|
||||||
@@ -341,9 +337,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
|||||||
for k, v := range objects {
|
for k, v := range objects {
|
||||||
switch k {
|
switch k {
|
||||||
case "version":
|
case "version":
|
||||||
versions, ok := v.(map[string]interface{})
|
versions, ok := v.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Logf("failed casting %s to map[string]interface{}", k)
|
t.Logf("failed casting %s to map[string]any", k)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New format db
|
// New format db
|
||||||
@@ -408,9 +404,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "dockerhub":
|
case "dockerhub":
|
||||||
obj, ok := v.([]interface{})
|
obj, ok := v.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Logf("failed to cast %s to []interface{}", k)
|
t.Logf("failed to cast %s to []any", k)
|
||||||
}
|
}
|
||||||
err := con.CreateObjectWithStringId(
|
err := con.CreateObjectWithStringId(
|
||||||
k,
|
k,
|
||||||
@@ -422,9 +418,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "ssl":
|
case "ssl":
|
||||||
obj, ok := v.(map[string]interface{})
|
obj, ok := v.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Logf("failed to case %s to map[string]interface{}", k)
|
t.Logf("failed to case %s to map[string]any", k)
|
||||||
}
|
}
|
||||||
err := con.CreateObjectWithStringId(
|
err := con.CreateObjectWithStringId(
|
||||||
k,
|
k,
|
||||||
@@ -436,9 +432,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "settings":
|
case "settings":
|
||||||
obj, ok := v.(map[string]interface{})
|
obj, ok := v.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Logf("failed to case %s to map[string]interface{}", k)
|
t.Logf("failed to case %s to map[string]any", k)
|
||||||
}
|
}
|
||||||
err := con.CreateObjectWithStringId(
|
err := con.CreateObjectWithStringId(
|
||||||
k,
|
k,
|
||||||
@@ -450,9 +446,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "tunnel_server":
|
case "tunnel_server":
|
||||||
obj, ok := v.(map[string]interface{})
|
obj, ok := v.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Logf("failed to case %s to map[string]interface{}", k)
|
t.Logf("failed to case %s to map[string]any", k)
|
||||||
}
|
}
|
||||||
err := con.CreateObjectWithStringId(
|
err := con.CreateObjectWithStringId(
|
||||||
k,
|
k,
|
||||||
@@ -466,18 +462,18 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
default:
|
default:
|
||||||
objlist, ok := v.([]interface{})
|
objlist, ok := v.([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Logf("failed to cast %s to []interface{}", k)
|
t.Logf("failed to cast %s to []any", k)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, obj := range objlist {
|
for _, obj := range objlist {
|
||||||
value, ok := obj.(map[string]interface{})
|
value, ok := obj.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Logf("failed to cast %v to map[string]interface{}", obj)
|
t.Logf("failed to cast %v to map[string]any", obj)
|
||||||
} else {
|
} else {
|
||||||
var ok bool
|
var ok bool
|
||||||
var id interface{}
|
var id any
|
||||||
switch k {
|
switch k {
|
||||||
case "endpoint_relations":
|
case "endpoint_relations":
|
||||||
// TODO: need to make into an int, then do that weird
|
// TODO: need to make into an int, then do that weird
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ const dummyLogoURL = "example.com"
|
|||||||
|
|
||||||
// initTestingDBConn creates a settings service with raw database DB connection
|
// initTestingDBConn creates a settings service with raw database DB connection
|
||||||
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
|
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
|
||||||
func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]interface{}) error {
|
func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]any) error {
|
||||||
//insert a obj
|
//insert a obj
|
||||||
return dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj)
|
return dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(store *Store) error {
|
func setup(store *Store) error {
|
||||||
dummySettingsObj := map[string]interface{}{
|
dummySettingsObj := map[string]any{
|
||||||
"LogoURL": dummyLogoURL,
|
"LogoURL": dummyLogoURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
|
|||||||
return &models.Version{
|
return &models.Version{
|
||||||
SchemaVersion: dbVersionToSemanticVersion(dbVersion),
|
SchemaVersion: dbVersionToSemanticVersion(dbVersion),
|
||||||
Edition: edition,
|
Edition: edition,
|
||||||
InstanceID: string(instanceId),
|
InstanceID: instanceId,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,5 +111,6 @@ func (store *Store) finishMigrateLegacyVersion(versionToWrite *models.Version) e
|
|||||||
store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey))
|
store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey))
|
||||||
store.connection.DeleteObject(bucketName, []byte(legacyEditionKey))
|
store.connection.DeleteObject(bucketName, []byte(legacyEditionKey))
|
||||||
store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
|
store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
"github.com/Masterminds/semver"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ func migrationError(err error, context string) error {
|
|||||||
return errors.Wrap(err, "failed in "+context)
|
return errors.Wrap(err, "failed in "+context)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFunctionName(i interface{}) string {
|
func GetFunctionName(i any) string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,20 +39,19 @@ func (m *Migrator) Migrate() error {
|
|||||||
latestMigrations := m.LatestMigrations()
|
latestMigrations := m.LatestMigrations()
|
||||||
if latestMigrations.Version.Equal(schemaVersion) &&
|
if latestMigrations.Version.Equal(schemaVersion) &&
|
||||||
version.MigratorCount != len(latestMigrations.MigrationFuncs) {
|
version.MigratorCount != len(latestMigrations.MigrationFuncs) {
|
||||||
err := runMigrations(latestMigrations.MigrationFuncs)
|
if err := runMigrations(latestMigrations.MigrationFuncs); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
newMigratorCount = len(latestMigrations.MigrationFuncs)
|
newMigratorCount = len(latestMigrations.MigrationFuncs)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// regular path when major/minor/patch versions differ
|
// regular path when major/minor/patch versions differ
|
||||||
for _, migration := range m.migrations {
|
for _, migration := range m.migrations {
|
||||||
if schemaVersion.LessThan(migration.Version) {
|
if schemaVersion.LessThan(migration.Version) {
|
||||||
|
|
||||||
log.Info().Msgf("migrating data to %s", migration.Version.String())
|
log.Info().Msgf("migrating data to %s", migration.Version.String())
|
||||||
err := runMigrations(migration.MigrationFuncs)
|
|
||||||
if err != nil {
|
if err := runMigrations(migration.MigrationFuncs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,16 +62,14 @@ func (m *Migrator) Migrate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.Always()
|
if err := m.Always(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return migrationError(err, "Always migrations returned error")
|
return migrationError(err, "Always migrations returned error")
|
||||||
}
|
}
|
||||||
|
|
||||||
version.SchemaVersion = portainer.APIVersion
|
version.SchemaVersion = portainer.APIVersion
|
||||||
version.MigratorCount = newMigratorCount
|
version.MigratorCount = newMigratorCount
|
||||||
|
|
||||||
err = m.versionService.UpdateVersion(version)
|
if err := m.versionService.UpdateVersion(version); err != nil {
|
||||||
if err != nil {
|
|
||||||
return migrationError(err, "StoreDBVersion")
|
return migrationError(err, "StoreDBVersion")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +96,7 @@ func (m *Migrator) NeedsMigration() bool {
|
|||||||
// In this particular instance we should log a fatal error
|
// In this particular instance we should log a fatal error
|
||||||
if m.CurrentDBEdition() != portainer.PortainerCE {
|
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/")
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/chisel/crypto"
|
"github.com/portainer/portainer/api/chisel/crypto"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,9 +38,11 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
|
|||||||
log.Info().Msg("ServerInfo object not found")
|
log.Info().Msg("ServerInfo object not found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Failed to read ServerInfo from DB")
|
Msg("Failed to read ServerInfo from DB")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,14 +52,15 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
|
|||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Failed to read ServerInfo from DB")
|
Msg("Failed to read ServerInfo from DB")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.fileService.StoreChiselPrivateKey(key)
|
if err := m.fileService.StoreChiselPrivateKey(key); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Failed to save Chisel private key to disk")
|
Msg("Failed to save Chisel private key to disk")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -64,14 +68,14 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverInfo.PrivateKeySeed = ""
|
serverInfo.PrivateKeySeed = ""
|
||||||
err = m.TunnelServerService.UpdateInfo(serverInfo)
|
if err := m.TunnelServerService.UpdateInfo(serverInfo); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
log.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("Failed to clean private key seed in DB")
|
Msg("Failed to clean private key seed in DB")
|
||||||
} else {
|
} else {
|
||||||
log.Info().Msg("Success to migrate private key seed to private key file")
|
log.Info().Msg("Success to migrate private key seed to private key file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,9 +88,8 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, edgeStack := range edgeStacks {
|
for _, edgeStack := range edgeStacks {
|
||||||
|
|
||||||
for environmentID, environmentStatus := range edgeStack.Status {
|
for environmentID, environmentStatus := range edgeStack.Status {
|
||||||
// skip if status is already updated
|
// Skip if status is already updated
|
||||||
if len(environmentStatus.Status) > 0 {
|
if len(environmentStatus.Status) > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -146,8 +149,7 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
|
|||||||
edgeStack.Status[environmentID] = environmentStatus
|
edgeStack.Status[environmentID] = environmentStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack)
|
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,3 +23,29 @@ func (migrator *Migrator) updateAppTemplatesVersionForDB110() error {
|
|||||||
|
|
||||||
return migrator.settingsService.UpdateSettings(settings)
|
return migrator.settingsService.UpdateSettings(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In PortainerCE the resource overcommit option should always be true across all endpoints
|
||||||
|
func (migrator *Migrator) updateResourceOverCommitToDB110() error {
|
||||||
|
log.Info().Msg("updating resource overcommit setting to true")
|
||||||
|
|
||||||
|
endpoints, err := migrator.endpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if endpoint.Type == portainer.KubernetesLocalEnvironment ||
|
||||||
|
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
|
||||||
|
endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
|
||||||
|
|
||||||
|
endpoint.Kubernetes.Configuration.EnableResourceOverCommit = true
|
||||||
|
|
||||||
|
err = migrator.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
32
api/datastore/migrator/migrate_dbversion111.go
Normal file
32
api/datastore/migrator/migrate_dbversion111.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package migrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (migrator *Migrator) cleanPendingActionsForDeletedEndpointsForDB111() error {
|
||||||
|
log.Info().Msg("cleaning up pending actions for deleted endpoints")
|
||||||
|
|
||||||
|
pendingActions, err := migrator.pendingActionsService.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints := make(map[portainer.EndpointID]struct{})
|
||||||
|
for _, action := range pendingActions {
|
||||||
|
endpoints[action.EndpointID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for endpointId := range endpoints {
|
||||||
|
_, err := migrator.endpointService.Endpoint(endpointId)
|
||||||
|
if dataservices.IsErrObjectNotFound(err) {
|
||||||
|
err := migrator.pendingActionsService.DeleteByEndpointID(endpointId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
33
api/datastore/migrator/migrate_dbversion130.go
Normal file
33
api/datastore/migrator/migrate_dbversion130.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package migrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/segmentio/encoding/json"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (migrator *Migrator) migratePendingActionsDataForDB130() error {
|
||||||
|
log.Info().Msg("Migrating pending actions data")
|
||||||
|
|
||||||
|
pendingActions, err := migrator.pendingActionsService.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pa := range pendingActions {
|
||||||
|
actionData, err := json.Marshal(pa.ActionData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pa.ActionData = string(actionData)
|
||||||
|
|
||||||
|
// Update the pending action
|
||||||
|
err = migrator.pendingActionsService.Update(pa.ID, &pa)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -32,8 +32,8 @@ func (m *Migrator) updateStacksToDB24() error {
|
|||||||
for idx := range stacks {
|
for idx := range stacks {
|
||||||
stack := &stacks[idx]
|
stack := &stacks[idx]
|
||||||
stack.Status = portainer.StackStatusActive
|
stack.Status = portainer.StackStatusActive
|
||||||
err := m.stackService.Update(stack.ID, stack)
|
|
||||||
if err != nil {
|
if err := m.stackService.Update(stack.ID, stack); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func (m *Migrator) updateDockerhubToDB32() error {
|
|||||||
migrated = true
|
migrated = true
|
||||||
} else {
|
} else {
|
||||||
// delete subsequent duplicates
|
// delete subsequent duplicates
|
||||||
m.registryService.Delete(portainer.RegistryID(r.ID))
|
m.registryService.Delete(r.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package migrator
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/Masterminds/semver"
|
"github.com/Masterminds/semver/v3"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/database/models"
|
"github.com/portainer/portainer/api/database/models"
|
||||||
"github.com/portainer/portainer/api/dataservices/dockerhub"
|
"github.com/portainer/portainer/api/dataservices/dockerhub"
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||||
"github.com/portainer/portainer/api/dataservices/extension"
|
"github.com/portainer/portainer/api/dataservices/extension"
|
||||||
"github.com/portainer/portainer/api/dataservices/fdoprofile"
|
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
||||||
"github.com/portainer/portainer/api/dataservices/registry"
|
"github.com/portainer/portainer/api/dataservices/registry"
|
||||||
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
|
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
|
||||||
"github.com/portainer/portainer/api/dataservices/role"
|
"github.com/portainer/portainer/api/dataservices/role"
|
||||||
@@ -40,7 +40,6 @@ type (
|
|||||||
endpointService *endpoint.Service
|
endpointService *endpoint.Service
|
||||||
endpointRelationService *endpointrelation.Service
|
endpointRelationService *endpointrelation.Service
|
||||||
extensionService *extension.Service
|
extensionService *extension.Service
|
||||||
fdoProfilesService *fdoprofile.Service
|
|
||||||
registryService *registry.Service
|
registryService *registry.Service
|
||||||
resourceControlService *resourcecontrol.Service
|
resourceControlService *resourcecontrol.Service
|
||||||
roleService *role.Service
|
roleService *role.Service
|
||||||
@@ -58,6 +57,7 @@ type (
|
|||||||
edgeStackService *edgestack.Service
|
edgeStackService *edgestack.Service
|
||||||
edgeJobService *edgejob.Service
|
edgeJobService *edgejob.Service
|
||||||
TunnelServerService *tunnelserver.Service
|
TunnelServerService *tunnelserver.Service
|
||||||
|
pendingActionsService *pendingactions.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigratorParameters represents the required parameters to create a new Migrator instance.
|
// MigratorParameters represents the required parameters to create a new Migrator instance.
|
||||||
@@ -67,7 +67,6 @@ type (
|
|||||||
EndpointService *endpoint.Service
|
EndpointService *endpoint.Service
|
||||||
EndpointRelationService *endpointrelation.Service
|
EndpointRelationService *endpointrelation.Service
|
||||||
ExtensionService *extension.Service
|
ExtensionService *extension.Service
|
||||||
FDOProfilesService *fdoprofile.Service
|
|
||||||
RegistryService *registry.Service
|
RegistryService *registry.Service
|
||||||
ResourceControlService *resourcecontrol.Service
|
ResourceControlService *resourcecontrol.Service
|
||||||
RoleService *role.Service
|
RoleService *role.Service
|
||||||
@@ -85,6 +84,7 @@ type (
|
|||||||
EdgeStackService *edgestack.Service
|
EdgeStackService *edgestack.Service
|
||||||
EdgeJobService *edgejob.Service
|
EdgeJobService *edgejob.Service
|
||||||
TunnelServerService *tunnelserver.Service
|
TunnelServerService *tunnelserver.Service
|
||||||
|
PendingActionsService *pendingactions.Service
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -96,7 +96,6 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
|
|||||||
endpointService: parameters.EndpointService,
|
endpointService: parameters.EndpointService,
|
||||||
endpointRelationService: parameters.EndpointRelationService,
|
endpointRelationService: parameters.EndpointRelationService,
|
||||||
extensionService: parameters.ExtensionService,
|
extensionService: parameters.ExtensionService,
|
||||||
fdoProfilesService: parameters.FDOProfilesService,
|
|
||||||
registryService: parameters.RegistryService,
|
registryService: parameters.RegistryService,
|
||||||
resourceControlService: parameters.ResourceControlService,
|
resourceControlService: parameters.ResourceControlService,
|
||||||
roleService: parameters.RoleService,
|
roleService: parameters.RoleService,
|
||||||
@@ -114,6 +113,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
|
|||||||
edgeStackService: parameters.EdgeStackService,
|
edgeStackService: parameters.EdgeStackService,
|
||||||
edgeJobService: parameters.EdgeJobService,
|
edgeJobService: parameters.EdgeJobService,
|
||||||
TunnelServerService: parameters.TunnelServerService,
|
TunnelServerService: parameters.TunnelServerService,
|
||||||
|
pendingActionsService: parameters.PendingActionsService,
|
||||||
}
|
}
|
||||||
|
|
||||||
migrator.initMigrations()
|
migrator.initMigrations()
|
||||||
@@ -228,12 +228,18 @@ func (m *Migrator) initMigrations() {
|
|||||||
m.migrateDockerDesktopExtensionSetting,
|
m.migrateDockerDesktopExtensionSetting,
|
||||||
m.updateEdgeStackStatusForDB100,
|
m.updateEdgeStackStatusForDB100,
|
||||||
)
|
)
|
||||||
|
|
||||||
m.addMigrations("2.20",
|
m.addMigrations("2.20",
|
||||||
m.updateAppTemplatesVersionForDB110,
|
m.updateAppTemplatesVersionForDB110,
|
||||||
|
m.updateResourceOverCommitToDB110,
|
||||||
|
)
|
||||||
|
m.addMigrations("2.20.2",
|
||||||
|
m.cleanPendingActionsForDeletedEndpointsForDB111,
|
||||||
|
)
|
||||||
|
m.addMigrations("2.22.0",
|
||||||
|
m.migratePendingActionsDataForDB130,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add new migrations below...
|
// Add new migrations above...
|
||||||
// One function per migration, each versions migration funcs in the same file.
|
// One function per migration, each versions migration funcs in the same file.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
98
api/datastore/pendingactions_test.go
Normal file
98
api/datastore/pendingactions_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package datastore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||||
|
"github.com/portainer/portainer/api/pendingactions/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cleanNAPWithOverridePolicies struct {
|
||||||
|
EndpointGroupID portainer.EndpointGroupID
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ConvertCleanNAPWithOverridePoliciesPayload(t *testing.T) {
|
||||||
|
t.Run("test ConvertCleanNAPWithOverridePoliciesPayload", func(t *testing.T) {
|
||||||
|
|
||||||
|
_, store := MustNewTestStore(t, true, false)
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
gid := portainer.EndpointGroupID(1)
|
||||||
|
|
||||||
|
testData := []struct {
|
||||||
|
Name string
|
||||||
|
PendingAction portainer.PendingAction
|
||||||
|
Expected any
|
||||||
|
Err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "test actiondata with EndpointGroupID 1",
|
||||||
|
PendingAction: handlers.NewCleanNAPWithOverridePolicies(
|
||||||
|
1,
|
||||||
|
&gid,
|
||||||
|
),
|
||||||
|
Expected: portainer.EndpointGroupID(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test actionData nil",
|
||||||
|
PendingAction: handlers.NewCleanNAPWithOverridePolicies(
|
||||||
|
2,
|
||||||
|
nil,
|
||||||
|
),
|
||||||
|
Expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "test actionData empty and expected error",
|
||||||
|
PendingAction: portainer.PendingAction{
|
||||||
|
EndpointID: 2,
|
||||||
|
Action: actions.CleanNAPWithOverridePolicies,
|
||||||
|
ActionData: "",
|
||||||
|
},
|
||||||
|
Expected: nil,
|
||||||
|
Err: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range testData {
|
||||||
|
err := store.PendingActions().Create(&d.PendingAction)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingActions, err := store.PendingActions().ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpointPendingAction := range pendingActions {
|
||||||
|
t.Run(d.Name, func(t *testing.T) {
|
||||||
|
if endpointPendingAction.Action == actions.CleanNAPWithOverridePolicies {
|
||||||
|
var payload cleanNAPWithOverridePolicies
|
||||||
|
|
||||||
|
err := endpointPendingAction.UnmarshallActionData(&payload)
|
||||||
|
|
||||||
|
if d.Err && err == nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Expected == nil && payload.EndpointGroupID != 0 {
|
||||||
|
t.Errorf("expected nil, got %d", payload.EndpointGroupID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Expected != nil {
|
||||||
|
expected := d.Expected.(portainer.EndpointGroupID)
|
||||||
|
if d.Expected != nil && expected != payload.EndpointGroupID {
|
||||||
|
t.Errorf("expected EndpointGroupID %d, got %d", expected, payload.EndpointGroupID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
store.PendingActions().Delete(d.PendingAction.ID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
184
api/datastore/postinit/migrate_post_init.go
Normal file
184
api/datastore/postinit/migrate_post_init.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package postinit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
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/internal/endpointutils"
|
||||||
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
|
"github.com/portainer/portainer/api/pendingactions/actions"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostInitMigrator struct {
|
||||||
|
kubeFactory *cli.ClientFactory
|
||||||
|
dockerFactory *dockerClient.ClientFactory
|
||||||
|
dataStore dataservices.DataStore
|
||||||
|
assetsPath string
|
||||||
|
kubernetesDeployer portainer.KubernetesDeployer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostInitMigrator(
|
||||||
|
kubeFactory *cli.ClientFactory,
|
||||||
|
dockerFactory *dockerClient.ClientFactory,
|
||||||
|
dataStore dataservices.DataStore,
|
||||||
|
assetsPath string,
|
||||||
|
kubernetesDeployer portainer.KubernetesDeployer,
|
||||||
|
) *PostInitMigrator {
|
||||||
|
return &PostInitMigrator{
|
||||||
|
kubeFactory: kubeFactory,
|
||||||
|
dockerFactory: dockerFactory,
|
||||||
|
dataStore: dataStore,
|
||||||
|
assetsPath: assetsPath,
|
||||||
|
kubernetesDeployer: kubernetesDeployer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostInitMigrate will run all post-init migrations, which require docker/kube clients for all edge or non-edge environments
|
||||||
|
func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
|
||||||
|
environments, err := postInitMigrator.dataStore.Endpoint().Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error getting environments")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, environment := range environments {
|
||||||
|
// edge environments will run after the server starts, in pending actions
|
||||||
|
if endpointutils.IsEdgeEndpoint(&environment) {
|
||||||
|
log.Info().Msgf("Adding pending action 'PostInitMigrateEnvironment' for environment %d", environment.ID)
|
||||||
|
err = postInitMigrator.createPostInitMigrationPendingAction(environment.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error creating pending action for environment %d", environment.ID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// non-edge environments will run before the server starts.
|
||||||
|
err = postInitMigrator.MigrateEnvironment(&environment)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error running post-init migrations for non-edge environment %d", environment.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to create a post init migration pending action. If it already exists, do nothing
|
||||||
|
// this function exists for readability, not reusability
|
||||||
|
// TODO: This should be moved into pending actions as part of the pending action migration
|
||||||
|
func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(environmentID portainer.EndpointID) error {
|
||||||
|
// If there are no pending actions for the given endpoint, create one
|
||||||
|
err := postInitMigrator.dataStore.PendingActions().Create(&portainer.PendingAction{
|
||||||
|
EndpointID: environmentID,
|
||||||
|
Action: actions.PostInitMigrateEnvironment,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error creating pending action for environment %d", environmentID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateEnvironment runs migrations on a single environment
|
||||||
|
func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endpoint) error {
|
||||||
|
log.Info().Msgf("Executing post init migration for environment %d", environment.ID)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case endpointutils.IsKubernetesEndpoint(environment):
|
||||||
|
// get the kubeclient for the environment, and skip all kube migrations if there's an error
|
||||||
|
kubeclient, err := migrator.kubeFactory.GetPrivilegedKubeClient(environment)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if one environment fails, it is logged and the next migration runs. The error is returned at the end and handled by pending actions
|
||||||
|
err = migrator.MigrateIngresses(*environment, kubeclient)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case endpointutils.IsDockerEndpoint(environment):
|
||||||
|
// get the docker client for the environment, and skip all docker migrations if there's an error
|
||||||
|
dockerClient, err := migrator.dockerFactory.CreateClient(environment, "", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error creating docker client for environment: %d", environment.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
migrator.MigrateGPUs(*environment, dockerClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (migrator *PostInitMigrator) MigrateIngresses(environment portainer.Endpoint, kubeclient *cli.KubeClient) error {
|
||||||
|
// Early exit if we do not need to migrate!
|
||||||
|
if !environment.PostInitMigrations.MigrateIngresses {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("Migrating ingresses for environment %d", environment.ID)
|
||||||
|
|
||||||
|
err := migrator.kubeFactory.MigrateEndpointIngresses(&environment, migrator.dataStore, kubeclient)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error migrating ingresses for environment %d", environment.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateGPUs 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) MigrateGPUs(e portainer.Endpoint, dockerClient *client.Client) error {
|
||||||
|
return migrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||||
|
environment, err := tx.Endpoint().Endpoint(e.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error getting environment %d", environment.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Early exit if we do not need to migrate!
|
||||||
|
if !environment.PostInitMigrations.MigrateGPUs {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Debug().Msgf("Migrating GPUs for environment %d", e.ID)
|
||||||
|
|
||||||
|
// get all containers
|
||||||
|
containers, err := dockerClient.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to list containers for environment %d", environment.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole environment
|
||||||
|
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.Error().Err(err).Msg("failed to inspect container")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
|
||||||
|
for _, deviceRequest := range deviceRequests {
|
||||||
|
if deviceRequest.Driver == "nvidia" {
|
||||||
|
environment.EnableGPUManagement = true
|
||||||
|
break containersLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the MigrateGPUs flag to false so we don't run this again
|
||||||
|
environment.PostInitMigrations.MigrateGPUs = false
|
||||||
|
err = tx.Endpoint().UpdateEndpoint(environment.ID, environment)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("Error updating EnableGPUManagement flag for environment %d", environment.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||||
"github.com/portainer/portainer/api/dataservices/extension"
|
"github.com/portainer/portainer/api/dataservices/extension"
|
||||||
"github.com/portainer/portainer/api/dataservices/fdoprofile"
|
|
||||||
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
|
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
|
||||||
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
||||||
"github.com/portainer/portainer/api/dataservices/registry"
|
"github.com/portainer/portainer/api/dataservices/registry"
|
||||||
@@ -55,7 +54,6 @@ type Store struct {
|
|||||||
EndpointService *endpoint.Service
|
EndpointService *endpoint.Service
|
||||||
EndpointRelationService *endpointrelation.Service
|
EndpointRelationService *endpointrelation.Service
|
||||||
ExtensionService *extension.Service
|
ExtensionService *extension.Service
|
||||||
FDOProfilesService *fdoprofile.Service
|
|
||||||
HelmUserRepositoryService *helmuserrepository.Service
|
HelmUserRepositoryService *helmuserrepository.Service
|
||||||
RegistryService *registry.Service
|
RegistryService *registry.Service
|
||||||
ResourceControlService *resourcecontrol.Service
|
ResourceControlService *resourcecontrol.Service
|
||||||
@@ -138,12 +136,6 @@ func (store *Store) initServices() error {
|
|||||||
}
|
}
|
||||||
store.ExtensionService = extensionService
|
store.ExtensionService = extensionService
|
||||||
|
|
||||||
fdoProfilesService, err := fdoprofile.NewService(store.connection)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
store.FDOProfilesService = fdoProfilesService
|
|
||||||
|
|
||||||
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
|
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -289,11 +281,6 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
|
|||||||
return store.EndpointRelationService
|
return store.EndpointRelationService
|
||||||
}
|
}
|
||||||
|
|
||||||
// FDOProfile gives access to the FDOProfile data management layer
|
|
||||||
func (store *Store) FDOProfile() dataservices.FDOProfileService {
|
|
||||||
return store.FDOProfilesService
|
|
||||||
}
|
|
||||||
|
|
||||||
// HelmUserRepository access the helm user repository settings
|
// HelmUserRepository access the helm user repository settings
|
||||||
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
|
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
|
||||||
return store.HelmUserRepositoryService
|
return store.HelmUserRepositoryService
|
||||||
@@ -398,11 +385,10 @@ type storeExport struct {
|
|||||||
User []portainer.User `json:"users,omitempty"`
|
User []portainer.User `json:"users,omitempty"`
|
||||||
Version models.Version `json:"version,omitempty"`
|
Version models.Version `json:"version,omitempty"`
|
||||||
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
|
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) Export(filename string) (err error) {
|
func (store *Store) Export(filename string) (err error) {
|
||||||
|
|
||||||
backup := storeExport{}
|
backup := storeExport{}
|
||||||
|
|
||||||
if c, err := store.CustomTemplate().ReadAll(); err != nil {
|
if c, err := store.CustomTemplate().ReadAll(); err != nil {
|
||||||
@@ -606,6 +592,7 @@ func (store *Store) Export(filename string) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(filename, b, 0600)
|
return os.WriteFile(filename, b, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +603,7 @@ func (store *Store) Import(filename string) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal([]byte(s), &backup)
|
err = json.Unmarshal(s, &backup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ func (tx *StoreTx) IsErrObjectNotFound(err error) bool {
|
|||||||
|
|
||||||
func (tx *StoreTx) CustomTemplate() dataservices.CustomTemplateService { return nil }
|
func (tx *StoreTx) CustomTemplate() dataservices.CustomTemplateService { return nil }
|
||||||
|
|
||||||
func (tx *StoreTx) PendingActions() dataservices.PendingActionsService { return nil }
|
func (tx *StoreTx) PendingActions() dataservices.PendingActionsService {
|
||||||
|
return tx.store.PendingActionsService.Tx(tx.tx)
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *StoreTx) EdgeGroup() dataservices.EdgeGroupService {
|
func (tx *StoreTx) EdgeGroup() dataservices.EdgeGroupService {
|
||||||
return tx.store.EdgeGroupService.Tx(tx.tx)
|
return tx.store.EdgeGroupService.Tx(tx.tx)
|
||||||
@@ -42,7 +44,6 @@ func (tx *StoreTx) EndpointRelation() dataservices.EndpointRelationService {
|
|||||||
return tx.store.EndpointRelationService.Tx(tx.tx)
|
return tx.store.EndpointRelationService.Tx(tx.tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *StoreTx) FDOProfile() dataservices.FDOProfileService { return nil }
|
|
||||||
func (tx *StoreTx) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
|
func (tx *StoreTx) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
|
||||||
|
|
||||||
func (tx *StoreTx) Registry() dataservices.RegistryService {
|
func (tx *StoreTx) Registry() dataservices.RegistryService {
|
||||||
@@ -68,7 +69,10 @@ func (tx *StoreTx) Snapshot() dataservices.SnapshotService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tx *StoreTx) SSLSettings() dataservices.SSLSettingsService { return nil }
|
func (tx *StoreTx) SSLSettings() dataservices.SSLSettingsService { return nil }
|
||||||
func (tx *StoreTx) Stack() dataservices.StackService { return nil }
|
|
||||||
|
func (tx *StoreTx) Stack() dataservices.StackService {
|
||||||
|
return tx.store.StackService.Tx(tx.tx)
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *StoreTx) Tag() dataservices.TagService {
|
func (tx *StoreTx) Tag() dataservices.TagService {
|
||||||
return tx.store.TagService.Tx(tx.tx)
|
return tx.store.TagService.Tx(tx.tx)
|
||||||
@@ -78,7 +82,10 @@ func (tx *StoreTx) TeamMembership() dataservices.TeamMembershipService {
|
|||||||
return tx.store.TeamMembershipService.Tx(tx.tx)
|
return tx.store.TeamMembershipService.Tx(tx.tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *StoreTx) Team() dataservices.TeamService { return nil }
|
func (tx *StoreTx) Team() dataservices.TeamService {
|
||||||
|
return tx.store.TeamService.Tx(tx.tx)
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *StoreTx) TunnelServer() dataservices.TunnelServerService { return nil }
|
func (tx *StoreTx) TunnelServer() dataservices.TunnelServerService { return nil }
|
||||||
|
|
||||||
func (tx *StoreTx) User() dataservices.UserService {
|
func (tx *StoreTx) User() dataservices.UserService {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"TenantID": ""
|
"TenantID": ""
|
||||||
},
|
},
|
||||||
"ComposeSyntaxMaxVersion": "",
|
"ComposeSyntaxMaxVersion": "",
|
||||||
|
"ContainerEngine": "",
|
||||||
"Edge": {
|
"Edge": {
|
||||||
"AsyncMode": false,
|
"AsyncMode": false,
|
||||||
"CommandInterval": 0,
|
"CommandInterval": 0,
|
||||||
@@ -583,7 +584,6 @@
|
|||||||
"AuthenticationMethod": 1,
|
"AuthenticationMethod": 1,
|
||||||
"BlackListedLabels": [],
|
"BlackListedLabels": [],
|
||||||
"Edge": {
|
"Edge": {
|
||||||
"AsyncMode": false,
|
|
||||||
"CommandInterval": 0,
|
"CommandInterval": 0,
|
||||||
"PingInterval": 0,
|
"PingInterval": 0,
|
||||||
"SnapshotInterval": 0
|
"SnapshotInterval": 0
|
||||||
@@ -602,7 +602,7 @@
|
|||||||
"RequiredPasswordLength": 12
|
"RequiredPasswordLength": 12
|
||||||
},
|
},
|
||||||
"KubeconfigExpiry": "0",
|
"KubeconfigExpiry": "0",
|
||||||
"KubectlShellImage": "portainer/kubectl-shell",
|
"KubectlShellImage": "portainer/kubectl-shell:2.22.0",
|
||||||
"LDAPSettings": {
|
"LDAPSettings": {
|
||||||
"AnonymousMode": true,
|
"AnonymousMode": true,
|
||||||
"AutoCreateUsers": true,
|
"AutoCreateUsers": true,
|
||||||
@@ -631,6 +631,7 @@
|
|||||||
"LogoURL": "",
|
"LogoURL": "",
|
||||||
"OAuthSettings": {
|
"OAuthSettings": {
|
||||||
"AccessTokenURI": "",
|
"AccessTokenURI": "",
|
||||||
|
"AuthStyle": 0,
|
||||||
"AuthorizationURI": "",
|
"AuthorizationURI": "",
|
||||||
"ClientID": "",
|
"ClientID": "",
|
||||||
"DefaultTeamID": 0,
|
"DefaultTeamID": 0,
|
||||||
@@ -643,17 +644,10 @@
|
|||||||
"Scopes": "",
|
"Scopes": "",
|
||||||
"UserIdentifier": ""
|
"UserIdentifier": ""
|
||||||
},
|
},
|
||||||
"ShowKomposeBuildOption": false,
|
|
||||||
"SnapshotInterval": "5m",
|
"SnapshotInterval": "5m",
|
||||||
"TemplatesURL": "",
|
"TemplatesURL": "",
|
||||||
"TrustOnFirstConnect": false,
|
"TrustOnFirstConnect": false,
|
||||||
"UserSessionTimeout": "8h",
|
"UserSessionTimeout": "8h",
|
||||||
"fdoConfiguration": {
|
|
||||||
"enabled": false,
|
|
||||||
"ownerPassword": "",
|
|
||||||
"ownerURL": "",
|
|
||||||
"ownerUsername": ""
|
|
||||||
},
|
|
||||||
"openAMTConfiguration": {
|
"openAMTConfiguration": {
|
||||||
"certFileContent": "",
|
"certFileContent": "",
|
||||||
"certFileName": "",
|
"certFileName": "",
|
||||||
@@ -669,6 +663,7 @@
|
|||||||
"snapshots": [
|
"snapshots": [
|
||||||
{
|
{
|
||||||
"Docker": {
|
"Docker": {
|
||||||
|
"ContainerCount": 0,
|
||||||
"DockerSnapshotRaw": {
|
"DockerSnapshotRaw": {
|
||||||
"Containers": null,
|
"Containers": null,
|
||||||
"Images": null,
|
"Images": null,
|
||||||
@@ -676,6 +671,7 @@
|
|||||||
"Architecture": "",
|
"Architecture": "",
|
||||||
"BridgeNfIp6tables": false,
|
"BridgeNfIp6tables": false,
|
||||||
"BridgeNfIptables": false,
|
"BridgeNfIptables": false,
|
||||||
|
"CDISpecDirs": null,
|
||||||
"CPUSet": false,
|
"CPUSet": false,
|
||||||
"CPUShares": false,
|
"CPUShares": false,
|
||||||
"CgroupDriver": "",
|
"CgroupDriver": "",
|
||||||
@@ -773,6 +769,7 @@
|
|||||||
"GpuUseList": null,
|
"GpuUseList": null,
|
||||||
"HealthyContainerCount": 0,
|
"HealthyContainerCount": 0,
|
||||||
"ImageCount": 9,
|
"ImageCount": 9,
|
||||||
|
"IsPodman": false,
|
||||||
"NodeCount": 0,
|
"NodeCount": 0,
|
||||||
"RunningContainerCount": 5,
|
"RunningContainerCount": 5,
|
||||||
"ServiceCount": 0,
|
"ServiceCount": 0,
|
||||||
@@ -807,7 +804,6 @@
|
|||||||
"FromAppTemplate": false,
|
"FromAppTemplate": false,
|
||||||
"GitConfig": null,
|
"GitConfig": null,
|
||||||
"Id": 2,
|
"Id": 2,
|
||||||
"IsComposeFormat": false,
|
|
||||||
"Name": "alpine",
|
"Name": "alpine",
|
||||||
"Namespace": "",
|
"Namespace": "",
|
||||||
"Option": null,
|
"Option": null,
|
||||||
@@ -830,7 +826,6 @@
|
|||||||
"FromAppTemplate": false,
|
"FromAppTemplate": false,
|
||||||
"GitConfig": null,
|
"GitConfig": null,
|
||||||
"Id": 5,
|
"Id": 5,
|
||||||
"IsComposeFormat": false,
|
|
||||||
"Name": "redis",
|
"Name": "redis",
|
||||||
"Namespace": "",
|
"Namespace": "",
|
||||||
"Option": null,
|
"Option": null,
|
||||||
@@ -853,7 +848,6 @@
|
|||||||
"FromAppTemplate": false,
|
"FromAppTemplate": false,
|
||||||
"GitConfig": null,
|
"GitConfig": null,
|
||||||
"Id": 6,
|
"Id": 6,
|
||||||
"IsComposeFormat": false,
|
|
||||||
"Name": "nginx",
|
"Name": "nginx",
|
||||||
"Namespace": "",
|
"Namespace": "",
|
||||||
"Option": null,
|
"Option": null,
|
||||||
@@ -903,6 +897,7 @@
|
|||||||
"color": ""
|
"color": ""
|
||||||
},
|
},
|
||||||
"TokenIssueAt": 0,
|
"TokenIssueAt": 0,
|
||||||
|
"UseCache": false,
|
||||||
"Username": "admin"
|
"Username": "admin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -932,10 +927,11 @@
|
|||||||
"color": ""
|
"color": ""
|
||||||
},
|
},
|
||||||
"TokenIssueAt": 0,
|
"TokenIssueAt": 0,
|
||||||
|
"UseCache": false,
|
||||||
"Username": "prabhat"
|
"Username": "prabhat"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version": {
|
"version": {
|
||||||
"VERSION": "{\"SchemaVersion\":\"2.20.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
"VERSION": "{\"SchemaVersion\":\"2.22.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,27 +52,24 @@ func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if init {
|
if init {
|
||||||
err = store.Init()
|
if err := store.Init(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return newStore, nil, nil, err
|
return newStore, nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if newStore {
|
if newStore {
|
||||||
// from MigrateData
|
// From MigrateData
|
||||||
v := models.Version{
|
v := models.Version{
|
||||||
SchemaVersion: portainer.APIVersion,
|
SchemaVersion: portainer.APIVersion,
|
||||||
Edition: int(portainer.PortainerCE),
|
Edition: int(portainer.PortainerCE),
|
||||||
}
|
}
|
||||||
err = store.VersionService.UpdateVersion(&v)
|
if err := store.VersionService.UpdateVersion(&v); err != nil {
|
||||||
if err != nil {
|
|
||||||
return newStore, nil, nil, err
|
return newStore, nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown := func() {
|
teardown := func() {
|
||||||
err := store.Close()
|
if err := store.Close(); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("")
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
api/demo/demo.go
118
api/demo/demo.go
@@ -1,118 +0,0 @@
|
|||||||
package demo
|
|
||||||
|
|
||||||
import (
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EnvironmentDetails struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Users []portainer.UserID `json:"users"`
|
|
||||||
Environments []portainer.EndpointID `json:"environments"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
details EnvironmentDetails
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService() *Service {
|
|
||||||
return &Service{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) Details() EnvironmentDetails {
|
|
||||||
return service.details
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) Init(store dataservices.DataStore, cryptoService portainer.CryptoService) error {
|
|
||||||
log.Info().Msg("starting demo environment")
|
|
||||||
|
|
||||||
isClean, err := isCleanStore(store)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, "failed checking if store is clean")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isClean {
|
|
||||||
return errors.New(" Demo environment can only be initialized on a clean database")
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := initDemoUser(store, cryptoService)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, "failed creating demo user")
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointIds, err := initDemoEndpoints(store)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, "failed creating demo endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = initDemoSettings(store)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, "failed updating demo settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
service.details = EnvironmentDetails{
|
|
||||||
Enabled: true,
|
|
||||||
Users: []portainer.UserID{id},
|
|
||||||
// endpoints 2,3 are created after deployment of portainer
|
|
||||||
Environments: endpointIds,
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isCleanStore(store dataservices.DataStore) (bool, error) {
|
|
||||||
endpoints, err := store.Endpoint().Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(endpoints) > 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
users, err := store.User().ReadAll()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(users) > 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) IsDemo() bool {
|
|
||||||
return service.details.Enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) IsDemoEnvironment(environmentID portainer.EndpointID) bool {
|
|
||||||
if !service.IsDemo() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, demoEndpointID := range service.details.Environments {
|
|
||||||
if environmentID == demoEndpointID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) IsDemoUser(userID portainer.UserID) bool {
|
|
||||||
if !service.IsDemo() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, demoUserID := range service.details.Users {
|
|
||||||
if userID == demoUserID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package demo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initDemoUser(
|
|
||||||
store dataservices.DataStore,
|
|
||||||
cryptoService portainer.CryptoService,
|
|
||||||
) (portainer.UserID, error) {
|
|
||||||
|
|
||||||
password, err := cryptoService.Hash("tryportainer")
|
|
||||||
if err != nil {
|
|
||||||
return 0, errors.WithMessage(err, "failed creating password hash")
|
|
||||||
}
|
|
||||||
|
|
||||||
admin := &portainer.User{
|
|
||||||
Username: "admin",
|
|
||||||
Password: password,
|
|
||||||
Role: portainer.AdministratorRole,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.User().Create(admin)
|
|
||||||
return admin.ID, errors.WithMessage(err, "failed creating user")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDemoEndpoints(store dataservices.DataStore) ([]portainer.EndpointID, error) {
|
|
||||||
localEndpointId, err := initDemoLocalEndpoint(store)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// second and third endpoints are going to be created with docker-compose as a part of the demo environment set up.
|
|
||||||
// ref: https://github.com/portainer/portainer-demo/blob/master/docker-compose.yml
|
|
||||||
return []portainer.EndpointID{localEndpointId, localEndpointId + 1, localEndpointId + 2}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDemoLocalEndpoint(store dataservices.DataStore) (portainer.EndpointID, error) {
|
|
||||||
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
|
|
||||||
localEndpoint := &portainer.Endpoint{
|
|
||||||
ID: id,
|
|
||||||
Name: "local",
|
|
||||||
URL: "unix:///var/run/docker.sock",
|
|
||||||
PublicURL: "demo.portainer.io",
|
|
||||||
Type: portainer.DockerEnvironment,
|
|
||||||
GroupID: portainer.EndpointGroupID(1),
|
|
||||||
TLSConfig: portainer.TLSConfiguration{
|
|
||||||
TLS: false,
|
|
||||||
},
|
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
|
||||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
|
||||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
|
||||||
TagIDs: []portainer.TagID{},
|
|
||||||
Status: portainer.EndpointStatusUp,
|
|
||||||
Snapshots: []portainer.DockerSnapshot{},
|
|
||||||
Kubernetes: portainer.KubernetesDefault(),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := store.Endpoint().Create(localEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return id, errors.WithMessage(err, "failed creating local endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = store.Snapshot().Create(&portainer.Snapshot{EndpointID: id})
|
|
||||||
if err != nil {
|
|
||||||
return id, errors.WithMessage(err, "failed creating snapshot")
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, errors.WithMessage(err, "failed creating local endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDemoSettings(
|
|
||||||
store dataservices.DataStore,
|
|
||||||
) error {
|
|
||||||
settings, err := store.Settings().Settings()
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithMessage(err, "failed fetching settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.EnableTelemetry = false
|
|
||||||
settings.LogoURL = ""
|
|
||||||
|
|
||||||
err = store.Settings().UpdateSettings(settings)
|
|
||||||
return errors.WithMessage(err, "failed updating settings")
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user