Compare commits
1073 Commits
debug-api-
...
refactor/E
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ab739adfd | ||
|
|
0ee6c5c6e9 | ||
|
|
1e2dbd7778 | ||
|
|
92dd6ed7bc | ||
|
|
f780207b82 | ||
|
|
86a848d927 | ||
|
|
ca4130b221 | ||
|
|
bb7c6077d5 | ||
|
|
bccab06abb | ||
|
|
531f88b947 | ||
|
|
2953848b9a | ||
|
|
c0ba221021 | ||
|
|
be85d34c4b | ||
|
|
7125ef81f3 | ||
|
|
1aae2e27f4 | ||
|
|
3237e1990c | ||
|
|
1e61f7e305 | ||
|
|
5586910e9d | ||
|
|
bb646162d1 | ||
|
|
cfe0d3092d | ||
|
|
6fde4195f8 | ||
|
|
36b8c849b3 | ||
|
|
0f6607e703 | ||
|
|
23295d2736 | ||
|
|
6290e9facc | ||
|
|
95424c322d | ||
|
|
a1e610a39a | ||
|
|
a27cc6c0e5 | ||
|
|
2b4cb1b7b4 | ||
|
|
26074437ca | ||
|
|
665a25e448 | ||
|
|
4a91e947ed | ||
|
|
d514eeec86 | ||
|
|
0ef4aad79a | ||
|
|
8355d449c5 | ||
|
|
fd7e8a629e | ||
|
|
7757bf7a84 | ||
|
|
5862aa5dd8 | ||
|
|
925a0d0a9a | ||
|
|
2a7a96f498 | ||
|
|
c472fe9c18 | ||
|
|
0eaf296e1b | ||
|
|
598b8d0f28 | ||
|
|
e1a3010bc7 | ||
|
|
2de4863532 | ||
|
|
8cf54cd0df | ||
|
|
1ef1953d7d | ||
|
|
5b033abaa4 | ||
|
|
5865f1ca77 | ||
|
|
f59573f306 | ||
|
|
1cecbd7177 | ||
|
|
acf9203580 | ||
|
|
9845518aa9 | ||
|
|
d7e83aad26 | ||
|
|
df47f3d8a8 | ||
|
|
d0ecf6c16b | ||
|
|
e400c4dfc6 | ||
|
|
721457b71d | ||
|
|
b19800681f | ||
|
|
6a4e44ee0a | ||
|
|
37ece734f0 | ||
|
|
bf79ef7d89 | ||
|
|
883ef2578f | ||
|
|
a585f34106 | ||
|
|
b128139b69 | ||
|
|
4c425a7af8 | ||
|
|
400d95c1a5 | ||
|
|
ca617e2ac9 | ||
|
|
4a90b8a3f7 | ||
|
|
43ad3face2 | ||
|
|
69e61be474 | ||
|
|
a4ea7a3709 | ||
|
|
c5ecf8a66d | ||
|
|
c2c0631495 | ||
|
|
4ff3cee72e | ||
|
|
c4e8251e52 | ||
|
|
21b00c267d | ||
|
|
86ec058347 | ||
|
|
9fac997300 | ||
|
|
704d70c99b | ||
|
|
e996d29d52 | ||
|
|
634326b5cd | ||
|
|
94379763f8 | ||
|
|
bb61723ba1 | ||
|
|
ff1f549590 | ||
|
|
b8f20a4f05 | ||
|
|
b5c5df798a | ||
|
|
88da28694c | ||
|
|
4f0f53b9aa | ||
|
|
03b9a9b65d | ||
|
|
fa755ffbca | ||
|
|
5ad83d0adb | ||
|
|
9fa097d45f | ||
|
|
7acd1080ad | ||
|
|
8c533bee67 | ||
|
|
fbec123595 | ||
|
|
09f60c3277 | ||
|
|
146681e1c7 | ||
|
|
615af4fdee | ||
|
|
0bcb57568c | ||
|
|
db61fb149b | ||
|
|
76b871d8a0 | ||
|
|
a725883cbc | ||
|
|
ecd54ab929 | ||
|
|
0e9902fee9 | ||
|
|
b93624fa1f | ||
|
|
91cfd2d0f2 | ||
|
|
2d94f020d0 | ||
|
|
01b9c64216 | ||
|
|
b93aced176 | ||
|
|
a216a1e960 | ||
|
|
020ecb740a | ||
|
|
de5c959e24 | ||
|
|
a9c6fa5ac2 | ||
|
|
39c431392e | ||
|
|
cbe23dc753 | ||
|
|
afaeddb887 | ||
|
|
39eed67fd7 | ||
|
|
64b227b2e1 | ||
|
|
979af5301e | ||
|
|
10014ae171 | ||
|
|
bf51f1b6c9 | ||
|
|
60ae6a63fc | ||
|
|
c752b98120 | ||
|
|
8b11e1678e | ||
|
|
eefb4c4287 | ||
|
|
29c1862754 | ||
|
|
801b7d43ee | ||
|
|
14d84c8025 | ||
|
|
d8c4dcbe72 | ||
|
|
31d68f8091 | ||
|
|
91088a5e0f | ||
|
|
e4ae4d5312 | ||
|
|
74515f102d | ||
|
|
b37120802e | ||
|
|
f5e09618f0 | ||
|
|
1a9a564553 | ||
|
|
8a432ebbf8 | ||
|
|
bc47061624 | ||
|
|
ceabb2884b | ||
|
|
f293ea41d3 | ||
|
|
c452de82b7 | ||
|
|
599d214e50 | ||
|
|
f02ede00b3 | ||
|
|
f1f46f4da1 | ||
|
|
c96e076871 | ||
|
|
89c1d0e337 | ||
|
|
8c16fbb8aa | ||
|
|
11571fd6ea | ||
|
|
dfc1a7b1d7 | ||
|
|
7cb6e3f66a | ||
|
|
4cc96b4b30 | ||
|
|
4c6bbe9a2f | ||
|
|
ea2f752a4f | ||
|
|
4c8af378af | ||
|
|
e91b4f5c83 | ||
|
|
2018529add | ||
|
|
58651810bd | ||
|
|
2363d23de0 | ||
|
|
2cd5d55b00 | ||
|
|
3d22cde096 | ||
|
|
cd89487c41 | ||
|
|
b12e1aade4 | ||
|
|
716c196682 | ||
|
|
7dc6a1559f | ||
|
|
806e1fdffa | ||
|
|
2eca5e05d4 | ||
|
|
223dfe89dd | ||
|
|
9f9cdf7d43 | ||
|
|
caf87bb0b5 | ||
|
|
f7dd73b0f7 | ||
|
|
933e764a13 | ||
|
|
e43973da1a | ||
|
|
a2388226ad | ||
|
|
0074bcc2ee | ||
|
|
a4dfeda4ae | ||
|
|
90759182db | ||
|
|
79822e1d3b | ||
|
|
9d3f13ac92 | ||
|
|
2ac70b1eb6 | ||
|
|
57fa044f2e | ||
|
|
3721c1478e | ||
|
|
424c98e256 | ||
|
|
2d69e93efa | ||
|
|
d7fc2046d7 | ||
|
|
4a331b71e1 | ||
|
|
834ab7c158 | ||
|
|
f799dd86c3 | ||
|
|
3233987a21 | ||
|
|
58c1a60fee | ||
|
|
8129e7590b | ||
|
|
73950f3603 | ||
|
|
c7756f3018 | ||
|
|
4f04fe54a7 | ||
|
|
c90a1be0e5 | ||
|
|
0c5a0eb3a0 | ||
|
|
ecf7f7ec14 | ||
|
|
e8e8329aab | ||
|
|
4c2906e89d | ||
|
|
fb2646b70c | ||
|
|
3cd0409184 | ||
|
|
1b041a029e | ||
|
|
69776b4863 | ||
|
|
2d05103fed | ||
|
|
6b5940e00e | ||
|
|
3a49dbf803 | ||
|
|
1cda08ca11 | ||
|
|
93bf630105 | ||
|
|
0ec7dfce69 | ||
|
|
eda07614ce | ||
|
|
b498cd657f | ||
|
|
61b568a738 | ||
|
|
d803d5f821 | ||
|
|
2347133438 | ||
|
|
96de026eba | ||
|
|
d340c4ea96 | ||
|
|
9567072ce0 | ||
|
|
d18b276e30 | ||
|
|
af77e33993 | ||
|
|
fdd79cece8 | ||
|
|
ac94d344df | ||
|
|
bcbdb01785 | ||
|
|
a2f734051c | ||
|
|
93866644c6 | ||
|
|
6242952141 | ||
|
|
b4dd5c5989 | ||
|
|
ef00350922 | ||
|
|
8acea44ee8 | ||
|
|
c193360741 | ||
|
|
4f34a78f7f | ||
|
|
f96e7ff434 | ||
|
|
e37e87971d | ||
|
|
5daef54456 | ||
|
|
db93e5880f | ||
|
|
881fa01eb2 | ||
|
|
14fa60f6e6 | ||
|
|
b58cd1e87e | ||
|
|
395d86dcd1 | ||
|
|
dbd476008b | ||
|
|
5a04338087 | ||
|
|
dc5f866a24 | ||
|
|
83551201fb | ||
|
|
e156243e43 | ||
|
|
1473cc208b | ||
|
|
d29b688eb9 | ||
|
|
077046030d | ||
|
|
5f3c0ff835 | ||
|
|
23e3cdb193 | ||
|
|
e6984c5787 | ||
|
|
0743f26ab8 | ||
|
|
8fa49d47f4 | ||
|
|
6ef53f0598 | ||
|
|
365316971b | ||
|
|
511adabce2 | ||
|
|
5b96136dd2 | ||
|
|
42fce1ec57 | ||
|
|
22f4c5d650 | ||
|
|
945798a662 | ||
|
|
6a29198c5c | ||
|
|
7197ca435a | ||
|
|
c3c2221437 | ||
|
|
d8fcce4c31 | ||
|
|
c86b76261a | ||
|
|
acc340b324 | ||
|
|
e0609e3d93 | ||
|
|
926ca19a1b | ||
|
|
c03b2ebbc1 | ||
|
|
e82c88317e | ||
|
|
59f543f442 | ||
|
|
f092b85f55 | ||
|
|
cfed481d6e | ||
|
|
5f6ddc2fad | ||
|
|
334eee0c8c | ||
|
|
550e235d59 | ||
|
|
9970fb3940 | ||
|
|
5d2723f4b9 | ||
|
|
a062a0bfbe | ||
|
|
706d66a76e | ||
|
|
2d22c4ff7d | ||
|
|
d77a0887a7 | ||
|
|
2383d243d5 | ||
|
|
426c132f97 | ||
|
|
1ff19f8604 | ||
|
|
14a581e86b | ||
|
|
ed279ba65b | ||
|
|
19eceaf37f | ||
|
|
1963d064a3 | ||
|
|
58d130ee37 | ||
|
|
98e6393274 | ||
|
|
745bbb7d79 | ||
|
|
757461d58b | ||
|
|
f20d3e72b9 | ||
|
|
731f3959c7 | ||
|
|
0f9a0e25f2 | ||
|
|
ae339a0047 | ||
|
|
77f8b9333a | ||
|
|
bbea0bc8a5 | ||
|
|
4b9c857d85 | ||
|
|
b5771df6a8 | ||
|
|
7ed8e9e167 | ||
|
|
80a3a5f16e | ||
|
|
3e654ff9b2 | ||
|
|
9b287f3020 | ||
|
|
a7404e00d1 | ||
|
|
3654109332 | ||
|
|
bf9dc8c2d0 | ||
|
|
67f8e8f3c2 | ||
|
|
56d6dfe02e | ||
|
|
861a9a5bbb | ||
|
|
1b470845b8 | ||
|
|
3c26aa8f34 | ||
|
|
de953da5a4 | ||
|
|
5356d1feeb | ||
|
|
7a8a20e0cc | ||
|
|
a7474188b9 | ||
|
|
6fe56f89c6 | ||
|
|
a98f480974 | ||
|
|
8ccac7c98f | ||
|
|
e0ce3671e8 | ||
|
|
62128d1069 | ||
|
|
a65ffe519a | ||
|
|
5ac1ea3df8 | ||
|
|
bf56bdb8f6 | ||
|
|
b00aa68c2b | ||
|
|
8c5edd2c97 | ||
|
|
c650868fe9 | ||
|
|
30a2bb0495 | ||
|
|
1a451823d9 | ||
|
|
feab2a757e | ||
|
|
17839aa473 | ||
|
|
fc1aec3bb8 | ||
|
|
d64e7eacfc | ||
|
|
7f805ac5be | ||
|
|
308a78db21 | ||
|
|
814fc9dfc0 | ||
|
|
3635df89dc | ||
|
|
30248eabb4 | ||
|
|
3636ac5c26 | ||
|
|
f6e8b25cf3 | ||
|
|
124e0bf9b9 | ||
|
|
45def82156 | ||
|
|
76bdf6f220 | ||
|
|
e142be399d | ||
|
|
13ba72ee07 | ||
|
|
f17a608dc7 | ||
|
|
6ee5cc6a56 | ||
|
|
44582732bb | ||
|
|
ea03024fbc | ||
|
|
795e6a5b3c | ||
|
|
2b17cb9104 | ||
|
|
347f66b1f1 | ||
|
|
40c387f4f4 | ||
|
|
15cbdb8af9 | ||
|
|
621a01ba3b | ||
|
|
37f382d286 | ||
|
|
77b49ae9c5 | ||
|
|
29648f517b | ||
|
|
8f42af49e8 | ||
|
|
0ab7987684 | ||
|
|
31d956dbcb | ||
|
|
2cc80e5e5d | ||
|
|
fb6e26a302 | ||
|
|
9cca299833 | ||
|
|
4c86be725d | ||
|
|
0669ad77d3 | ||
|
|
2bfc956f58 | ||
|
|
89194405ee | ||
|
|
5f0af62521 | ||
|
|
e3299eddd5 | ||
|
|
bdde278139 | ||
|
|
01ea9afe33 | ||
|
|
8345d1471e | ||
|
|
2a55d20eff | ||
|
|
7dca784ec6 | ||
|
|
37484566eb | ||
|
|
70710cfeb7 | ||
|
|
03712966e4 | ||
|
|
07100258cd | ||
|
|
4c6f5f961e | ||
|
|
77e1f5aa34 | ||
|
|
3baab6d695 | ||
|
|
d546ff269b | ||
|
|
60275dd31c | ||
|
|
07df4b1591 | ||
|
|
fd916bc8a2 | ||
|
|
769c8372fb | ||
|
|
d032119ebc | ||
|
|
ac47649631 | ||
|
|
8d6797dc9f | ||
|
|
197b0bcbde | ||
|
|
6918da2414 | ||
|
|
085381e6fc | ||
|
|
6074d1fcb5 | ||
|
|
96e5d44cc2 | ||
|
|
a45ef3d72e | ||
|
|
c819d4e7f7 | ||
|
|
bc6a667a6b | ||
|
|
7dcd6f9b9e | ||
|
|
c8d334e603 | ||
|
|
ab9b0c2147 | ||
|
|
6d659b4a2c | ||
|
|
defce0cf6d | ||
|
|
5f66020e42 | ||
|
|
b3e72ecaa0 | ||
|
|
b98c71f1ab | ||
|
|
f9a09301a8 | ||
|
|
2c247efd0f | ||
|
|
86d0e30eb7 | ||
|
|
69a91ff90a | ||
|
|
e0481f69b1 | ||
|
|
088262b6dc | ||
|
|
1b12ee9f01 | ||
|
|
5507b1e8c9 | ||
|
|
273a3f9a10 | ||
|
|
afe6cd6df0 | ||
|
|
95ac2cc4c3 | ||
|
|
9a8e95d017 | ||
|
|
631503fc1b | ||
|
|
23f3008500 | ||
|
|
89dd72b4ac | ||
|
|
5a375ff055 | ||
|
|
f081631808 | ||
|
|
e66dea44e3 | ||
|
|
392c7f74b8 | ||
|
|
1dba5e464b | ||
|
|
56dc2d1000 | ||
|
|
5c05ec489e | ||
|
|
cef9255161 | ||
|
|
0befdacc0e | ||
|
|
b2105f3614 | ||
|
|
58d66d3142 | ||
|
|
9f6702d0b8 | ||
|
|
44d69f3a3f | ||
|
|
e255bd710f | ||
|
|
d73622ed9c | ||
|
|
4753d52532 | ||
|
|
f9bbe000fb | ||
|
|
bfc610c192 | ||
|
|
51b9804fab | ||
|
|
e2168d21c7 | ||
|
|
2dddc1c6b9 | ||
|
|
c9253319d9 | ||
|
|
968fc98401 | ||
|
|
921e9cfc6e | ||
|
|
9b53960906 | ||
|
|
402a62a5e2 | ||
|
|
3470ea049a | ||
|
|
7fd263e8cc | ||
|
|
36c6d3f21b | ||
|
|
5f3dd0a64f | ||
|
|
42ca1287df | ||
|
|
2874a79279 | ||
|
|
8574dd2371 | ||
|
|
53eb5aa1ee | ||
|
|
eb8644330e | ||
|
|
8663de580a | ||
|
|
34298d96c5 | ||
|
|
9d103ffbeb | ||
|
|
5847c2b8ef | ||
|
|
a09fe7e10c | ||
|
|
5640cce4d6 | ||
|
|
00bbf4ac63 | ||
|
|
a748e15c16 | ||
|
|
cfdb9c126f | ||
|
|
851a3346a9 | ||
|
|
c9aae27b29 | ||
|
|
087848539f | ||
|
|
a74e389521 | ||
|
|
eff6ec9df9 | ||
|
|
8dec95c2cd | ||
|
|
5b02f636d7 | ||
|
|
ac458d0daa | ||
|
|
5b5dc320d5 | ||
|
|
d04747b309 | ||
|
|
07dd6bbe84 | ||
|
|
406ff8812c | ||
|
|
5942f4ff58 | ||
|
|
adf92ce5e0 | ||
|
|
fed3d14adf | ||
|
|
73db588080 | ||
|
|
6769326c8b | ||
|
|
e6d0e297dd | ||
|
|
0cd272211a | ||
|
|
6570f1f8eb | ||
|
|
1c180346e4 | ||
|
|
1d5d1bb12d | ||
|
|
0c27316034 | ||
|
|
d3bed3072b | ||
|
|
329e8bcad5 | ||
|
|
4bdf30c038 | ||
|
|
7793b98813 | ||
|
|
02de7b2715 | ||
|
|
9c0e0607a4 | ||
|
|
baf9c3db0a | ||
|
|
6c193a8a45 | ||
|
|
48a0f40621 | ||
|
|
4dc643acd9 | ||
|
|
1d42db93f1 | ||
|
|
33c3f8460c | ||
|
|
dd0d1737b0 | ||
|
|
3d28a6f877 | ||
|
|
2fc518f221 | ||
|
|
137ce37096 | ||
|
|
e529327851 | ||
|
|
3625ab6faa | ||
|
|
afb024d2a4 | ||
|
|
b2bc4b92d6 | ||
|
|
e5fd0c9595 | ||
|
|
649c1c9cee | ||
|
|
919a854d93 | ||
|
|
7fe0712b61 | ||
|
|
b4a6f6911c | ||
|
|
59d35d26d8 | ||
|
|
95558ed4ad | ||
|
|
e1b474d04f | ||
|
|
9732d1b5d8 | ||
|
|
701410d259 | ||
|
|
123754cee7 | ||
|
|
d75d2ba9ce | ||
|
|
046738c967 | ||
|
|
0436be7bc4 | ||
|
|
94d64997cc | ||
|
|
294d1668d4 | ||
|
|
4bd6618fb9 | ||
|
|
62197a67f7 | ||
|
|
c1dc1b49d1 | ||
|
|
b917e12b62 | ||
|
|
a8ccd2b153 | ||
|
|
68975620c5 | ||
|
|
67d3abcc9d | ||
|
|
90b0cb84f4 | ||
|
|
b22cdb3559 | ||
|
|
37896661d6 | ||
|
|
f38b8234d9 | ||
|
|
52e150fa29 | ||
|
|
929749c0da | ||
|
|
09bf5d03f4 | ||
|
|
ac6f52ab76 | ||
|
|
0ddcad66f3 | ||
|
|
930d9e5628 | ||
|
|
8936ae9b7a | ||
|
|
db9d87c918 | ||
|
|
b59a0ba823 | ||
|
|
2188005b48 | ||
|
|
a1528475ba | ||
|
|
5cbf52377d | ||
|
|
756ac034ec | ||
|
|
1008afd1fe | ||
|
|
563ead85cc | ||
|
|
eba5879ec8 | ||
|
|
b48aa1274d | ||
|
|
3e485c3152 | ||
|
|
dffd45c5f9 | ||
|
|
c1cc8bad77 | ||
|
|
8dcc5e4adb | ||
|
|
4558ce84cf | ||
|
|
adc87b8f8e | ||
|
|
ce8455953e | ||
|
|
a61b18dd93 | ||
|
|
d6a3fe23e9 | ||
|
|
cbaba43842 | ||
|
|
c173888b64 | ||
|
|
82e9e2a895 | ||
|
|
4fee359247 | ||
|
|
9cdc0da615 | ||
|
|
8fd0efa34f | ||
|
|
79bfd8f6fe | ||
|
|
2114c15f55 | ||
|
|
d2f6d1e415 | ||
|
|
241440a474 | ||
|
|
2e19f4ea6d | ||
|
|
95bc508462 | ||
|
|
d78b762f7b | ||
|
|
9dfac98a26 | ||
|
|
e26a607d28 | ||
|
|
6dc1841c14 | ||
|
|
c28be7aced | ||
|
|
dd01165224 | ||
|
|
d484a0eb64 | ||
|
|
fe8e834dbf | ||
|
|
0f0513c684 | ||
|
|
7006c17ce4 | ||
|
|
253a3a2b40 | ||
|
|
1e4c4e2616 | ||
|
|
75f40fe485 | ||
|
|
61e8e68c31 | ||
|
|
583346321e | ||
|
|
4cfa584c7c | ||
|
|
d012a4efc4 | ||
|
|
e0f3a8c0a2 | ||
|
|
bb48ab00cb | ||
|
|
eccc8131dd | ||
|
|
c21921a08d | ||
|
|
573e05d1c7 | ||
|
|
246e351817 | ||
|
|
6775c7b6ec | ||
|
|
881e99df53 | ||
|
|
78dcba614d | ||
|
|
30e23ea5b4 | ||
|
|
e1e81731b9 | ||
|
|
16377221f9 | ||
|
|
a0237852ef | ||
|
|
193e0c7d6f | ||
|
|
77c29ff87e | ||
|
|
2868da296a | ||
|
|
ff10588383 | ||
|
|
6b02d9a1e3 | ||
|
|
9f3d5185b0 | ||
|
|
f94147b07b | ||
|
|
49d02e0386 | ||
|
|
e82d0cfbdb | ||
|
|
c8051b68d4 | ||
|
|
37d4a80769 | ||
|
|
9ef2e27aae | ||
|
|
9e1f80cf37 | ||
|
|
459c95169a | ||
|
|
5048f08b5f | ||
|
|
e785d1572e | ||
|
|
95a4f83466 | ||
|
|
4edf232e41 | ||
|
|
903cf284e7 | ||
|
|
a550bfaedb | ||
|
|
446febb0f6 | ||
|
|
cb9fe2606c | ||
|
|
55211ef00e | ||
|
|
e48ceb15e9 | ||
|
|
1b12cc9f31 | ||
|
|
0365ed8e70 | ||
|
|
7624ff10ee | ||
|
|
535a26412f | ||
|
|
ee5600b6af | ||
|
|
3f51d077ac | ||
|
|
0219d41ba7 | ||
|
|
f3e2ccd487 | ||
|
|
368e6b2a44 | ||
|
|
1100a2bd28 | ||
|
|
16dc66f173 | ||
|
|
c1f94be9b2 | ||
|
|
58947fee69 | ||
|
|
0c995ae1c8 | ||
|
|
f6d6be90e4 | ||
|
|
5488389278 | ||
|
|
69f498c431 | ||
|
|
669327da7c | ||
|
|
191f8e17ee | ||
|
|
ae2bec4bd9 | ||
|
|
367f3dd6d4 | ||
|
|
8f1ac38963 | ||
|
|
7a6ff10268 | ||
|
|
fd91de3571 | ||
|
|
ab3a6f402e | ||
|
|
d3edb7ebd5 | ||
|
|
c23b8b2816 | ||
|
|
724f1f63b7 | ||
|
|
c6ae8467c0 | ||
|
|
56087bcbb3 | ||
|
|
315c1c7e1e | ||
|
|
819dc4d561 | ||
|
|
380a64d546 | ||
|
|
6429546462 | ||
|
|
ebfb71da05 | ||
|
|
ae0b9b1e30 | ||
|
|
e9de484c3e | ||
|
|
83a1ce9d2a | ||
|
|
66fd039933 | ||
|
|
1722257d68 | ||
|
|
7d8b037761 | ||
|
|
cd52e04a5a | ||
|
|
a0fa64781a | ||
|
|
43e3cb476b | ||
|
|
a1a88eb5e4 | ||
|
|
cb79dc18f8 | ||
|
|
e9384a6987 | ||
|
|
90a0e6fe35 | ||
|
|
e5f8466fb9 | ||
|
|
c3110a85b2 | ||
|
|
89eda13eb3 | ||
|
|
c96551e410 | ||
|
|
9f7d5ac842 | ||
|
|
648c1db437 | ||
|
|
4e20d70a99 | ||
|
|
3b2f0ff9eb | ||
|
|
fcb76f570e | ||
|
|
c384d834f5 | ||
|
|
45e2ed3d86 | ||
|
|
6e0f83b99e | ||
|
|
4fe2a7c750 | ||
|
|
f8b8d549fd | ||
|
|
1b0db4971f | ||
|
|
6063f368ea | ||
|
|
8ef584e41c | ||
|
|
ceaee4e175 | ||
|
|
1e21961e6a | ||
|
|
5777c18297 | ||
|
|
ef1d648c07 | ||
|
|
393d1fc91d | ||
|
|
f9fe440401 | ||
|
|
fad376b415 | ||
|
|
d3f094cb18 | ||
|
|
1950c4ca2b | ||
|
|
5232427a5b | ||
|
|
0fac1f85f7 | ||
|
|
70ce4e70d9 | ||
|
|
47f2490059 | ||
|
|
4d123895ea | ||
|
|
36e7981ab7 | ||
|
|
53025178ef | ||
|
|
f71fe87ba7 | ||
|
|
6078234d07 | ||
|
|
fa162cafc1 | ||
|
|
9ef5636718 | ||
|
|
7accdf704c | ||
|
|
d570aee554 | ||
|
|
a7d458f0bd | ||
|
|
1a9d793f2f | ||
|
|
0242c8e4ef | ||
|
|
6c4c958bf0 | ||
|
|
dd1662c8b8 | ||
|
|
fdfebcf731 | ||
|
|
9ce3e7d20d | ||
|
|
bf8b9463d3 | ||
|
|
9375e577b0 | ||
|
|
d95a67a567 | ||
|
|
160e210ffe | ||
|
|
c9eaad6237 | ||
|
|
2edff939ef | ||
|
|
13338c46bb | ||
|
|
ea05814af4 | ||
|
|
0fe2ddf535 | ||
|
|
9af9395b73 | ||
|
|
d9cc7eda51 | ||
|
|
77c3f9131b | ||
|
|
2b2580fb61 | ||
|
|
f870619fb6 | ||
|
|
602e42739e | ||
|
|
326a8abdc7 | ||
|
|
c0f3d0193d | ||
|
|
f9427c8fb2 | ||
|
|
9b02f575ef | ||
|
|
5b4f6098d8 | ||
|
|
ccaf2bedb7 | ||
|
|
88757d2617 | ||
|
|
d79586cf6a | ||
|
|
a9b1a9c194 | ||
|
|
eb5036b96f | ||
|
|
2f0dbf2ae1 | ||
|
|
c79be58700 | ||
|
|
d24e5ff71e | ||
|
|
6536d36c24 | ||
|
|
6174940ac2 | ||
|
|
4c98fcd7db | ||
|
|
ad8054ac1f | ||
|
|
a54c54ef24 | ||
|
|
27095ede22 | ||
|
|
e2789ab354 | ||
|
|
d4f4bb532f | ||
|
|
c6ab5d5717 | ||
|
|
234627f278 | ||
|
|
87214d48be | ||
|
|
a2a35a1851 | ||
|
|
11f0574ad3 | ||
|
|
9fbc6177a6 | ||
|
|
b91e06a60a | ||
|
|
ad3f4ff711 | ||
|
|
7edcfd6eab | ||
|
|
735b2063ea | ||
|
|
bce4d02dd2 | ||
|
|
e84126ec13 | ||
|
|
3a324acb0e | ||
|
|
c6f7427283 | ||
|
|
ace01eac9d | ||
|
|
8d304b78cb | ||
|
|
c17baa36ef | ||
|
|
8cbff097e4 | ||
|
|
294738cb0d | ||
|
|
69bc815acd | ||
|
|
fb62edefbc | ||
|
|
5e35ff8b8a | ||
|
|
20053b1f07 | ||
|
|
cc6c5d45b7 | ||
|
|
f480e0ccf6 | ||
|
|
d85149e328 | ||
|
|
cee241e77c | ||
|
|
8ec9515225 | ||
|
|
d4ffaaef2f | ||
|
|
eda8347091 | ||
|
|
4c23513a41 | ||
|
|
81d1f35bdc | ||
|
|
36c93c7f57 | ||
|
|
b67f404d8d | ||
|
|
95fb5a4baa | ||
|
|
dd372637cb | ||
|
|
c1a4856e9d | ||
|
|
92b7e64689 | ||
|
|
a750259a2c | ||
|
|
87accfce5d | ||
|
|
29f0daa7ea | ||
|
|
a247db7e93 | ||
|
|
1fbaf5fcbf | ||
|
|
c981e6ff7b | ||
|
|
ee1ee633d7 | ||
|
|
a7ab0a5662 | ||
|
|
bed4257194 | ||
|
|
5ee570e075 | ||
|
|
9666c21b8a | ||
|
|
5cf789a8e4 | ||
|
|
6a4a353b92 | ||
|
|
02355acfa8 | ||
|
|
04eb718f88 | ||
|
|
36888b5ad4 | ||
|
|
7bd971f838 | ||
|
|
c3ce4d8b53 | ||
|
|
fb3a31a4fd | ||
|
|
b6852b5e30 | ||
|
|
34e2178752 | ||
|
|
83a17de1c0 | ||
|
|
e5b27d7a57 | ||
|
|
fb14a85483 | ||
|
|
8d4cb5e16b | ||
|
|
ad8b8399c4 | ||
|
|
8ff2fa66b6 | ||
|
|
539948b5a6 | ||
|
|
bfe1cace77 | ||
|
|
7b806cf586 | ||
|
|
69a824c25b | ||
|
|
8d733ccc8c | ||
|
|
2574f223b4 | ||
|
|
f2d93654f5 | ||
|
|
5e74b90780 | ||
|
|
78ce176268 | ||
|
|
dfb398d091 | ||
|
|
4e9b3a8940 | ||
|
|
0141e55936 | ||
|
|
46fba176f0 | ||
|
|
441e265c32 | ||
|
|
d28030abea | ||
|
|
aa0f1221de | ||
|
|
305a949692 | ||
|
|
31d3fd730c | ||
|
|
a46002502f | ||
|
|
56fcc91e30 | ||
|
|
8a8058e4eb | ||
|
|
20a66fb10f | ||
|
|
628f822025 | ||
|
|
d8db8718bd | ||
|
|
5b40c79ea3 | ||
|
|
ae9025c1fb | ||
|
|
0014e39b61 | ||
|
|
5d1ea8ceb2 | ||
|
|
079478f191 | ||
|
|
65c050dc87 | ||
|
|
21fbd37bfb | ||
|
|
b28f635fb2 | ||
|
|
0580d3833a | ||
|
|
bff9bb7800 | ||
|
|
fb3d333453 | ||
|
|
2c25e1d48e | ||
|
|
5469392ec7 | ||
|
|
e1c7079c81 | ||
|
|
75c1b485ab | ||
|
|
03590d46e6 | ||
|
|
9dc6aa81cb | ||
|
|
d0b88d7e2f | ||
|
|
5343b965aa | ||
|
|
104c82c54e | ||
|
|
c0569a0752 | ||
|
|
ad86b6b11f | ||
|
|
ff32e87b97 | ||
|
|
1e78234f04 | ||
|
|
d0a9c046b3 | ||
|
|
c54bb255ba | ||
|
|
8843b7b0e8 | ||
|
|
a95d734c34 | ||
|
|
8262487401 | ||
|
|
57e53d1a21 | ||
|
|
e28a1491d4 | ||
|
|
9342ba9792 | ||
|
|
2552eb5e25 | ||
|
|
ddaf9dc885 | ||
|
|
11c778cfeb | ||
|
|
11dffdee9a | ||
|
|
d4d80ed8f7 | ||
|
|
0ba10b44ec | ||
|
|
0f617f7f87 | ||
|
|
423dd5e394 | ||
|
|
44737029a9 | ||
|
|
ce22544c60 | ||
|
|
9106e74e61 | ||
|
|
6c57ddb563 | ||
|
|
a2e1570162 | ||
|
|
ea60740d48 | ||
|
|
762c664948 | ||
|
|
d574a71cb1 | ||
|
|
bb066cd58c | ||
|
|
e779939ae1 | ||
|
|
aa830a0e58 | ||
|
|
52ac54f15c | ||
|
|
cc0ab75aca | ||
|
|
7e3347da2b | ||
|
|
87e9d7f8d4 | ||
|
|
6d3a33635d | ||
|
|
090268d7b6 | ||
|
|
698a91596e | ||
|
|
bb447bb02a | ||
|
|
5ffcbe8677 | ||
|
|
ac6296b86d | ||
|
|
3239a61bda | ||
|
|
2a43285593 | ||
|
|
36071837cb | ||
|
|
1ef713d80b | ||
|
|
82b848af0c | ||
|
|
b059641c80 | ||
|
|
728e885b9d | ||
|
|
3acefba069 | ||
|
|
9205f67791 | ||
|
|
6d95643a68 | ||
|
|
149c414d08 | ||
|
|
f8b4663e0a | ||
|
|
7b774c702d | ||
|
|
8045a15a50 | ||
|
|
9a18dd8162 | ||
|
|
70a7eefa22 | ||
|
|
3356d1abe2 | ||
|
|
7ee8dac832 | ||
|
|
5b3f099f4e | ||
|
|
5f5cb36df1 | ||
|
|
3645ff7459 | ||
|
|
9a92b97b7e | ||
|
|
005c48b1ad | ||
|
|
4fb1880ddc | ||
|
|
54145ce949 | ||
|
|
b040aa1e78 | ||
|
|
985eef6987 | ||
|
|
a5c3116b0c | ||
|
|
df381b6a33 | ||
|
|
9223c0226a | ||
|
|
314fdc850e | ||
|
|
43bbeed141 | ||
|
|
e07253bcef | ||
|
|
23b9baa059 | ||
|
|
05357ecce5 | ||
|
|
1a8fe82821 | ||
|
|
95f4db4f48 | ||
|
|
43600083a7 | ||
|
|
e6477b0b97 | ||
|
|
6aa7fdb4f2 | ||
|
|
4997e9c7be | ||
|
|
f0456cbf5f | ||
|
|
a0d349e0b3 | ||
|
|
f5e774c89d | ||
|
|
552d3f8a3e | ||
|
|
39f9173956 | ||
|
|
e4fc41fc94 | ||
|
|
ce7d234cba | ||
|
|
35701f5899 | ||
|
|
3d4d2b50ae | ||
|
|
0da4e3ae63 | ||
|
|
ad7055ee01 | ||
|
|
8076455423 | ||
|
|
23eca3ce80 | ||
|
|
4cc672f902 | ||
|
|
82fb5f7ac1 | ||
|
|
de59ea030a | ||
|
|
d9be6d1724 | ||
|
|
958a8e97e9 | ||
|
|
5fd202d629 | ||
|
|
768f1aa663 | ||
|
|
69caa1179f | ||
|
|
9a2cdc4a93 | ||
|
|
14a8b1d897 | ||
|
|
712207e69f | ||
|
|
8d46692d66 | ||
|
|
3241738775 | ||
|
|
ce840997bf | ||
|
|
88c4a43a19 | ||
|
|
b4acbfc9e1 | ||
|
|
8bf1c91bc9 | ||
|
|
a66fd78dc1 | ||
|
|
b004b33935 | ||
|
|
d32793e84e | ||
|
|
fd4b515350 | ||
|
|
0cd2a4558b | ||
|
|
89359a21ce | ||
|
|
69baa279d4 | ||
|
|
33861a834b | ||
|
|
dd4d126934 | ||
|
|
7275d23e4b | ||
|
|
d7306fb22e | ||
|
|
ebc0a8c772 | ||
|
|
f26e1fa21b | ||
|
|
6b27ba9121 | ||
|
|
975dc9c1da | ||
|
|
6fe26a52dd | ||
|
|
cd66e32912 | ||
|
|
81f8b88541 | ||
|
|
882051cc30 | ||
|
|
ed8f9b5931 | ||
|
|
e5e57978af | ||
|
|
75fef397d3 | ||
|
|
624490716e | ||
|
|
8eff32ebc7 | ||
|
|
cd19eb036b | ||
|
|
95f706aabe | ||
|
|
1551b02fde | ||
|
|
557f4773cf | ||
|
|
b84e1c8550 | ||
|
|
46e1a01625 | ||
|
|
7238372d8d | ||
|
|
00126cd08a | ||
|
|
58c44ad1ea | ||
|
|
84611a90a1 | ||
|
|
f78a6568a6 | ||
|
|
825269c119 | ||
|
|
60cd7b5527 | ||
|
|
767fabe0ce | ||
|
|
f86ba7b176 | ||
|
|
912250732a | ||
|
|
ae731b5496 | ||
|
|
92eaa02156 | ||
|
|
18252ab854 | ||
|
|
212400c283 | ||
|
|
8ed41de815 | ||
|
|
97a880e6c1 | ||
|
|
f39775752d | ||
|
|
6d6c70a98b | ||
|
|
461fc91446 | ||
|
|
8059cae8e7 | ||
|
|
41107191c3 | ||
|
|
cb6a5fa41d | ||
|
|
66799a53f4 | ||
|
|
892fdbf60d | ||
|
|
b6309682ef | ||
|
|
be11dfc231 | ||
|
|
12527aa820 | ||
|
|
0d0f9499eb | ||
|
|
60eab3e263 | ||
|
|
eb547162e9 | ||
|
|
0864c371e8 | ||
|
|
b90b1701e9 | ||
|
|
eb4ff12744 | ||
|
|
0522032515 | ||
|
|
bca1c6b9cf | ||
|
|
4195d93a16 | ||
|
|
e8a8b71daa | ||
|
|
aea62723c0 | ||
|
|
9b58c2e466 | ||
|
|
c41f7f8270 | ||
|
|
ac096dda46 | ||
|
|
e686d64011 | ||
|
|
1ccdb64938 | ||
|
|
71c0e8e661 | ||
|
|
c162e180e0 | ||
|
|
e806f74652 | ||
|
|
d52417c14f | ||
|
|
75d854e6ad | ||
|
|
0b2217a916 | ||
|
|
ca30efeca7 | ||
|
|
dc98850489 | ||
|
|
01dc9066b7 | ||
|
|
3aacaa7caf | ||
|
|
b031a30f62 | ||
|
|
12cddbd896 | ||
|
|
3791b7a16f | ||
|
|
d754532ab1 | ||
|
|
9a48ceaec1 | ||
|
|
1132c9ce87 | ||
|
|
668d526604 | ||
|
|
0e257c200f | ||
|
|
df05914fac | ||
|
|
0ffb84aaa6 | ||
|
|
b01180bb29 | ||
|
|
16f8b737f1 | ||
|
|
d9d1d6bfaa |
@@ -1,44 +0,0 @@
|
|||||||
version: "2"
|
|
||||||
checks:
|
|
||||||
argument-count:
|
|
||||||
enabled: false
|
|
||||||
complex-logic:
|
|
||||||
enabled: false
|
|
||||||
file-lines:
|
|
||||||
enabled: false
|
|
||||||
method-complexity:
|
|
||||||
enabled: false
|
|
||||||
method-count:
|
|
||||||
enabled: false
|
|
||||||
method-lines:
|
|
||||||
enabled: false
|
|
||||||
nested-control-flow:
|
|
||||||
enabled: false
|
|
||||||
return-statements:
|
|
||||||
enabled: false
|
|
||||||
similar-code:
|
|
||||||
enabled: false
|
|
||||||
identical-code:
|
|
||||||
enabled: false
|
|
||||||
plugins:
|
|
||||||
gofmt:
|
|
||||||
enabled: true
|
|
||||||
eslint:
|
|
||||||
enabled: true
|
|
||||||
channel: "eslint-5"
|
|
||||||
config:
|
|
||||||
config: .eslintrc.yml
|
|
||||||
exclude_patterns:
|
|
||||||
- assets/
|
|
||||||
- build/
|
|
||||||
- dist/
|
|
||||||
- distribution/
|
|
||||||
- node_modules
|
|
||||||
- test/
|
|
||||||
- webpack/
|
|
||||||
- gruntfile.js
|
|
||||||
- webpack.config.js
|
|
||||||
- api/
|
|
||||||
- "!app/kubernetes/**"
|
|
||||||
- .github/
|
|
||||||
- .tmp/
|
|
||||||
@@ -31,7 +31,12 @@ rules:
|
|||||||
[
|
[
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
|
pathGroups:
|
||||||
|
[
|
||||||
|
{ pattern: '@@/**', group: 'internal', position: 'after' },
|
||||||
|
{ pattern: '@/**', group: 'internal' },
|
||||||
|
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
|
||||||
|
],
|
||||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||||
pathGroupsExcludedImportTypes: ['internal'],
|
pathGroupsExcludedImportTypes: ['internal'],
|
||||||
},
|
},
|
||||||
@@ -41,6 +46,7 @@ settings:
|
|||||||
'import/resolver':
|
'import/resolver':
|
||||||
alias:
|
alias:
|
||||||
map:
|
map:
|
||||||
|
- ['@@', './app/react/components']
|
||||||
- ['@', './app']
|
- ['@', './app']
|
||||||
extensions: ['.js', '.ts', '.tsx']
|
extensions: ['.js', '.ts', '.tsx']
|
||||||
|
|
||||||
@@ -52,6 +58,7 @@ overrides:
|
|||||||
parser: '@typescript-eslint/parser'
|
parser: '@typescript-eslint/parser'
|
||||||
plugins:
|
plugins:
|
||||||
- '@typescript-eslint'
|
- '@typescript-eslint'
|
||||||
|
- 'regex'
|
||||||
extends:
|
extends:
|
||||||
- airbnb
|
- airbnb
|
||||||
- airbnb-typescript
|
- airbnb-typescript
|
||||||
@@ -68,11 +75,19 @@ overrides:
|
|||||||
version: 'detect'
|
version: 'detect'
|
||||||
rules:
|
rules:
|
||||||
import/order:
|
import/order:
|
||||||
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
|
[
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }],
|
||||||
|
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
no-plusplus: off
|
||||||
func-style: [error, 'declaration']
|
func-style: [error, 'declaration']
|
||||||
import/prefer-default-export: off
|
import/prefer-default-export: off
|
||||||
no-use-before-define: ['error', { functions: false }]
|
no-use-before-define: "off"
|
||||||
'@typescript-eslint/no-use-before-define': ['error', { functions: false }]
|
'@typescript-eslint/no-use-before-define': ['error', { functions: false, "allowNamedExports": true }]
|
||||||
no-shadow: 'off'
|
no-shadow: 'off'
|
||||||
'@typescript-eslint/no-shadow': off
|
'@typescript-eslint/no-shadow': off
|
||||||
jsx-a11y/no-autofocus: warn
|
jsx-a11y/no-autofocus: warn
|
||||||
@@ -85,11 +100,17 @@ overrides:
|
|||||||
'@typescript-eslint/explicit-module-boundary-types': off
|
'@typescript-eslint/explicit-module-boundary-types': off
|
||||||
'@typescript-eslint/no-unused-vars': 'error'
|
'@typescript-eslint/no-unused-vars': 'error'
|
||||||
'@typescript-eslint/no-explicit-any': 'error'
|
'@typescript-eslint/no-explicit-any': 'error'
|
||||||
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
|
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either', controlComponents: ['Input', 'Checkbox'] }]
|
||||||
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
|
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
|
||||||
'react/jsx-no-bind': off
|
'react/jsx-no-bind': off
|
||||||
'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' }]]
|
||||||
|
overrides: # allow props spreading for hoc files
|
||||||
|
- files:
|
||||||
|
- app/**/with*.ts{,x}
|
||||||
|
rules:
|
||||||
|
'react/jsx-props-no-spreading': off
|
||||||
- files:
|
- files:
|
||||||
- app/**/*.test.*
|
- app/**/*.test.*
|
||||||
extends:
|
extends:
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ cf5056d9c03b62d91a25c3b9127caac838695f98
|
|||||||
|
|
||||||
# prettier v2
|
# prettier v2
|
||||||
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
|
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
|
||||||
|
|
||||||
|
# tailwind prettier
|
||||||
|
58d66d3142950bb90a7d85511c034ac9fabba9ba
|
||||||
11
.github/DISCUSSION_TEMPLATE/help.yaml
vendored
Normal file
11
.github/DISCUSSION_TEMPLATE/help.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Before asking a question, make sure it hasn't been already asked and answered. You can search our [discussions](https://github.com/orgs/portainer/discussions) and [bug reports](https://github.com/portainer/portainer/issues) in GitHub. Also, be sure to check our [knowledge base](https://portal.portainer.io/knowledge) and [documentation](https://docs.portainer.io/) first.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Ask a Question!
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
38
.github/DISCUSSION_TEMPLATE/ideas.yaml
vendored
Normal file
38
.github/DISCUSSION_TEMPLATE/ideas.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Welcome!
|
||||||
|
|
||||||
|
Thanks for suggesting an idea for Portainer!
|
||||||
|
|
||||||
|
Before opening a new idea or feature request, make sure that we do not have any duplicates already open. You can ensure this by [searching this discussion cagetory](https://github.com/orgs/portainer/discussions/categories/ideas). If there is a duplicate, please add a comment to the existing idea instead.
|
||||||
|
|
||||||
|
Also, be sure to check our [knowledge base](https://portal.portainer.io/knowledge) and [documentation](https://docs.portainer.io) as they may point you toward a solution.
|
||||||
|
|
||||||
|
**DO NOT FILE DUPLICATE REQUESTS.**
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem? Please describe
|
||||||
|
description: Short list of what the feature request aims to address.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add any other context or screenshots about the feature request here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
54
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
54
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -1,54 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a bug report
|
|
||||||
title: ''
|
|
||||||
labels: bug/need-confirmation, kind/bug
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
Thanks for reporting a bug for Portainer !
|
|
||||||
|
|
||||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
|
||||||
|
|
||||||
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
|
|
||||||
|
|
||||||
Before opening a new issue, make sure that we do not have any duplicates
|
|
||||||
already open. You can ensure this by searching the issue list for this
|
|
||||||
repository. If there is a duplicate, please close your issue and add a comment
|
|
||||||
to the existing issue instead.
|
|
||||||
|
|
||||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
|
||||||
-->
|
|
||||||
|
|
||||||
**Bug description**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Portainer Logs**
|
|
||||||
Provide the logs of your Portainer container or Service.
|
|
||||||
You can see how [here](https://documentation.portainer.io/r/portainer-logs)
|
|
||||||
|
|
||||||
**Steps to reproduce the issue:**
|
|
||||||
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Technical details:**
|
|
||||||
|
|
||||||
- Portainer version:
|
|
||||||
- Docker version (managed by Portainer):
|
|
||||||
- Kubernetes version (managed by Portainer):
|
|
||||||
- Platform (windows/linux):
|
|
||||||
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
|
|
||||||
- Browser:
|
|
||||||
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
|
|
||||||
- Have you reviewed our technical documentation and knowledge base? Yes/No
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
164
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
164
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Create a report to help us improve.
|
||||||
|
labels: kind/bug,bug/need-confirmation
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Welcome!
|
||||||
|
|
||||||
|
The issue tracker is for reporting bugs. If you have an [idea for a new feature](https://github.com/orgs/portainer/discussions/categories/ideas) or a [general question about Portainer](https://github.com/orgs/portainer/discussions/categories/help) please post in our [GitHub Discussions](https://github.com/orgs/portainer/discussions).
|
||||||
|
|
||||||
|
You can also ask for help in our [community Slack channel](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA).
|
||||||
|
|
||||||
|
**DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS**.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: Before you start please confirm the following.
|
||||||
|
options:
|
||||||
|
- label: Yes, I've searched similar issues on [GitHub](https://github.com/portainer/portainer/issues).
|
||||||
|
required: true
|
||||||
|
- label: Yes, I've checked whether this issue is covered in the Portainer [documentation](https://docs.portainer.io) or [knowledge base](https://portal.portainer.io/knowledge).
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# About your issue
|
||||||
|
|
||||||
|
Tell us a bit about the issue you're having.
|
||||||
|
|
||||||
|
How to write a good bug report:
|
||||||
|
|
||||||
|
- Respect the issue template as much as possible.
|
||||||
|
- Summarize the issue so that we understand what is going wrong.
|
||||||
|
- Describe what you would have expected to have happened, and what actually happened instead.
|
||||||
|
- Provide easy to follow steps to reproduce the issue.
|
||||||
|
- Remain clear and concise.
|
||||||
|
- Format your messages to help the reader focus on what matters and understand the structure of your message, use [Markdown syntax](https://help.github.com/articles/github-flavored-markdown).
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Problem Description
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: A clear and concise description of what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
description: A clear and concise description of what actually happens.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Please be as detailed as possible when providing steps to reproduce.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Portainer logs or screenshots
|
||||||
|
description: Provide Portainer container logs or any screenshots related to the issue.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# About your environment
|
||||||
|
|
||||||
|
Tell us a bit about your Portainer environment.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Portainer version
|
||||||
|
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
|
||||||
|
options:
|
||||||
|
- '2.18.4'
|
||||||
|
- '2.18.3'
|
||||||
|
- '2.18.2'
|
||||||
|
- '2.18.1'
|
||||||
|
- '2.17.1'
|
||||||
|
- '2.17.0'
|
||||||
|
- '2.16.2'
|
||||||
|
- '2.16.1'
|
||||||
|
- '2.16.0'
|
||||||
|
- '2.15.1'
|
||||||
|
- '2.15.0'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Portainer Edition
|
||||||
|
multiple: false
|
||||||
|
options:
|
||||||
|
- 'Business Edition (BE/EE) with 5NF / 3NF license'
|
||||||
|
- 'Business Edition (BE/EE) with Home & Student license'
|
||||||
|
- 'Business Edition (BE/EE) with Starter license'
|
||||||
|
- 'Business Edition (BE/EE) with Professional or Enterprise license'
|
||||||
|
- 'Community Edition (CE)'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Platform and Version
|
||||||
|
description: |
|
||||||
|
Enter your container management platform (Docker | Swarm | Kubernetes) along with the version.
|
||||||
|
Example: Docker 24.0.3 | Docker Swarm 24.0.3 | Kubernetes 1.26
|
||||||
|
You can find our supported platforms [in our documentation](https://docs.portainer.io/start/requirements-and-prerequisites).
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: OS and Architecture
|
||||||
|
description: |
|
||||||
|
Enter your Operating System, Version and Architecture. Example: Ubuntu 22.04, AMD64 | Raspbian OS, ARM64
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Browser
|
||||||
|
description: |
|
||||||
|
Enter your browser and version. Example: Google Chrome 114.0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: What command did you use to deploy Portainer?
|
||||||
|
description: |
|
||||||
|
Example: `docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest`
|
||||||
|
If you deployed Portainer using a compose file or manifest you can provide this here as well.
|
||||||
|
render: bash
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Any additional information about your environment, the bug, or anything else you think might be helpful.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
12
.github/ISSUE_TEMPLATE/config.yml
vendored
12
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Portainer Business Edition - Get 5 nodes free
|
- name: Question
|
||||||
url: https://portainer.io/pricing/take5
|
url: https://github.com/orgs/portainer/discussions/new?category=help
|
||||||
about: Portainer Business Edition has more features, more support and you can now get 5 nodes free for as long as you want.
|
about: Ask us a question about Portainer usage or deployment.
|
||||||
|
- name: Idea or Feature Request
|
||||||
|
url: https://github.com/orgs/portainer/discussions/new?category=ideas
|
||||||
|
about: Suggest an idea or feature/enhancement that should be added in Portainer.
|
||||||
|
- name: Portainer Business Edition - Get 3 Nodes Free
|
||||||
|
url: https://www.portainer.io/take-3
|
||||||
|
about: Portainer Business Edition has more features, more support and you can now get 3 nodes free for as long as you want.
|
||||||
|
|||||||
4
.github/workflows/label-conflcts.yaml
vendored
4
.github/workflows/label-conflcts.yaml
vendored
@@ -11,5 +11,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
CONFLICT_LABEL_NAME: 'has conflicts'
|
CONFLICT_LABEL_NAME: 'has conflicts'
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
MAX_RETRIES: 5
|
MAX_RETRIES: 10
|
||||||
WAIT_MS: 5000
|
WAIT_MS: 60000
|
||||||
|
|||||||
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -21,10 +21,12 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.19.5
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Run linters
|
- name: Run linters
|
||||||
uses: wearerequired/lint-action@v1
|
uses: wearerequired/lint-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -36,3 +38,9 @@ jobs:
|
|||||||
gofmt_dir: api/
|
gofmt_dir: api/
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
uses: icrawl/action-tsc@v1
|
uses: icrawl/action-tsc@v1
|
||||||
|
- name: GolangCI-Lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: v1.52.2
|
||||||
|
working-directory: api
|
||||||
|
args: --timeout=10m -c .golangci.yaml
|
||||||
|
|||||||
129
.github/workflows/nightly-security-scan.yml
vendored
129
.github/workflows/nightly-security-scan.yml
vendored
@@ -2,21 +2,22 @@ name: Nightly Code Security Scan
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 8 * * *'
|
- cron: '0 20 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
client-dependencies:
|
client-dependencies:
|
||||||
name: Client dependency check
|
name: Client Dependency Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >- # only run for develop branch
|
if: >- # only run for develop branch
|
||||||
github.ref == 'refs/heads/develop'
|
github.ref == 'refs/heads/develop'
|
||||||
outputs:
|
outputs:
|
||||||
js: ${{ steps.set-matrix.outputs.js_result }}
|
js: ${{ steps.set-matrix.outputs.js_result }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- name: checkout repository
|
||||||
|
uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Run Snyk to check for vulnerabilities
|
- name: scan vulnerabilities by Snyk
|
||||||
uses: snyk/actions/node@master
|
uses: snyk/actions/node@master
|
||||||
continue-on-error: true # To make sure that artifact upload gets called
|
continue-on-error: true # To make sure that artifact upload gets called
|
||||||
env:
|
env:
|
||||||
@@ -24,141 +25,115 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
json: true
|
json: true
|
||||||
|
|
||||||
- name: Upload js security scan result as artifact
|
- name: upload scan result as develop artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: js-security-scan-develop-result
|
name: js-security-scan-develop-result
|
||||||
path: snyk.json
|
path: snyk.json
|
||||||
|
|
||||||
- name: Export scan result to html file
|
- name: develop scan report export to html
|
||||||
run: |
|
run: |
|
||||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
|
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
|
||||||
|
|
||||||
- name: Upload js result html file
|
- name: upload html file as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: html-js-result-${{github.run_id}}
|
name: html-js-result-${{github.run_id}}
|
||||||
path: js-result.html
|
path: js-result.html
|
||||||
|
|
||||||
- name: Analyse the js result
|
- name: analyse vulnerabilities
|
||||||
id: set-matrix
|
id: set-matrix
|
||||||
run: |
|
run: |
|
||||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||||
echo "::set-output name=js_result::${result}"
|
echo "js_result=${result}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
server-dependencies:
|
server-dependencies:
|
||||||
name: Server dependency check
|
name: Server Dependency Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >- # only run for develop branch
|
if: >- # only run for develop branch
|
||||||
github.ref == 'refs/heads/develop'
|
github.ref == 'refs/heads/develop'
|
||||||
outputs:
|
outputs:
|
||||||
go: ${{ steps.set-matrix.outputs.go_result }}
|
go: ${{ steps.set-matrix.outputs.go_result }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- name: checkout repository
|
||||||
|
uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Download go modules
|
- name: install Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.19.5'
|
||||||
|
|
||||||
|
- name: download Go modules
|
||||||
run: cd ./api && go get -t -v -d ./...
|
run: cd ./api && go get -t -v -d ./...
|
||||||
|
|
||||||
- name: Run Snyk to check for vulnerabilities
|
- name: scan vulnerabilities by Snyk
|
||||||
uses: snyk/actions/golang@master
|
|
||||||
continue-on-error: true # To make sure that artifact upload gets called
|
continue-on-error: true # To make sure that artifact upload gets called
|
||||||
env:
|
env:
|
||||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||||
with:
|
run: |
|
||||||
args: --file=./api/go.mod
|
yarn global add snyk
|
||||||
json: true
|
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||||
|
|
||||||
- name: Upload go security scan result as artifact
|
- name: upload scan result as develop artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: go-security-scan-develop-result
|
name: go-security-scan-develop-result
|
||||||
path: snyk.json
|
path: snyk.json
|
||||||
|
|
||||||
- name: Export scan result to html file
|
- name: develop scan report export to html
|
||||||
run: |
|
run: |
|
||||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
|
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
|
||||||
|
|
||||||
- name: Upload go result html file
|
- name: upload html file as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: html-go-result-${{github.run_id}}
|
name: html-go-result-${{github.run_id}}
|
||||||
path: go-result.html
|
path: go-result.html
|
||||||
|
|
||||||
- name: Analyse the go result
|
- name: analyse vulnerabilities
|
||||||
id: set-matrix
|
id: set-matrix
|
||||||
run: |
|
run: |
|
||||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
|
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
|
||||||
echo "::set-output name=go_result::${result}"
|
echo "go_result=${result}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
image-vulnerability:
|
image-vulnerability:
|
||||||
name: Build docker image and Image vulnerability check
|
name: Image Vulnerability Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.ref == 'refs/heads/develop'
|
github.ref == 'refs/heads/develop'
|
||||||
outputs:
|
outputs:
|
||||||
image: ${{ steps.set-matrix.outputs.image_result }}
|
image: ${{ steps.set-matrix.outputs.image_result }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: scan vulnerabilities by Trivy
|
||||||
uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Use golang 1.18
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.18'
|
|
||||||
|
|
||||||
- name: Use Node.js 12.x
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12.x
|
|
||||||
|
|
||||||
- name: Install packages and build
|
|
||||||
run: yarn install && yarn build
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: build/linux/Dockerfile
|
|
||||||
tags: trivy-portainer:${{ github.sha }}
|
|
||||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
|
||||||
|
|
||||||
- name: Load docker image
|
|
||||||
run: |
|
|
||||||
docker load --input /tmp/trivy-portainer-image.tar
|
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
|
||||||
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 trivy-portainer:${{ github.sha }}
|
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop
|
||||||
|
|
||||||
- name: Upload image security scan result as artifact
|
- name: upload image security scan result as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: image-security-scan-develop-result
|
name: image-security-scan-develop-result
|
||||||
path: image-trivy.json
|
path: image-trivy.json
|
||||||
|
|
||||||
- name: Export scan result to html file
|
- name: develop scan report export to html
|
||||||
run: |
|
run: |
|
||||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
|
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result")
|
||||||
|
|
||||||
- name: Upload go result html file
|
- name: upload html file as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: html-image-result-${{github.run_id}}
|
name: html-image-result-${{github.run_id}}
|
||||||
path: image-result.html
|
path: image-result.html
|
||||||
|
|
||||||
- name: Analyse the trivy result
|
- name: analyse vulnerabilities
|
||||||
id: set-matrix
|
id: set-matrix
|
||||||
run: |
|
run: |
|
||||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
|
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
|
||||||
echo "::set-output name=image_result::${result}"
|
echo "image_result=${result}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
result-analysis:
|
result-analysis:
|
||||||
name: Analyse scan result
|
name: Analyse Scan Results
|
||||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
@@ -169,21 +144,21 @@ jobs:
|
|||||||
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
|
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
|
||||||
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
|
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
|
||||||
steps:
|
steps:
|
||||||
- name: Display the results of js, go and image
|
- name: display the results of js, Go, and image scan
|
||||||
run: |
|
run: |
|
||||||
echo ${{ matrix.js.status }}
|
echo "${{ matrix.js.status }}"
|
||||||
echo ${{ matrix.go.status }}
|
echo "${{ matrix.go.status }}"
|
||||||
echo ${{ matrix.image.status }}
|
echo "${{ matrix.image.status }}"
|
||||||
echo ${{ matrix.js.summary }}
|
echo "${{ matrix.js.summary }}"
|
||||||
echo ${{ matrix.go.summary }}
|
echo "${{ matrix.go.summary }}"
|
||||||
echo ${{ matrix.image.summary }}
|
echo "${{ matrix.image.summary }}"
|
||||||
|
|
||||||
- name: Send Slack message
|
- name: send message to Slack
|
||||||
if: >-
|
if: >-
|
||||||
matrix.js.status == 'failure' ||
|
matrix.js.status == 'failure' ||
|
||||||
matrix.go.status == 'failure' ||
|
matrix.go.status == 'failure' ||
|
||||||
matrix.image.status == 'failure'
|
matrix.image.status == 'failure'
|
||||||
uses: slackapi/slack-github-action@v1.18.0
|
uses: slackapi/slack-github-action@v1.23.0
|
||||||
with:
|
with:
|
||||||
payload: |
|
payload: |
|
||||||
{
|
{
|
||||||
|
|||||||
127
.github/workflows/pr-security.yml
vendored
127
.github/workflows/pr-security.yml
vendored
@@ -12,10 +12,11 @@ on:
|
|||||||
- 'build/linux/Dockerfile'
|
- 'build/linux/Dockerfile'
|
||||||
- 'build/linux/alpine.Dockerfile'
|
- 'build/linux/alpine.Dockerfile'
|
||||||
- 'build/windows/Dockerfile'
|
- 'build/windows/Dockerfile'
|
||||||
|
- '.github/workflows/pr-security.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
client-dependencies:
|
client-dependencies:
|
||||||
name: Client dependency check
|
name: Client Dependency Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request &&
|
github.event.pull_request &&
|
||||||
@@ -23,9 +24,10 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- name: checkout repository
|
||||||
|
uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Run Snyk to check for vulnerabilities
|
- name: scan vulnerabilities by Snyk
|
||||||
uses: snyk/actions/node@master
|
uses: snyk/actions/node@master
|
||||||
continue-on-error: true # To make sure that artifact upload gets called
|
continue-on-error: true # To make sure that artifact upload gets called
|
||||||
env:
|
env:
|
||||||
@@ -33,13 +35,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
json: true
|
json: true
|
||||||
|
|
||||||
- name: Upload js security scan result as artifact
|
- name: upload scan result as pull-request artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: js-security-scan-feat-result
|
name: js-security-scan-feat-result
|
||||||
path: snyk.json
|
path: snyk.json
|
||||||
|
|
||||||
- name: Download artifacts from develop branch
|
- name: download artifacts from develop branch built by nightly scan
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -51,24 +53,24 @@ jobs:
|
|||||||
echo "null" > ./js-snyk-develop.json
|
echo "null" > ./js-snyk-develop.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Export scan result to html file
|
- name: pr vs develop scan report comparison export to html
|
||||||
run: |
|
run: |
|
||||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result")
|
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=table --export --export-filename="/data/js-result")
|
||||||
|
|
||||||
- name: Upload js result html file
|
- name: upload html file as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: html-js-result-compare-to-develop-${{github.run_id}}
|
name: html-js-result-compare-to-develop-${{github.run_id}}
|
||||||
path: js-result.html
|
path: js-result.html
|
||||||
|
|
||||||
- name: Analyse the js diff result
|
- name: analyse different vulnerabilities against develop branch
|
||||||
id: set-diff-matrix
|
id: set-diff-matrix
|
||||||
run: |
|
run: |
|
||||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
|
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=matrix)
|
||||||
echo "::set-output name=js_diff_result::${result}"
|
echo "js_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
server-dependencies:
|
server-dependencies:
|
||||||
name: Server dependency check
|
name: Server Dependency Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request &&
|
github.event.pull_request &&
|
||||||
@@ -76,27 +78,32 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- name: checkout repository
|
||||||
|
uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Download go modules
|
- name: install Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.19.5'
|
||||||
|
|
||||||
|
- name: download Go modules
|
||||||
run: cd ./api && go get -t -v -d ./...
|
run: cd ./api && go get -t -v -d ./...
|
||||||
|
|
||||||
- name: Run Snyk to check for vulnerabilities
|
- name: scan vulnerabilities by Snyk
|
||||||
uses: snyk/actions/golang@master
|
|
||||||
continue-on-error: true # To make sure that artifact upload gets called
|
continue-on-error: true # To make sure that artifact upload gets called
|
||||||
env:
|
env:
|
||||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||||
with:
|
run: |
|
||||||
args: --file=./api/go.mod
|
yarn global add snyk
|
||||||
json: true
|
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
|
||||||
|
|
||||||
- name: Upload go security scan result as artifact
|
- name: upload scan result as pull-request artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: go-security-scan-feature-result
|
name: go-security-scan-feature-result
|
||||||
path: snyk.json
|
path: snyk.json
|
||||||
|
|
||||||
- name: Download artifacts from develop branch
|
- name: download artifacts from develop branch built by nightly scan
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -108,24 +115,24 @@ jobs:
|
|||||||
echo "null" > ./go-snyk-develop.json
|
echo "null" > ./go-snyk-develop.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Export scan result to html file
|
- name: pr vs develop scan report comparison export to html
|
||||||
run: |
|
run: |
|
||||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result")
|
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=table --export --export-filename="/data/go-result")
|
||||||
|
|
||||||
- name: Upload go result html file
|
- name: upload html file as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: html-go-result-compare-to-develop-${{github.run_id}}
|
name: html-go-result-compare-to-develop-${{github.run_id}}
|
||||||
path: go-result.html
|
path: go-result.html
|
||||||
|
|
||||||
- name: Analyse the go diff result
|
- name: analyse different vulnerabilities against develop branch
|
||||||
id: set-diff-matrix
|
id: set-diff-matrix
|
||||||
run: |
|
run: |
|
||||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
|
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=matrix)
|
||||||
echo "::set-output name=go_diff_result::${result}"
|
echo "go_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
image-vulnerability:
|
image-vulnerability:
|
||||||
name: Build docker image and Image vulnerability check
|
name: Image Vulnerability Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
github.event.pull_request &&
|
github.event.pull_request &&
|
||||||
@@ -133,50 +140,53 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: checkout code
|
||||||
uses: actions/checkout@master
|
uses: actions/checkout@master
|
||||||
|
|
||||||
- name: Use golang 1.18
|
- name: install Go 1.19.5
|
||||||
uses: actions/setup-go@v3
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: '1.18'
|
go-version: '1.19.5'
|
||||||
|
|
||||||
- name: Use Node.js 12.x
|
- name: install Node.js 18.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Install packages and build
|
- name: Install packages
|
||||||
run: yarn install && yarn build
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: build
|
||||||
uses: docker/setup-buildx-action@v1
|
run: make build-all
|
||||||
|
|
||||||
- name: Build and push
|
- name: set up docker buildx
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: build and compress image
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: build/linux/Dockerfile
|
file: build/linux/Dockerfile
|
||||||
tags: trivy-portainer:${{ github.sha }}
|
tags: trivy-portainer:${{ github.sha }}
|
||||||
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
|
||||||
|
|
||||||
- name: Load docker image
|
- name: load docker image
|
||||||
run: |
|
run: |
|
||||||
docker load --input /tmp/trivy-portainer-image.tar
|
docker load --input /tmp/trivy-portainer-image.tar
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
- name: scan vulnerabilities by Trivy
|
||||||
uses: docker://docker.io/aquasec/trivy:latest
|
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 trivy-portainer:${{ github.sha }}
|
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
|
||||||
|
|
||||||
- name: Upload image security scan result as artifact
|
- name: upload image security scan result as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: image-security-scan-feature-result
|
name: image-security-scan-feature-result
|
||||||
path: image-trivy.json
|
path: image-trivy.json
|
||||||
|
|
||||||
- name: Download artifacts from develop branch
|
- name: download artifacts from develop branch built by nightly scan
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -188,24 +198,24 @@ jobs:
|
|||||||
echo "null" > ./image-trivy-develop.json
|
echo "null" > ./image-trivy-develop.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Export scan result to html file
|
- name: pr vs develop scan report comparison export to html
|
||||||
run: |
|
run: |
|
||||||
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="/data/image-trivy-develop.json" -output-type=table -export -export-filename="/data/image-result")
|
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-result")
|
||||||
|
|
||||||
- name: Upload image result html file
|
- name: upload html file as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: html-image-result-compare-to-develop-${{github.run_id}}
|
name: html-image-result-compare-to-develop-${{github.run_id}}
|
||||||
path: image-result.html
|
path: image-result.html
|
||||||
|
|
||||||
- name: Analyse the image diff result
|
- name: analyse different vulnerabilities against develop branch
|
||||||
id: set-diff-matrix
|
id: set-diff-matrix
|
||||||
run: |
|
run: |
|
||||||
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
|
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=matrix)
|
||||||
echo "::set-output name=image_diff_result::${result}"
|
echo "image_diff_result=${result}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
result-analysis:
|
result-analysis:
|
||||||
name: Analyse scan result compared to develop
|
name: Analyse Scan Result Against develop Branch
|
||||||
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
needs: [client-dependencies, server-dependencies, image-vulnerability]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: >-
|
if: >-
|
||||||
@@ -217,17 +227,16 @@ jobs:
|
|||||||
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
|
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
|
||||||
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
|
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
|
||||||
steps:
|
steps:
|
||||||
|
- name: check job status of diff result
|
||||||
- name: Check job status of diff result
|
|
||||||
if: >-
|
if: >-
|
||||||
matrix.jsdiff.status == 'failure' ||
|
matrix.jsdiff.status == 'failure' ||
|
||||||
matrix.godiff.status == 'failure' ||
|
matrix.godiff.status == 'failure' ||
|
||||||
matrix.imagediff.status == 'failure'
|
matrix.imagediff.status == 'failure'
|
||||||
run: |
|
run: |
|
||||||
echo ${{ matrix.jsdiff.status }}
|
echo "${{ matrix.jsdiff.status }}"
|
||||||
echo ${{ matrix.godiff.status }}
|
echo "${{ matrix.godiff.status }}"
|
||||||
echo ${{ matrix.imagediff.status }}
|
echo "${{ matrix.imagediff.status }}"
|
||||||
echo ${{ matrix.jsdiff.summary }}
|
echo "${{ matrix.jsdiff.summary }}"
|
||||||
echo ${{ matrix.godiff.summary }}
|
echo "${{ matrix.godiff.summary }}"
|
||||||
echo ${{ matrix.imagediff.summary }}
|
echo "${{ matrix.imagediff.summary }}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@@ -2,6 +2,7 @@ name: Close Stale Issues
|
|||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 12 * * *'
|
- cron: '0 12 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -9,7 +10,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v4.0.0
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/test.yaml
vendored
26
.github/workflows/test.yaml
vendored
@@ -8,22 +8,18 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '14'
|
node-version: '18'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: yarn test:client
|
run: make test-client ARGS="--maxWorkers=2"
|
||||||
# test-server:
|
test-server:
|
||||||
# runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# env:
|
steps:
|
||||||
# GOPRIVATE: "github.com/portainer"
|
- uses: actions/checkout@v3
|
||||||
# steps:
|
- uses: actions/setup-go@v3
|
||||||
# - uses: actions/checkout@v3
|
with:
|
||||||
# - uses: actions/setup-go@v3
|
go-version: 1.19.5
|
||||||
# with:
|
- name: Run tests
|
||||||
# go-version: '1.18'
|
run: make test-server
|
||||||
# - name: Run tests
|
|
||||||
# run: |
|
|
||||||
# cd api
|
|
||||||
# go test ./...
|
|
||||||
|
|||||||
29
.github/workflows/validate-openapi-spec.yaml
vendored
Normal file
29
.github/workflows/validate-openapi-spec.yaml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Validate OpenAPI specs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
- 'release/*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
openapi-spec:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.18'
|
||||||
|
|
||||||
|
- name: Download golang modules
|
||||||
|
run: cd ./api && go get -t -v -d ./...
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'yarn'
|
||||||
|
- run: yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Validate OpenAPI Spec
|
||||||
|
run: make docs-validate
|
||||||
53
.github/workflows/validate-openapi-spec.yml
vendored
53
.github/workflows/validate-openapi-spec.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Validate
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- 'release/*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
openapi-spec:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Setup Node v14
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 14
|
|
||||||
|
|
||||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Setup Go v1.17.3
|
|
||||||
uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: '^1.17.3'
|
|
||||||
|
|
||||||
- name: Prebuild docs
|
|
||||||
run: yarn prebuild:docs
|
|
||||||
|
|
||||||
- name: Build OpenAPI 2.0 Spec
|
|
||||||
run: yarn build:docs
|
|
||||||
|
|
||||||
# Install dependencies globally to bypass installing all frontend deps
|
|
||||||
- name: Install swagger2openapi and swagger-cli
|
|
||||||
run: yarn global add swagger2openapi @apidevtools/swagger-cli
|
|
||||||
|
|
||||||
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
|
|
||||||
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
|
|
||||||
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
|
|
||||||
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
|
|
||||||
run: yarn validate:docs
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,11 +7,14 @@ storybook-static
|
|||||||
.tmp
|
.tmp
|
||||||
**/.vscode/settings.json
|
**/.vscode/settings.json
|
||||||
**/.vscode/tasks.json
|
**/.vscode/tasks.json
|
||||||
|
.vscode
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
|
|
||||||
.eslintcache
|
.eslintcache
|
||||||
__debug_bin
|
__debug_bin*
|
||||||
|
|
||||||
api/docs
|
api/docs
|
||||||
.idea
|
.idea
|
||||||
.env
|
.env
|
||||||
|
go.work.sum
|
||||||
|
|
||||||
|
|||||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
yarn lint-staged
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(ts|tsx)'],
|
|
||||||
addons: [
|
|
||||||
'@storybook/addon-links',
|
|
||||||
'@storybook/addon-essentials',
|
|
||||||
{
|
|
||||||
name: '@storybook/addon-postcss',
|
|
||||||
options: {
|
|
||||||
cssLoaderOptions: {
|
|
||||||
importLoaders: 1,
|
|
||||||
modules: {
|
|
||||||
localIdentName: '[path][name]__[local]',
|
|
||||||
auto: true,
|
|
||||||
exportLocalsConvention: 'camelCaseOnly',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
postcssLoaderOptions: {
|
|
||||||
implementation: require('postcss'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
webpackFinal: (config) => {
|
|
||||||
config.resolve.plugins = [
|
|
||||||
...(config.resolve.plugins || []),
|
|
||||||
new TsconfigPathsPlugin({
|
|
||||||
extensions: config.resolve.extensions,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
core: {
|
|
||||||
builder: 'webpack5',
|
|
||||||
},
|
|
||||||
staticDirs: ['./public'],
|
|
||||||
};
|
|
||||||
95
.storybook/main.ts
Normal file
95
.storybook/main.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { StorybookConfig } from '@storybook/react-webpack5';
|
||||||
|
|
||||||
|
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
||||||
|
import { Configuration } from 'webpack';
|
||||||
|
import postcss from 'postcss';
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../app/**/*.stories.@(ts|tsx)'],
|
||||||
|
addons: [
|
||||||
|
'@storybook/addon-links',
|
||||||
|
'@storybook/addon-essentials',
|
||||||
|
{
|
||||||
|
name: '@storybook/addon-styling',
|
||||||
|
options: {
|
||||||
|
cssLoaderOptions: {
|
||||||
|
importLoaders: 1,
|
||||||
|
modules: {
|
||||||
|
localIdentName: '[path][name]__[local]',
|
||||||
|
auto: true,
|
||||||
|
exportLocalsConvention: 'camelCaseOnly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
postCss: {
|
||||||
|
implementation: postcss,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webpackFinal: (config) => {
|
||||||
|
const rules = config?.module?.rules || [];
|
||||||
|
|
||||||
|
const imageRule = rules.find((rule) => {
|
||||||
|
const test = (rule as { test: RegExp }).test;
|
||||||
|
|
||||||
|
if (!test) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return test.test('.svg');
|
||||||
|
}) as { [key: string]: any };
|
||||||
|
|
||||||
|
imageRule.exclude = /\.svg$/;
|
||||||
|
|
||||||
|
rules.unshift({
|
||||||
|
test: /\.svg$/i,
|
||||||
|
type: 'asset',
|
||||||
|
resourceQuery: {
|
||||||
|
not: [/c/],
|
||||||
|
}, // exclude react component if *.svg?url
|
||||||
|
});
|
||||||
|
|
||||||
|
rules.unshift({
|
||||||
|
test: /\.svg$/i,
|
||||||
|
issuer: /\.(js|ts)(x)?$/,
|
||||||
|
resourceQuery: /c/,
|
||||||
|
// *.svg?c
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: '@svgr/webpack',
|
||||||
|
options: {
|
||||||
|
icon: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
resolve: {
|
||||||
|
...config.resolve,
|
||||||
|
plugins: [
|
||||||
|
...(config.resolve?.plugins || []),
|
||||||
|
new TsconfigPathsPlugin({
|
||||||
|
extensions: config.resolve?.extensions,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
...config.module,
|
||||||
|
rules,
|
||||||
|
},
|
||||||
|
} satisfies Configuration;
|
||||||
|
},
|
||||||
|
staticDirs: ['./public'],
|
||||||
|
typescript: {
|
||||||
|
reactDocgen: 'react-docgen-typescript',
|
||||||
|
},
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-webpack5',
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -3,6 +3,7 @@ import '../app/assets/css';
|
|||||||
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, mswDecorator } from 'msw-storybook-addon';
|
||||||
import { handlers } from '@/setup-tests/server-handlers';
|
import { handlers } from '@/setup-tests/server-handlers';
|
||||||
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
|
||||||
// Initialize MSW
|
// Initialize MSW
|
||||||
initMSW({
|
initMSW({
|
||||||
@@ -31,11 +32,17 @@ export const parameters = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testQueryClient = new QueryClient({
|
||||||
|
defaultOptions: { queries: { retry: false } },
|
||||||
|
});
|
||||||
|
|
||||||
export const decorators = [
|
export const decorators = [
|
||||||
(Story) => (
|
(Story) => (
|
||||||
|
<QueryClientProvider client={testQueryClient}>
|
||||||
<UIRouter plugins={[pushStateLocationPlugin]}>
|
<UIRouter plugins={[pushStateLocationPlugin]}>
|
||||||
<Story />
|
<Story />
|
||||||
</UIRouter>
|
</UIRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
),
|
),
|
||||||
mswDecorator,
|
mswDecorator,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,6 +15,15 @@
|
|||||||
// ],
|
// ],
|
||||||
// "description": "Log output to console"
|
// "description": "Log output to console"
|
||||||
// }
|
// }
|
||||||
|
"React Named Export Component": {
|
||||||
|
"prefix": "rnec",
|
||||||
|
"body": [
|
||||||
|
"export function $TM_FILENAME_BASE() {",
|
||||||
|
" return <div>$TM_FILENAME_BASE</div>;",
|
||||||
|
"}"
|
||||||
|
],
|
||||||
|
"description": "React Named Export Component"
|
||||||
|
},
|
||||||
"Component": {
|
"Component": {
|
||||||
"scope": "javascript",
|
"scope": "javascript",
|
||||||
"prefix": "mycomponent",
|
"prefix": "mycomponent",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Each commit message should include a **type**, a **scope** and a **subject**:
|
|||||||
<type>(<scope>): <subject>
|
<type>(<scope>): <subject>
|
||||||
```
|
```
|
||||||
|
|
||||||
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
|
Lines should not exceed 100 characters. This allows the message to be easier to read on GitHub as well as in various git tools and produces a nice, neat commit log ie:
|
||||||
|
|
||||||
```
|
```
|
||||||
#271 feat(containers): add exposed ports in the containers view
|
#271 feat(containers): add exposed ports in the containers view
|
||||||
@@ -63,7 +63,7 @@ The subject contains succinct description of the change:
|
|||||||
|
|
||||||
## Contribution process
|
## Contribution process
|
||||||
|
|
||||||
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
|
Our contribution process is described below. Some of the steps can be visualized inside GitHub via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
|
||||||
|
|
||||||
### Bug report
|
### Bug report
|
||||||
|
|
||||||
@@ -79,30 +79,42 @@ The feature request process is similar to the bug report process but has an extr
|
|||||||
|
|
||||||
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
|
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
|
||||||
|
|
||||||
Install dependencies with yarn:
|
Install dependencies:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ yarn
|
$ make deps
|
||||||
```
|
```
|
||||||
|
|
||||||
Then build and run the project in a Docker container:
|
Then build and run the project in a Docker container:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ yarn start
|
$ make dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Portainer can now be accessed at <https://localhost:9443>.
|
Portainer server can now be accessed at <https://localhost:9443>. and UI dev server runs on <http://localhost:8999>.
|
||||||
|
|
||||||
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
|
if you want to build the project you can run:
|
||||||
|
|
||||||
### Build customisation
|
```sh
|
||||||
|
make build-all
|
||||||
|
```
|
||||||
|
|
||||||
You can customise the following settings:
|
For additional make commands, run `make help`.
|
||||||
|
|
||||||
|
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
|
||||||
|
|
||||||
|
### Build customization
|
||||||
|
|
||||||
|
You can customize the following settings:
|
||||||
|
|
||||||
- `PORTAINER_DATA`: The host dir or volume name used by portainer (default is `/tmp/portainer`, which won't persist over reboots).
|
- `PORTAINER_DATA`: The host dir or volume name used by portainer (default is `/tmp/portainer`, which won't persist over reboots).
|
||||||
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
|
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
|
||||||
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
|
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
|
||||||
|
|
||||||
|
## Testing your build
|
||||||
|
|
||||||
|
The `--log-level=DEBUG` flag can be passed to the Portainer container in order to provide additional debug output which may be useful when troubleshooting your builds. Please note that this flag was originally intended for internal use and as such the format, functionality and output may change between releases without warning.
|
||||||
|
|
||||||
## Adding api docs
|
## Adding api docs
|
||||||
|
|
||||||
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
|
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
|
||||||
|
|||||||
126
Makefile
Normal file
126
Makefile
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
|
||||||
|
# For a list of valid GOOS and GOARCH values
|
||||||
|
# Note: these can be overriden on the command line e.g. `make PLATFORM=<platform> ARCH=<arch>`
|
||||||
|
PLATFORM=$(shell go env GOOS)
|
||||||
|
ARCH=$(shell go env GOARCH)
|
||||||
|
|
||||||
|
# build target, can be one of "production", "testing", "development"
|
||||||
|
ENV=development
|
||||||
|
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
||||||
|
TAG=latest
|
||||||
|
|
||||||
|
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.8.11
|
||||||
|
GOTESTSUM=go run gotest.tools/gotestsum@latest
|
||||||
|
|
||||||
|
# Don't change anything below this line unless you know what you're doing
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
|
||||||
|
##@ Building
|
||||||
|
.PHONY: init-dist build-storybook build build-client build-server build-image devops
|
||||||
|
init-dist:
|
||||||
|
@mkdir -p dist
|
||||||
|
|
||||||
|
build-all: deps build-server build-client ## Build the client, server and download external dependancies (doesn't build an image)
|
||||||
|
|
||||||
|
build-client: init-dist ## Build the client
|
||||||
|
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
|
||||||
|
|
||||||
|
build-server: init-dist ## Build the server binary
|
||||||
|
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
|
||||||
|
|
||||||
|
build-image: build-all ## Build the Portainer image locally
|
||||||
|
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
|
||||||
|
|
||||||
|
build-storybook: ## Build and serve the storybook files
|
||||||
|
yarn storybook:build
|
||||||
|
|
||||||
|
devops: clean deps build-client ## Build the everything target specifically for CI
|
||||||
|
echo "Building the devops binary..."
|
||||||
|
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
|
||||||
|
|
||||||
|
##@ Build dependencies
|
||||||
|
.PHONY: deps server-deps client-deps tidy
|
||||||
|
deps: server-deps client-deps ## Download all client and server build dependancies
|
||||||
|
|
||||||
|
server-deps: init-dist ## Download dependant server binaries
|
||||||
|
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
|
||||||
|
|
||||||
|
client-deps: ## Install client dependencies
|
||||||
|
yarn
|
||||||
|
|
||||||
|
tidy: ## Tidy up the go.mod file
|
||||||
|
cd api && go mod tidy
|
||||||
|
|
||||||
|
|
||||||
|
##@ Cleanup
|
||||||
|
.PHONY: clean
|
||||||
|
clean: ## Remove all build and download artifacts
|
||||||
|
@echo "Clearing the dist directory..."
|
||||||
|
@rm -rf dist/*
|
||||||
|
|
||||||
|
|
||||||
|
##@ Testing
|
||||||
|
.PHONY: test test-client test-server
|
||||||
|
test: test-server test-client ## Run all tests
|
||||||
|
|
||||||
|
test-client: ## Run client tests
|
||||||
|
yarn test $(ARGS)
|
||||||
|
|
||||||
|
test-server: ## Run server tests
|
||||||
|
cd api && $(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
|
||||||
|
|
||||||
|
##@ Dev
|
||||||
|
.PHONY: dev dev-client dev-server
|
||||||
|
dev: ## Run both the client and server in development mode
|
||||||
|
make dev-server
|
||||||
|
make dev-client
|
||||||
|
|
||||||
|
dev-client: ## Run the client in development mode
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
dev-server: build-server ## Run the server in development mode
|
||||||
|
@./dev/run_container.sh
|
||||||
|
|
||||||
|
|
||||||
|
##@ Format
|
||||||
|
.PHONY: format format-client format-server
|
||||||
|
|
||||||
|
format: format-client format-server ## Format all code
|
||||||
|
|
||||||
|
format-client: ## Format client code
|
||||||
|
yarn format
|
||||||
|
|
||||||
|
format-server: ## Format server code
|
||||||
|
cd api && go fmt ./...
|
||||||
|
|
||||||
|
##@ Lint
|
||||||
|
.PHONY: lint lint-client lint-server
|
||||||
|
lint: lint-client lint-server ## Lint all code
|
||||||
|
|
||||||
|
lint-client: ## Lint client code
|
||||||
|
yarn lint
|
||||||
|
|
||||||
|
lint-server: ## Lint server code
|
||||||
|
cd api && go vet ./...
|
||||||
|
|
||||||
|
|
||||||
|
##@ Extension
|
||||||
|
.PHONY: dev-extension
|
||||||
|
dev-extension: build-server build-client ## Run the extension in development mode
|
||||||
|
make local -f build/docker-extension/Makefile
|
||||||
|
|
||||||
|
|
||||||
|
##@ Docs
|
||||||
|
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
|
||||||
|
docs-build: init-dist ## Build docs
|
||||||
|
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 --markdownFiles ./
|
||||||
|
|
||||||
|
docs-validate: docs-build ## Validate docs
|
||||||
|
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
|
||||||
|
yarn swagger-cli validate dist/docs/openapi.yaml
|
||||||
|
|
||||||
|
##@ Helpers
|
||||||
|
.PHONY: help
|
||||||
|
help: ## Display this help
|
||||||
|
@awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||||
25
README.md
25
README.md
@@ -9,26 +9,20 @@ Portainer consists of a single container that can run on any cluster. It can be
|
|||||||
**Portainer Business Edition** builds on the open-source base and includes a range of advanced features and functions (like RBAC and Support) that are specific to the needs of business users.
|
**Portainer Business Edition** builds on the open-source base and includes a range of advanced features and functions (like RBAC and Support) that are specific to the needs of business users.
|
||||||
|
|
||||||
- [Compare Portainer CE and Compare Portainer BE](https://portainer.io/products)
|
- [Compare Portainer CE and Compare Portainer BE](https://portainer.io/products)
|
||||||
- [Take5 – get 5 free nodes of Portainer Business for as long as you want them](https://portainer.io/pricing/take5)
|
- [Take3 – get 3 free nodes of Portainer Business for as long as you want them](https://www.portainer.io/take-3)
|
||||||
- [Portainer BE install guide](https://install.portainer.io)
|
- [Portainer BE install guide](https://install.portainer.io)
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
|
|
||||||
|
|
||||||
Please note that the public demo cluster is **reset every 15min**.
|
|
||||||
|
|
||||||
## Latest Version
|
## Latest Version
|
||||||
|
|
||||||
Portainer CE is updated regularly. We aim to do an update release every couple of months.
|
Portainer CE is updated regularly. We aim to do an update release every couple of months.
|
||||||
|
|
||||||
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
|
[](https://github.com/portainer/portainer/releases/latest)
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
- [Deploy Portainer](https://docs.portainer.io/v/ce-2.9/start/install)
|
- [Deploy Portainer](https://docs.portainer.io/start/install)
|
||||||
- [Documentation](https://documentation.portainer.io)
|
- [Documentation](https://docs.portainer.io)
|
||||||
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
|
- [Contribute to the project](https://docs.portainer.io/contribute/contribute)
|
||||||
|
|
||||||
## Features & Functions
|
## Features & Functions
|
||||||
|
|
||||||
@@ -36,23 +30,22 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
|
|||||||
|
|
||||||
- [Portainer CE for Docker / Docker Swarm](https://www.portainer.io/solutions/docker)
|
- [Portainer CE for Docker / Docker Swarm](https://www.portainer.io/solutions/docker)
|
||||||
- [Portainer CE for Kubernetes](https://www.portainer.io/solutions/kubernetes-ui)
|
- [Portainer CE for Kubernetes](https://www.portainer.io/solutions/kubernetes-ui)
|
||||||
- [Portainer CE for Azure ACI](https://www.portainer.io/solutions/serverless-containers)
|
|
||||||
|
|
||||||
## Getting help
|
## Getting help
|
||||||
|
|
||||||
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
|
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
|
||||||
|
|
||||||
Learn more about Portainer's community support channels [here.](https://www.portainer.io/community_help)
|
Learn more about Portainer's community support channels [here.](https://www.portainer.io/get-support-for-portainer)
|
||||||
|
|
||||||
- Issues: https://github.com/portainer/portainer/issues
|
- Issues: https://github.com/portainer/portainer/issues
|
||||||
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
|
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
|
||||||
|
|
||||||
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.
|
You can join the Portainer Community by visiting [https://www.portainer.io/join-our-community](https://www.portainer.io/join-our-community). This will give you advance notice of events, content and other related Portainer content.
|
||||||
|
|
||||||
## Reporting bugs and contributing
|
## Reporting bugs and contributing
|
||||||
|
|
||||||
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
|
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
|
||||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request.
|
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://docs.portainer.io/contribute/contribute) to build it locally and make a pull request.
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
@@ -66,7 +59,7 @@ If you are a developer, and our code in this repo makes sense to you, we would l
|
|||||||
|
|
||||||
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
|
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
|
||||||
|
|
||||||
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
|
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/privacy-policy). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
|
|||||||
31
api/.golangci.yaml
Normal file
31
api/.golangci.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
linters:
|
||||||
|
# Disable all linters, the defaults don't pass on our code yet
|
||||||
|
disable-all: true
|
||||||
|
|
||||||
|
# Enable these for now
|
||||||
|
enable:
|
||||||
|
- depguard
|
||||||
|
- govet
|
||||||
|
- errorlint
|
||||||
|
- exportloopref
|
||||||
|
linters-settings:
|
||||||
|
depguard:
|
||||||
|
list-type: denylist
|
||||||
|
include-go-root: true
|
||||||
|
packages:
|
||||||
|
- github.com/sirupsen/logrus
|
||||||
|
- golang.org/x/exp
|
||||||
|
packages-with-error-message:
|
||||||
|
- github.com/sirupsen/logrus: 'logging is allowed only by github.com/rs/zerolog'
|
||||||
|
ignore-file-rules:
|
||||||
|
- '**/*_test.go'
|
||||||
|
- '**/base.go'
|
||||||
|
- '**/base_tx.go'
|
||||||
|
|
||||||
|
# errorlint is causing a typecheck error for some reason. The go compiler will report these
|
||||||
|
# anyway, so ignore them from the linter
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
- path: ./
|
||||||
|
linters:
|
||||||
|
- typecheck
|
||||||
@@ -2,7 +2,6 @@ package adminmonitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -11,9 +10,9 @@ import (
|
|||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
|
||||||
|
|
||||||
var logFatalf = log.Fatalf
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
|
const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
|
||||||
|
|
||||||
@@ -22,11 +21,11 @@ type Monitor struct {
|
|||||||
datastore dataservices.DataStore
|
datastore dataservices.DataStore
|
||||||
shutdownCtx context.Context
|
shutdownCtx context.Context
|
||||||
cancellationFunc context.CancelFunc
|
cancellationFunc context.CancelFunc
|
||||||
mu sync.Mutex
|
mu sync.RWMutex
|
||||||
adminInitDisabled bool
|
adminInitDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a monitor that when started will wait for the timeout duration and then sends the timeout signal to disable the application
|
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
|
||||||
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
|
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
|
||||||
return &Monitor{
|
return &Monitor{
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
@@ -49,24 +48,29 @@ func (m *Monitor) Start() {
|
|||||||
m.cancellationFunc = cancellationFunc
|
m.cancellationFunc = cancellationFunc
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Println("[DEBUG] [internal,init] [message: start initialization monitor ]")
|
log.Debug().Msg("start initialization monitor")
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(m.timeout):
|
case <-time.After(m.timeout):
|
||||||
initialized, err := m.WasInitialized()
|
initialized, err := m.WasInitialized()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logFatalf("%s", err)
|
log.Error().Err(err).Msg("AdminMonitor failed to determine if Portainer is Initialized")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !initialized {
|
if !initialized {
|
||||||
log.Println("[INFO] [internal,init] The Portainer instance timed out for security purposes. To re-enable your Portainer instance, you will need to restart Portainer")
|
log.Info().Msg("the Portainer instance timed out for security purposes, to re-enable your Portainer instance, you will need to restart Portainer")
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
m.adminInitDisabled = true
|
m.adminInitDisabled = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case <-cancellationCtx.Done():
|
case <-cancellationCtx.Done():
|
||||||
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
|
log.Debug().Msg("canceling initialization monitor")
|
||||||
case <-m.shutdownCtx.Done():
|
case <-m.shutdownCtx.Done():
|
||||||
log.Println("[DEBUG] [internal,init] [message: shutting down initialization monitor]")
|
log.Debug().Msg("shutting down initialization monitor")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -79,6 +83,7 @@ func (m *Monitor) Stop() {
|
|||||||
if m.cancellationFunc == nil {
|
if m.cancellationFunc == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.cancellationFunc()
|
m.cancellationFunc()
|
||||||
m.cancellationFunc = nil
|
m.cancellationFunc = nil
|
||||||
}
|
}
|
||||||
@@ -89,12 +94,14 @@ func (m *Monitor) WasInitialized() (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return len(users) > 0, nil
|
return len(users) > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Monitor) WasInstanceDisabled() bool {
|
func (m *Monitor) WasInstanceDisabled() bool {
|
||||||
m.mu.Lock()
|
m.mu.RLock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
return m.adminInitDisabled
|
return m.adminInitDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,13 +109,11 @@ func (m *Monitor) WasInstanceDisabled() bool {
|
|||||||
// Otherwise, it will pass through the request to next
|
// Otherwise, it will pass through the request to next
|
||||||
func (m *Monitor) WithRedirect(next http.Handler) http.Handler {
|
func (m *Monitor) WithRedirect(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if m.WasInstanceDisabled() {
|
if m.WasInstanceDisabled() && strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" {
|
||||||
if strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" {
|
|
||||||
w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout)
|
w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout)
|
||||||
httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil)
|
httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|||||||
74
api/agent/version.go
Normal file
74
api/agent/version.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/internal/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAgentVersionAndPlatform returns the agent version and platform
|
||||||
|
//
|
||||||
|
// it sends a ping to the agent and parses the version and platform from the headers
|
||||||
|
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
|
||||||
|
httpCli := &http.Client{
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsConfig != nil {
|
||||||
|
httpCli.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.ParseURL(endpointUrl + "/ping")
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL.Scheme = "https"
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpCli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
|
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
version := resp.Header.Get(portainer.PortainerAgentHeader)
|
||||||
|
if version == "" {
|
||||||
|
return 0, "", errors.New("Version Header is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||||
|
if agentPlatformHeader == "" {
|
||||||
|
return 0, "", errors.New("Agent Platform Header is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if agentPlatformNumber == 0 {
|
||||||
|
return 0, "", errors.New("Agent platform is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
return portainer.AgentPlatform(agentPlatformNumber), version, nil
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package apikey
|
package apikey
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,13 +15,3 @@ type APIKeyService interface {
|
|||||||
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
|
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
|
||||||
InvalidateUserKeyCache(userId portainer.UserID) bool
|
InvalidateUserKeyCache(userId portainer.UserID) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package apikey
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/internal/securecookie"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ 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 := generateRandomKey(tt.wantLenth)
|
got := securecookie.GenerateRandomKey(tt.wantLenth)
|
||||||
is.Equal(tt.wantLenth, len(got))
|
is.Equal(tt.wantLenth, len(got))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -41,7 +42,7 @@ func Test_generateRandomKey(t *testing.T) {
|
|||||||
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++ {
|
for i := 0; i < 100; i++ {
|
||||||
key := generateRandomKey(8)
|
key := securecookie.GenerateRandomKey(8)
|
||||||
_, ok := keys[string(key)]
|
_, ok := keys[string(key)]
|
||||||
is.False(ok)
|
is.False(ok)
|
||||||
keys[string(key)] = true
|
keys[string(key)] = true
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const portainerAPIKeyPrefix = "ptr_"
|
const portainerAPIKeyPrefix = "ptr_"
|
||||||
@@ -39,7 +40,7 @@ func (a *apiKeyService) HashRaw(rawKey string) []byte {
|
|||||||
// 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 := generateRandomKey(32)
|
randKey := securecookie.GenerateRandomKey(32)
|
||||||
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
|
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
|
||||||
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
|
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ func (a *apiKeyService) GenerateApiKey(user portainer.User, description string)
|
|||||||
Digest: hashDigest,
|
Digest: hashDigest,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.apiKeyRepository.CreateAPIKey(apiKey)
|
err := a.apiKeyRepository.Create(apiKey)
|
||||||
if 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")
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@ func (a *apiKeyService) GenerateApiKey(user portainer.User, description string)
|
|||||||
|
|
||||||
// GetAPIKey returns an API key by its ID.
|
// GetAPIKey returns an API key by its ID.
|
||||||
func (a *apiKeyService) GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error) {
|
func (a *apiKeyService) GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error) {
|
||||||
return a.apiKeyRepository.GetAPIKey(apiKeyID)
|
return a.apiKeyRepository.Read(apiKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAPIKeys returns all the API keys associated to a user.
|
// GetAPIKeys returns all the API keys associated to a user.
|
||||||
@@ -88,7 +89,7 @@ func (a *apiKeyService) GetDigestUserAndKey(digest []byte) (portainer.User, port
|
|||||||
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve API key")
|
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve API key")
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := a.userRepository.User(apiKey.UserID)
|
user, err := a.userRepository.Read(apiKey.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve digest user")
|
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve digest user")
|
||||||
}
|
}
|
||||||
@@ -106,20 +107,20 @@ func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
|
|||||||
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.UpdateAPIKey(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
|
// get api-key digest to remove from cache
|
||||||
apiKey, err := a.apiKeyRepository.GetAPIKey(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
|
// delete the user/api-key from cache
|
||||||
a.cache.Delete(apiKey.Digest)
|
a.cache.Delete(apiKey.Digest)
|
||||||
return a.apiKeyRepository.DeleteAPIKey(apiKeyID)
|
return a.apiKeyRepository.Delete(apiKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *apiKeyService) InvalidateUserKeyCache(userId portainer.UserID) bool {
|
func (a *apiKeyService) InvalidateUserKeyCache(userId portainer.UserID) bool {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package apikey
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"log"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
|
func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
|
||||||
@@ -20,8 +22,7 @@ func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
|
|||||||
func Test_GenerateApiKey(t *testing.T) {
|
func Test_GenerateApiKey(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||||
|
|
||||||
@@ -74,8 +75,7 @@ func Test_GenerateApiKey(t *testing.T) {
|
|||||||
func Test_GetAPIKey(t *testing.T) {
|
func Test_GetAPIKey(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||||
|
|
||||||
@@ -94,8 +94,7 @@ func Test_GetAPIKey(t *testing.T) {
|
|||||||
func Test_GetAPIKeys(t *testing.T) {
|
func Test_GetAPIKeys(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||||
|
|
||||||
@@ -115,8 +114,7 @@ func Test_GetAPIKeys(t *testing.T) {
|
|||||||
func Test_GetDigestUserAndKey(t *testing.T) {
|
func Test_GetDigestUserAndKey(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||||
|
|
||||||
@@ -151,8 +149,7 @@ func Test_GetDigestUserAndKey(t *testing.T) {
|
|||||||
func Test_UpdateAPIKey(t *testing.T) {
|
func Test_UpdateAPIKey(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||||
|
|
||||||
@@ -169,11 +166,9 @@ func Test_UpdateAPIKey(t *testing.T) {
|
|||||||
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
|
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
|
||||||
is.NoError(err)
|
is.NoError(err)
|
||||||
|
|
||||||
log.Println(apiKey)
|
log.Debug().Str("wanted", fmt.Sprintf("%+v", apiKey)).Str("got", fmt.Sprintf("%+v", apiKeyGot)).Msg("")
|
||||||
log.Println(apiKeyGot)
|
|
||||||
|
|
||||||
is.Equal(apiKey.LastUsed, apiKeyGot.LastUsed)
|
is.Equal(apiKey.LastUsed, apiKeyGot.LastUsed)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Successfully updates api-key in cache upon api-key update", func(t *testing.T) {
|
t.Run("Successfully updates api-key in cache upon api-key update", func(t *testing.T) {
|
||||||
@@ -199,8 +194,7 @@ func Test_UpdateAPIKey(t *testing.T) {
|
|||||||
func Test_DeleteAPIKey(t *testing.T) {
|
func Test_DeleteAPIKey(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||||
|
|
||||||
@@ -240,8 +234,7 @@ func Test_DeleteAPIKey(t *testing.T) {
|
|||||||
func Test_InvalidateUserKeyCache(t *testing.T) {
|
func Test_InvalidateUserKeyCache(t *testing.T) {
|
||||||
is := assert.New(t)
|
is := assert.New(t)
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
|
||||||
|
|
||||||
|
|||||||
@@ -34,3 +34,45 @@ func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, e
|
|||||||
|
|
||||||
return buffer.Bytes(), nil
|
return buffer.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tarFileInBuffer represents a tar archive buffer.
|
||||||
|
type tarFileInBuffer struct {
|
||||||
|
b *bytes.Buffer
|
||||||
|
w *tar.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTarFileInBuffer() *tarFileInBuffer {
|
||||||
|
var b bytes.Buffer
|
||||||
|
return &tarFileInBuffer{
|
||||||
|
b: &b,
|
||||||
|
w: tar.NewWriter(&b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put puts a single file to tar archive buffer.
|
||||||
|
func (t *tarFileInBuffer) Put(fileContent []byte, fileName string, mode int64) error {
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: fileName,
|
||||||
|
Mode: mode,
|
||||||
|
Size: int64(len(fileContent)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.w.WriteHeader(hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := t.w.Write(fileContent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the archive as a byte array.
|
||||||
|
func (t *tarFileInBuffer) Bytes() []byte {
|
||||||
|
return t.b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tarFileInBuffer) Close() error {
|
||||||
|
return t.w.Close()
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package archive
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -84,7 +85,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
|
|||||||
for {
|
for {
|
||||||
header, err := tarReader.Next()
|
header, err := tarReader.Next()
|
||||||
|
|
||||||
if err == io.EOF {
|
if errors.Is(err, io.EOF) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
|
|||||||
}
|
}
|
||||||
outFile.Close()
|
outFile.Close()
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Tar: uknown type: %v in %s",
|
return fmt.Errorf("tar: unknown type: %v in %s",
|
||||||
header.Typeflag,
|
header.Typeflag,
|
||||||
header.Name)
|
header.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ package archive
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,22 +25,18 @@ func listFiles(dir string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_shouldCreateArhive(t *testing.T) {
|
func Test_shouldCreateArhive(t *testing.T) {
|
||||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
tmpdir := t.TempDir()
|
||||||
defer os.RemoveAll(tmpdir)
|
|
||||||
|
|
||||||
content := []byte("content")
|
content := []byte("content")
|
||||||
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||||
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
|
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
|
||||||
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||||
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||||
|
|
||||||
gzPath, err := TarGzDir(tmpdir)
|
gzPath, err := TarGzDir(tmpdir)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
|
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
|
||||||
|
|
||||||
extractionDir, _ := ioutils.TempDir("", "extract")
|
extractionDir := t.TempDir()
|
||||||
defer os.RemoveAll(extractionDir)
|
|
||||||
|
|
||||||
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
|
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,7 +47,7 @@ func Test_shouldCreateArhive(t *testing.T) {
|
|||||||
wasExtracted := func(p string) {
|
wasExtracted := func(p string) {
|
||||||
fullpath := path.Join(extractionDir, p)
|
fullpath := path.Join(extractionDir, p)
|
||||||
assert.Contains(t, extractedFiles, fullpath)
|
assert.Contains(t, extractedFiles, fullpath)
|
||||||
copyContent, _ := ioutil.ReadFile(fullpath)
|
copyContent, _ := os.ReadFile(fullpath)
|
||||||
assert.Equal(t, content, copyContent)
|
assert.Equal(t, content, copyContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,22 +57,18 @@ func Test_shouldCreateArhive(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
||||||
tmpdir, _ := ioutils.TempDir("", "backup")
|
tmpdir := t.TempDir()
|
||||||
defer os.RemoveAll(tmpdir)
|
|
||||||
|
|
||||||
content := []byte("content")
|
content := []byte("content")
|
||||||
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
|
||||||
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
|
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
|
||||||
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
|
||||||
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
|
||||||
|
|
||||||
gzPath, err := TarGzDir(tmpdir)
|
gzPath, err := TarGzDir(tmpdir)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
|
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
|
||||||
|
|
||||||
extractionDir, _ := ioutils.TempDir("", "extract")
|
extractionDir := t.TempDir()
|
||||||
defer os.RemoveAll(extractionDir)
|
|
||||||
|
|
||||||
r, _ := os.Open(gzPath)
|
r, _ := os.Open(gzPath)
|
||||||
ExtractTarGz(r, extractionDir)
|
ExtractTarGz(r, extractionDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,7 +79,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
|
|||||||
wasExtracted := func(p string) {
|
wasExtracted := func(p string) {
|
||||||
fullpath := path.Join(extractionDir, p)
|
fullpath := path.Join(extractionDir, p)
|
||||||
assert.Contains(t, extractedFiles, fullpath)
|
assert.Contains(t, extractedFiles, fullpath)
|
||||||
copyContent, _ := ioutil.ReadFile(fullpath)
|
copyContent, _ := os.ReadFile(fullpath)
|
||||||
assert.Equal(t, content, copyContent)
|
assert.Equal(t, content, copyContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
|
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
|
||||||
@@ -36,7 +36,7 @@ func extractFileFromArchive(file *zip.File, dest string) error {
|
|||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(f)
|
data, err := io.ReadAll(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
package archive
|
package archive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUnzipFile(t *testing.T) {
|
func TestUnzipFile(t *testing.T) {
|
||||||
dir, err := ioutil.TempDir("", "unzip-test-")
|
dir := t.TempDir()
|
||||||
assert.NoError(t, err)
|
|
||||||
defer os.RemoveAll(dir)
|
|
||||||
/*
|
/*
|
||||||
Archive structure.
|
Archive structure.
|
||||||
├── 0
|
├── 0
|
||||||
@@ -21,7 +18,7 @@ func TestUnzipFile(t *testing.T) {
|
|||||||
└── 0.txt
|
└── 0.txt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
err = UnzipFile("./testdata/sample_archive.zip", dir)
|
err := UnzipFile("./testdata/sample_archive.zip", dir)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
archiveDir := dir + "/sample_archive"
|
archiveDir := dir + "/sample_archive"
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/portainer/portainer/api/archive"
|
"github.com/portainer/portainer/api/archive"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
"github.com/portainer/portainer/api/crypto"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/offlinegate"
|
"github.com/portainer/portainer/api/http/offlinegate"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const rwxr__r__ os.FileMode = 0744
|
const rwxr__r__ os.FileMode = 0744
|
||||||
@@ -47,9 +48,9 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
|
|||||||
|
|
||||||
err := datastore.Export(exportFilename)
|
err := datastore.Export(exportFilename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Debugf("failed to export to %s", exportFilename)
|
log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
|
||||||
} else {
|
} else {
|
||||||
logrus.Debugf("exported to %s", exportFilename)
|
log.Debug().Str("filename", exportFilename).Msg("file exported")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package backup
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -43,6 +45,12 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
|
|||||||
return errors.Wrap(err, "Failed to stop db")
|
return errors.Wrap(err, "Failed to stop db")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// At some point, backups were created containing a subdirectory, now we need to handle both
|
||||||
|
restorePath, err = getRestoreSourcePath(restorePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
|
||||||
|
}
|
||||||
|
|
||||||
if err = restoreFiles(restorePath, filestorePath); err != nil {
|
if err = restoreFiles(restorePath, filestorePath); err != nil {
|
||||||
return errors.Wrap(err, "failed to restore the system state")
|
return errors.Wrap(err, "failed to restore the system state")
|
||||||
}
|
}
|
||||||
@@ -59,6 +67,26 @@ func extractArchive(r io.Reader, destinationDirPath string) error {
|
|||||||
return archive.ExtractTarGz(r, destinationDirPath)
|
return archive.ExtractTarGz(r, destinationDirPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRestoreSourcePath(dir string) (string, error) {
|
||||||
|
// find portainer.db or portainer.edb file. Return the parent directory
|
||||||
|
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
|
||||||
|
|
||||||
|
backupDirPath := dir
|
||||||
|
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if portainerdbRegex.MatchString(d.Name()) {
|
||||||
|
backupDirPath = filepath.Dir(path)
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return backupDirPath, err
|
||||||
|
}
|
||||||
|
|
||||||
func restoreFiles(srcDir string, destinationDir string) error {
|
func restoreFiles(srcDir string, destinationDir string) error {
|
||||||
for _, filename := range filesToRestore {
|
for _, filename := range filesToRestore {
|
||||||
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
|
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
|
||||||
|
|||||||
9
api/build/variables.go
Normal file
9
api/build/variables.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package build
|
||||||
|
|
||||||
|
// Variables to be set during the build time
|
||||||
|
var BuildNumber string
|
||||||
|
var ImageTag string
|
||||||
|
var NodejsVersion string
|
||||||
|
var YarnVersion string
|
||||||
|
var WebpackVersion string
|
||||||
|
var GoVersion string
|
||||||
61
api/chisel/crypto/crypto.go
Normal file
61
api/chisel/crypto/crypto.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
chshare "github.com/jpillora/chisel/share"
|
||||||
|
)
|
||||||
|
|
||||||
|
var one = new(big.Int).SetInt64(1)
|
||||||
|
|
||||||
|
// GenerateGo119CompatibleKey This function is basically copied from chshare.GenerateKey.
|
||||||
|
func GenerateGo119CompatibleKey(seed string) ([]byte, error) {
|
||||||
|
r := chshare.NewDetermRand([]byte(seed))
|
||||||
|
priv, err := ecdsaGenerateKey(elliptic.P256(), r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b, err := x509.MarshalECPrivateKey(priv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to marshal ECDSA private key: %w", err)
|
||||||
|
}
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: b}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is copied from Go1.19
|
||||||
|
func randFieldElement(c elliptic.Curve, rand io.Reader) (k *big.Int, err error) {
|
||||||
|
params := c.Params()
|
||||||
|
// Note that for P-521 this will actually be 63 bits more than the order, as
|
||||||
|
// division rounds down, but the extra bit is inconsequential.
|
||||||
|
b := make([]byte, params.N.BitLen()/8+8)
|
||||||
|
_, err = io.ReadFull(rand, b)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
k = new(big.Int).SetBytes(b)
|
||||||
|
n := new(big.Int).Sub(params.N, one)
|
||||||
|
k.Mod(k, n)
|
||||||
|
k.Add(k, one)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is copied from Go1.19
|
||||||
|
func ecdsaGenerateKey(c elliptic.Curve, rand io.Reader) (*ecdsa.PrivateKey, error) {
|
||||||
|
k, err := randFieldElement(c, rand)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
priv := new(ecdsa.PrivateKey)
|
||||||
|
priv.PublicKey.Curve = c
|
||||||
|
priv.D = k
|
||||||
|
priv.PublicKey.X, priv.PublicKey.Y = c.ScalarBaseMult(k.Bytes())
|
||||||
|
return priv, nil
|
||||||
|
}
|
||||||
37
api/chisel/crypto/crypto_test.go
Normal file
37
api/chisel/crypto/crypto_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateGo119CompatibleKey(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
seed string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want []byte
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Generate Go 1.19 compatible private key with a given seed",
|
||||||
|
args: args{seed: "94qh17MCIk8BOkiI"},
|
||||||
|
want: []byte("-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIHeohwk0Gy3RHVVViaHz7pz/HOiqA7fkv1FTM3mGgfT3oAoGCCqGSM49\nAwEHoUQDQgAEN7riX06xDsLNPuUmOvYFluNEakcFwZZRVvOcIYk/9VYnanDzW0Km\n8/BUUiKyJDuuGdS4fj9SlQ4iL8yBK01uKg==\n-----END EC PRIVATE KEY-----\n"),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := GenerateGo119CompatibleKey(tt.args.seed)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("GenerateGo119CompatibleKey() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("GenerateGo119CompatibleKey()\ngot: Z %v\nwant: %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,17 +2,23 @@ package chisel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
|
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
|
||||||
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
|
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
|
||||||
|
if endpoint.Edge.AsyncMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
service.mu.Lock()
|
service.mu.Lock()
|
||||||
tunnel := service.getTunnelDetails(endpointID)
|
tunnel := service.getTunnelDetails(endpoint.ID)
|
||||||
|
|
||||||
existingJobIndex := -1
|
existingJobIndex := -1
|
||||||
for idx, existingJob := range tunnel.Jobs {
|
for idx, existingJob := range tunnel.Jobs {
|
||||||
if existingJob.ID == edgeJob.ID {
|
if existingJob.ID == edgeJob.ID {
|
||||||
existingJobIndex = idx
|
existingJobIndex = idx
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +29,8 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
|
|||||||
tunnel.Jobs[existingJobIndex] = *edgeJob
|
tunnel.Jobs[existingJobIndex] = *edgeJob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache.Del(endpoint.ID)
|
||||||
|
|
||||||
service.mu.Unlock()
|
service.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +38,7 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
|
|||||||
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
||||||
service.mu.Lock()
|
service.mu.Lock()
|
||||||
|
|
||||||
for _, tunnel := range service.tunnelDetailsMap {
|
for endpointID, tunnel := range service.tunnelDetailsMap {
|
||||||
// Filter in-place
|
|
||||||
n := 0
|
n := 0
|
||||||
for _, edgeJob := range tunnel.Jobs {
|
for _, edgeJob := range tunnel.Jobs {
|
||||||
if edgeJob.ID != edgeJobID {
|
if edgeJob.ID != edgeJobID {
|
||||||
@@ -41,7 +48,28 @@ func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tunnel.Jobs = tunnel.Jobs[:n]
|
tunnel.Jobs = tunnel.Jobs[:n]
|
||||||
|
|
||||||
|
cache.Del(endpointID)
|
||||||
}
|
}
|
||||||
|
|
||||||
service.mu.Unlock()
|
service.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
|
||||||
|
service.mu.Lock()
|
||||||
|
tunnel := service.getTunnelDetails(endpointID)
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
for _, edgeJob := range tunnel.Jobs {
|
||||||
|
if edgeJob.ID != edgeJobID {
|
||||||
|
tunnel.Jobs[n] = edgeJob
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel.Jobs = tunnel.Jobs[:n]
|
||||||
|
|
||||||
|
cache.Del(endpointID)
|
||||||
|
|
||||||
|
service.mu.Unlock()
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ package chisel
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dchest/uniuri"
|
|
||||||
chserver "github.com/jpillora/chisel/server"
|
|
||||||
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/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
|
|
||||||
|
chserver "github.com/jpillora/chisel/server"
|
||||||
|
"github.com/jpillora/chisel/share/ccrypto"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -34,14 +36,16 @@ type Service struct {
|
|||||||
shutdownCtx context.Context
|
shutdownCtx context.Context
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
fileService portainer.FileService
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) *Service {
|
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context, fileService portainer.FileService) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
|
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
|
||||||
dataStore: dataStore,
|
dataStore: dataStore,
|
||||||
shutdownCtx: shutdownCtx,
|
shutdownCtx: shutdownCtx,
|
||||||
|
fileService: fileService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,14 +61,22 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
|
|||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Timeout: 3 * time.Second,
|
Timeout: 3 * time.Second,
|
||||||
}
|
}
|
||||||
_, err = httpClient.Do(req)
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 func() {
|
||||||
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: start for %.0f minutes]\n", endpointID, maxAlive.Minutes())
|
log.Debug().
|
||||||
|
Int("endpoint_id", int(endpointID)).
|
||||||
|
Float64("max_alive_minutes", maxAlive.Minutes()).
|
||||||
|
Msg("start")
|
||||||
|
|
||||||
maxAliveTicker := time.NewTicker(maxAlive)
|
maxAliveTicker := time.NewTicker(maxAlive)
|
||||||
defer maxAliveTicker.Stop()
|
defer maxAliveTicker.Stop()
|
||||||
pingTicker := time.NewTicker(tunnelCleanupInterval)
|
pingTicker := time.NewTicker(tunnelCleanupInterval)
|
||||||
@@ -76,14 +88,25 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
|||||||
service.SetTunnelStatusToActive(endpointID)
|
service.SetTunnelStatusToActive(endpointID)
|
||||||
err := service.pingAgent(endpointID)
|
err := service.pingAgent(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [warning: ping agent err=%s]\n", endpointID, err)
|
log.Debug().
|
||||||
|
Int("endpoint_id", int(endpointID)).
|
||||||
|
Err(err).
|
||||||
|
Msg("ping agent")
|
||||||
}
|
}
|
||||||
case <-maxAliveTicker.C:
|
case <-maxAliveTicker.C:
|
||||||
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: stop as %.0f minutes timeout]\n", endpointID, maxAlive.Minutes())
|
log.Debug().
|
||||||
|
Int("endpoint_id", int(endpointID)).
|
||||||
|
Float64("timeout_minutes", maxAlive.Minutes()).
|
||||||
|
Msg("tunnel keep alive timeout")
|
||||||
|
|
||||||
return
|
return
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
err := ctx.Err()
|
err := ctx.Err()
|
||||||
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: stop as err=%s]\n", endpointID, err)
|
log.Debug().
|
||||||
|
Int("endpoint_id", int(endpointID)).
|
||||||
|
Err(err).
|
||||||
|
Msg("tunnel stop")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,14 +119,15 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
|
|||||||
// It starts the tunnel status verification process in the background.
|
// It starts the tunnel status verification process in the background.
|
||||||
// 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 {
|
||||||
keySeed, err := service.retrievePrivateKeySeed()
|
privateKeyFile, err := service.retrievePrivateKeyFile()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
config := &chserver.Config{
|
config := &chserver.Config{
|
||||||
Reverse: true,
|
Reverse: true,
|
||||||
KeySeed: keySeed,
|
KeyFile: privateKeyFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
chiselServer, err := chserver.NewServer(config)
|
chiselServer, err := chserver.NewServer(config)
|
||||||
@@ -139,30 +163,48 @@ func (service *Service) StopTunnelServer() error {
|
|||||||
return service.chiselServer.Close()
|
return service.chiselServer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) retrievePrivateKeySeed() (string, error) {
|
func (service *Service) retrievePrivateKeyFile() (string, error) {
|
||||||
var serverInfo *portainer.TunnelServerInfo
|
privateKeyFile := service.fileService.GetDefaultChiselPrivateKeyPath()
|
||||||
|
|
||||||
serverInfo, err := service.dataStore.TunnelServer().Info()
|
exist, _ := service.fileService.FileExists(privateKeyFile)
|
||||||
if service.dataStore.IsErrObjectNotFound(err) {
|
if !exist {
|
||||||
keySeed := uniuri.NewLen(16)
|
log.Debug().
|
||||||
|
Str("private-key", privateKeyFile).
|
||||||
|
Msg("Chisel private key file does not exist")
|
||||||
|
|
||||||
serverInfo = &portainer.TunnelServerInfo{
|
privateKey, err := ccrypto.GenerateKey("")
|
||||||
PrivateKeySeed: keySeed,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := service.dataStore.TunnelServer().UpdateInfo(serverInfo)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
log.Error().
|
||||||
}
|
Err(err).
|
||||||
} else if err != nil {
|
Msg("Failed to generate chisel private key")
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverInfo.PrivateKeySeed, nil
|
err = service.fileService.StoreChiselPrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Err(err).
|
||||||
|
Msg("Failed to save Chisel private key to disk")
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
log.Info().
|
||||||
|
Str("private-key", privateKeyFile).
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) startTunnelVerificationLoop() {
|
func (service *Service) startTunnelVerificationLoop() {
|
||||||
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
|
log.Debug().
|
||||||
|
Float64("check_interval_seconds", tunnelCleanupInterval.Seconds()).
|
||||||
|
Msg("starting tunnel management process")
|
||||||
|
|
||||||
ticker := time.NewTicker(tunnelCleanupInterval)
|
ticker := time.NewTicker(tunnelCleanupInterval)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -170,10 +212,12 @@ func (service *Service) startTunnelVerificationLoop() {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
service.checkTunnels()
|
service.checkTunnels()
|
||||||
case <-service.shutdownCtx.Done():
|
case <-service.shutdownCtx.Done():
|
||||||
log.Println("[DEBUG] Shutting down tunnel service")
|
log.Debug().Msg("shutting down tunnel service")
|
||||||
|
|
||||||
if err := service.StopTunnelServer(); err != nil {
|
if err := service.StopTunnelServer(); err != nil {
|
||||||
log.Printf("Stopped tunnel service: %s", err)
|
log.Debug().Err(err).Msg("stopped tunnel service")
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -185,32 +229,53 @@ func (service *Service) checkTunnels() {
|
|||||||
|
|
||||||
service.mu.Lock()
|
service.mu.Lock()
|
||||||
for key, tunnel := range service.tunnelDetailsMap {
|
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
|
tunnels[key] = *tunnel
|
||||||
}
|
}
|
||||||
service.mu.Unlock()
|
service.mu.Unlock()
|
||||||
|
|
||||||
for endpointID, tunnel := range tunnels {
|
for endpointID, tunnel := range tunnels {
|
||||||
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed := time.Since(tunnel.LastActivity)
|
elapsed := time.Since(tunnel.LastActivity)
|
||||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", endpointID, tunnel.Status, elapsed.Seconds())
|
log.Debug().
|
||||||
|
Int("endpoint_id", int(endpointID)).
|
||||||
|
Str("status", tunnel.Status).
|
||||||
|
Float64("status_time_seconds", elapsed.Seconds()).
|
||||||
|
Msg("environment tunnel monitoring")
|
||||||
|
|
||||||
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
|
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
|
||||||
continue
|
log.Debug().
|
||||||
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
|
Int("endpoint_id", int(endpointID)).
|
||||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
|
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.Seconds() < activeTimeout.Seconds() {
|
if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
|
||||||
continue
|
log.Debug().
|
||||||
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
|
Int("endpoint_id", int(endpointID)).
|
||||||
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
|
Str("status", tunnel.Status).
|
||||||
|
Float64("status_time_seconds", elapsed.Seconds()).
|
||||||
|
Float64("timeout_seconds", activeTimeout.Seconds()).
|
||||||
|
Msg("ACTIVE state timeout exceeded")
|
||||||
|
|
||||||
err := service.snapshotEnvironment(endpointID, tunnel.Port)
|
err := service.snapshotEnvironment(endpointID, tunnel.Port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %d): %s", endpointID, err)
|
log.Error().
|
||||||
|
Int("endpoint_id", int(endpointID)).
|
||||||
|
Err(err).
|
||||||
|
Msg("unable to snapshot Edge environment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,14 +289,7 @@ func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tun
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointURL := endpoint.URL
|
|
||||||
|
|
||||||
endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
|
endpoint.URL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnelPort)
|
||||||
err = service.snapshotService.SnapshotEndpoint(endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint.URL = endpointURL
|
return service.snapshotService.SnapshotEndpoint(endpoint)
|
||||||
return service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ package chisel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dchest/uniuri"
|
|
||||||
"github.com/portainer/libcrypto"
|
"github.com/portainer/libcrypto"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||||
|
|
||||||
|
"github.com/dchest/uniuri"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -49,6 +52,8 @@ func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *porta
|
|||||||
|
|
||||||
service.tunnelDetailsMap[endpointID] = tunnel
|
service.tunnelDetailsMap[endpointID] = tunnel
|
||||||
|
|
||||||
|
cache.Del(endpointID)
|
||||||
|
|
||||||
return tunnel
|
return tunnel
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +67,10 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portai
|
|||||||
|
|
||||||
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
|
||||||
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
|
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)
|
tunnel := service.GetTunnelDetails(endpoint.ID)
|
||||||
|
|
||||||
if tunnel.Status == portainer.EdgeAgentActive {
|
if tunnel.Status == portainer.EdgeAgentActive {
|
||||||
@@ -99,6 +108,8 @@ func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID)
|
|||||||
tunnel.Credentials = ""
|
tunnel.Credentials = ""
|
||||||
tunnel.LastActivity = time.Now()
|
tunnel.LastActivity = time.Now()
|
||||||
service.mu.Unlock()
|
service.mu.Unlock()
|
||||||
|
|
||||||
|
cache.Del(endpointID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
|
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
|
||||||
@@ -115,12 +126,17 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
|||||||
credentials := tunnel.Credentials
|
credentials := tunnel.Credentials
|
||||||
if credentials != "" {
|
if credentials != "" {
|
||||||
tunnel.Credentials = ""
|
tunnel.Credentials = ""
|
||||||
|
|
||||||
|
if service.chiselServer != nil {
|
||||||
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
service.ProxyManager.DeleteEndpointProxy(endpointID)
|
||||||
|
|
||||||
service.mu.Unlock()
|
service.mu.Unlock()
|
||||||
|
|
||||||
|
cache.Del(endpointID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
|
||||||
@@ -129,6 +145,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
|||||||
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
|
// 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).
|
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
|
||||||
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
||||||
|
defer cache.Del(endpointID)
|
||||||
|
|
||||||
tunnel := service.getTunnelDetails(endpointID)
|
tunnel := service.getTunnelDetails(endpointID)
|
||||||
|
|
||||||
service.mu.Lock()
|
service.mu.Lock()
|
||||||
@@ -146,10 +164,13 @@ func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointI
|
|||||||
|
|
||||||
username, password := generateRandomCredentials()
|
username, password := generateRandomCredentials()
|
||||||
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
|
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
|
||||||
|
|
||||||
|
if service.chiselServer != nil {
|
||||||
err = service.chiselServer.AddUser(username, password, authorizedRemote)
|
err = service.chiselServer.AddUser(username, password, authorizedRemote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
|
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
"os"
|
"github.com/rs/zerolog/log"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,8 +34,9 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||||||
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: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
|
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
|
||||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
|
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
|
||||||
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
|
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
|
||||||
@@ -61,6 +61,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||||||
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
|
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
|
||||||
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"),
|
||||||
|
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("PRETTY", "JSON"),
|
||||||
}
|
}
|
||||||
|
|
||||||
kingpin.Parse()
|
kingpin.Parse()
|
||||||
@@ -70,6 +72,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +81,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||||||
|
|
||||||
// ValidateFlags validates the values of the flags.
|
// ValidateFlags validates the values of the flags.
|
||||||
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||||
|
|
||||||
displayDeprecationWarnings(flags)
|
displayDeprecationWarnings(flags)
|
||||||
|
|
||||||
err := validateEndpointURL(*flags.EndpointURL)
|
err := validateEndpointURL(*flags.EndpointURL)
|
||||||
@@ -100,16 +102,19 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
|||||||
|
|
||||||
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
||||||
if *flags.NoAnalytics {
|
if *flags.NoAnalytics {
|
||||||
log.Println("Warning: The --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect.")
|
log.Warn().Msg("the --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect")
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.SSL {
|
if *flags.SSL {
|
||||||
log.Println("Warning: SSL is enabled by default and there is no need for the --ssl flag. It has been kept to allow migration of instances running a previous version of Portainer with this flag enabled")
|
log.Warn().Msg("SSL is enabled by default and there is no need for the --ssl flag, it has been kept to allow migration of instances running a previous version of Portainer with this flag enabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpointURL(endpointURL string) error {
|
func validateEndpointURL(endpointURL string) error {
|
||||||
if endpointURL != "" {
|
if endpointURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -121,19 +126,23 @@ func validateEndpointURL(endpointURL string) error {
|
|||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return errSocketOrNamedPipeNotFound
|
return errSocketOrNamedPipeNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSnapshotInterval(snapshotInterval string) error {
|
func validateSnapshotInterval(snapshotInterval string) error {
|
||||||
if snapshotInterval != "" {
|
if snapshotInterval == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
_, err := time.ParseDuration(snapshotInterval)
|
_, err := time.ParseDuration(snapshotInterval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errInvalidSnapshotInterval
|
return errInvalidSnapshotInterval
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,24 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"log"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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) {
|
||||||
log.Printf("%s [y/N]", message)
|
fmt.Printf("%s [y/N]", message)
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
answer, err := reader.ReadString('\n')
|
answer, err := reader.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
answer = strings.Replace(answer, "\n", "", -1)
|
|
||||||
|
answer = strings.ReplaceAll(answer, "\n", "")
|
||||||
answer = strings.ToLower(answer)
|
answer = strings.ToLower(answer)
|
||||||
|
|
||||||
return answer == "y" || answer == "yes", nil
|
return answer == "y" || answer == "yes", nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
"gopkg.in/alecthomas/kingpin.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/datastore"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func importFromJson(fileService portainer.FileService, store *datastore.Store) {
|
|
||||||
// EXPERIMENTAL - if used with an incomplete json file, it will fail, as we don't have a way to default the model values
|
|
||||||
importFile := "/data/import.json"
|
|
||||||
if exists, _ := fileService.FileExists(importFile); exists {
|
|
||||||
if err := store.Import(importFile); err != nil {
|
|
||||||
logrus.WithError(err).Debugf("Import %s failed", importFile)
|
|
||||||
|
|
||||||
// TODO: should really rollback on failure, but then we have nothing.
|
|
||||||
} else {
|
|
||||||
logrus.Printf("Successfully imported %s to new portainer database", importFile)
|
|
||||||
}
|
|
||||||
// TODO: this is bad - its to ensure that any defaults that were broken in import, or migrations get set back to what we want
|
|
||||||
// I also suspect that everything from "Init to Init" is potentially a migration
|
|
||||||
err := store.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed initializing data store: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,56 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
|
stdlog "log"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/rs/zerolog/pkgerrors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func configureLogger() {
|
func configureLogger() {
|
||||||
logger := logrus.New() // logger is to implicitly substitute stdlib's log
|
zerolog.ErrorStackFieldName = "stack_trace"
|
||||||
log.SetOutput(logger.Writer())
|
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||||
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
|
|
||||||
formatter := &logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true}
|
stdlog.SetFlags(0)
|
||||||
|
stdlog.SetOutput(log.Logger)
|
||||||
|
|
||||||
logger.SetFormatter(formatter)
|
log.Logger = log.Logger.With().Caller().Stack().Logger()
|
||||||
logrus.SetFormatter(formatter)
|
}
|
||||||
|
|
||||||
logger.SetLevel(logrus.DebugLevel)
|
func setLoggingLevel(level string) {
|
||||||
logrus.SetLevel(logrus.DebugLevel)
|
switch level {
|
||||||
|
case "ERROR":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
case "WARN":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||||
|
case "INFO":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
case "DEBUG":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setLoggingMode(mode string) {
|
||||||
|
switch mode {
|
||||||
|
case "PRETTY":
|
||||||
|
log.Logger = log.Output(zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stderr,
|
||||||
|
TimeFormat: "2006/01/02 03:04PM",
|
||||||
|
FormatMessage: formatMessage,
|
||||||
|
})
|
||||||
|
case "JSON":
|
||||||
|
log.Logger = log.Output(os.Stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMessage(i interface{}) string {
|
||||||
|
if i == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s |", i)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,74 +3,85 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"math/rand"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/portainer/libhelm"
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/apikey"
|
"github.com/portainer/portainer/api/apikey"
|
||||||
|
"github.com/portainer/portainer/api/build"
|
||||||
"github.com/portainer/portainer/api/chisel"
|
"github.com/portainer/portainer/api/chisel"
|
||||||
"github.com/portainer/portainer/api/cli"
|
"github.com/portainer/portainer/api/cli"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
"github.com/portainer/portainer/api/crypto"
|
||||||
"github.com/portainer/portainer/api/database"
|
"github.com/portainer/portainer/api/database"
|
||||||
"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/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/demo"
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/docker"
|
||||||
|
dockerclient "github.com/portainer/portainer/api/docker/client"
|
||||||
"github.com/portainer/portainer/api/exec"
|
"github.com/portainer/portainer/api/exec"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/git"
|
"github.com/portainer/portainer/api/git"
|
||||||
"github.com/portainer/portainer/api/hostmanagement/openamt"
|
"github.com/portainer/portainer/api/hostmanagement/openamt"
|
||||||
"github.com/portainer/portainer/api/http"
|
"github.com/portainer/portainer/api/http"
|
||||||
"github.com/portainer/portainer/api/http/client"
|
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
"github.com/portainer/portainer/api/internal/edge"
|
"github.com/portainer/portainer/api/internal/edge"
|
||||||
|
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||||
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
"github.com/portainer/portainer/api/internal/snapshot"
|
"github.com/portainer/portainer/api/internal/snapshot"
|
||||||
"github.com/portainer/portainer/api/internal/ssl"
|
"github.com/portainer/portainer/api/internal/ssl"
|
||||||
|
"github.com/portainer/portainer/api/internal/upgrade"
|
||||||
"github.com/portainer/portainer/api/jwt"
|
"github.com/portainer/portainer/api/jwt"
|
||||||
"github.com/portainer/portainer/api/kubernetes"
|
"github.com/portainer/portainer/api/kubernetes"
|
||||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
"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/scheduler"
|
"github.com/portainer/portainer/api/scheduler"
|
||||||
"github.com/portainer/portainer/api/stacks"
|
"github.com/portainer/portainer/api/stacks/deployments"
|
||||||
|
"github.com/portainer/portainer/pkg/featureflags"
|
||||||
|
"github.com/portainer/portainer/pkg/libhelm"
|
||||||
|
"github.com/portainer/portainer/pkg/libstack"
|
||||||
|
"github.com/portainer/portainer/pkg/libstack/compose"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initCLI() *portainer.CLIFlags {
|
func initCLI() *portainer.CLIFlags {
|
||||||
var cliService portainer.CLIService = &cli.Service{}
|
var cliService portainer.CLIService = &cli.Service{}
|
||||||
flags, err := cliService.ParseFlags(portainer.APIVersion)
|
flags, err := cliService.ParseFlags(portainer.APIVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed parsing flags: %v", err)
|
log.Fatal().Err(err).Msg("failed parsing flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cliService.ValidateFlags(flags)
|
err = cliService.ValidateFlags(flags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed validating flags:%v", err)
|
log.Fatal().Err(err).Msg("failed validating flags")
|
||||||
}
|
}
|
||||||
|
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
func initFileService(dataStorePath string) portainer.FileService {
|
func initFileService(dataStorePath string) portainer.FileService {
|
||||||
fileService, err := filesystem.NewService(dataStorePath, "")
|
fileService, err := filesystem.NewService(dataStorePath, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed creating file service: %v", err)
|
log.Fatal().Err(err).Msg("failed creating file service")
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileService
|
return fileService
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
|
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
|
||||||
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
|
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("failed creating database connection: %s", err)
|
log.Fatal().Err(err).Msg("failed creating database connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
if bconn, ok := connection.(*boltdb.DbConnection); ok {
|
if bconn, ok := connection.(*boltdb.DbConnection); ok {
|
||||||
@@ -78,80 +89,78 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
|
|||||||
bconn.MaxBatchDelay = *flags.MaxBatchDelay
|
bconn.MaxBatchDelay = *flags.MaxBatchDelay
|
||||||
bconn.InitialMmapSize = *flags.InitialMmapSize
|
bconn.InitialMmapSize = *flags.InitialMmapSize
|
||||||
} else {
|
} else {
|
||||||
logrus.Fatalf("failed creating database connection: expecting a boltdb database type but a different one was received")
|
log.Fatal().Msg("failed creating database connection: expecting a boltdb database type but a different one was received")
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
logrus.Fatalf("Failed opening store: %v", err)
|
log.Fatal().Err(err).Msg("failed opening store")
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.Rollback {
|
if *flags.Rollback {
|
||||||
err := store.Rollback(false)
|
err := store.Rollback(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed rolling back: %v", err)
|
log.Fatal().Err(err).Msg("failed rolling back")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Println("Exiting rollback")
|
log.Info().Msg("exiting rollback")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init sets some defaults - it's basically a migration
|
// Init sets some defaults - it's basically a migration
|
||||||
err = store.Init()
|
err = store.Init()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed initializing data store: %v", err)
|
log.Fatal().Err(err).Msg("failed initializing data store")
|
||||||
}
|
}
|
||||||
|
|
||||||
if isNew {
|
if isNew {
|
||||||
// from MigrateData
|
instanceId, err := uuid.NewV4()
|
||||||
store.VersionService.StoreDBVersion(portainer.DBVersion)
|
|
||||||
|
|
||||||
err := updateSettingsFromFlags(store, flags)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed updating settings from flags: %v", err)
|
log.Fatal().Err(err).Msg("failed generating instance id")
|
||||||
|
}
|
||||||
|
|
||||||
|
migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{})
|
||||||
|
migratorCount := migratorInstance.GetMigratorCountOfCurrentAPIVersion()
|
||||||
|
|
||||||
|
// from MigrateData
|
||||||
|
v := models.Version{
|
||||||
|
SchemaVersion: portainer.APIVersion,
|
||||||
|
Edition: int(portainer.PortainerCE),
|
||||||
|
InstanceID: instanceId.String(),
|
||||||
|
MigratorCount: migratorCount,
|
||||||
|
}
|
||||||
|
store.VersionService.UpdateVersion(&v)
|
||||||
|
|
||||||
|
err = updateSettingsFromFlags(store, flags)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed updating settings from flags")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
storedVersion, err := store.VersionService.DBVersion()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatalf("Something Failed during creation of new database: %v", err)
|
|
||||||
}
|
|
||||||
if storedVersion != portainer.DBVersion {
|
|
||||||
err = store.MigrateData()
|
err = store.MigrateData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed migration: %v", err)
|
log.Fatal().Err(err).Msg("failed migration")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = updateSettingsFromFlags(store, flags)
|
err = updateSettingsFromFlags(store, flags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed updating settings from flags: %v", err)
|
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()
|
||||||
|
|
||||||
exportFilename := path.Join(*flags.Data, fmt.Sprintf("export-%d.json", time.Now().Unix()))
|
|
||||||
|
|
||||||
err := store.Export(exportFilename)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Debugf("Failed to export to %s", exportFilename)
|
|
||||||
} else {
|
|
||||||
logrus.Debugf("exported to %s", exportFilename)
|
|
||||||
}
|
|
||||||
connection.Close()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||||
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
|
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed creating compose manager: %v", err)
|
log.Fatal().Err(err).Msg("failed creating compose manager")
|
||||||
}
|
}
|
||||||
|
|
||||||
return composeWrapper
|
return composeWrapper
|
||||||
@@ -181,10 +190,15 @@ func initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initJWTService(userSessionTimeout string, dataStore dataservices.DataStore) (dataservices.JWTService, error) {
|
func initJWTService(userSessionTimeout string, dataStore dataservices.DataStore) (dataservices.JWTService, error) {
|
||||||
|
if userSessionTimeout == "" {
|
||||||
|
userSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||||
|
}
|
||||||
|
|
||||||
jwtService, err := jwt.NewService(userSessionTimeout, dataStore)
|
jwtService, err := jwt.NewService(userSessionTimeout, dataStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return jwtService, nil
|
return jwtService, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,8 +218,8 @@ func initOAuthService() portainer.OAuthService {
|
|||||||
return oauth.NewService()
|
return oauth.NewService()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initGitService() portainer.GitService {
|
func initGitService(ctx context.Context) portainer.GitService {
|
||||||
return git.NewService()
|
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) {
|
||||||
@@ -225,15 +239,21 @@ func initSSLService(addr, certPath, keyPath string, fileService portainer.FileSe
|
|||||||
return sslService, nil
|
return sslService, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
|
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *dockerclient.ClientFactory {
|
||||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
return dockerclient.NewClientFactory(signatureService, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory {
|
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
|
||||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
|
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSnapshotService(snapshotIntervalFromFlag string, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
|
func initSnapshotService(
|
||||||
|
snapshotIntervalFromFlag string,
|
||||||
|
dataStore dataservices.DataStore,
|
||||||
|
dockerClientFactory *dockerclient.ClientFactory,
|
||||||
|
kubernetesClientFactory *kubecli.ClientFactory,
|
||||||
|
shutdownCtx context.Context,
|
||||||
|
) (portainer.SnapshotService, error) {
|
||||||
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
|
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
|
||||||
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
|
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
|
||||||
|
|
||||||
@@ -300,55 +320,7 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
|
|||||||
sslSettings.HTTPEnabled = true
|
sslSettings.HTTPEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
|
return dataStore.SSLSettings().UpdateSettings(sslSettings)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// enableFeaturesFromFlags turns on or off feature flags
|
|
||||||
// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true)
|
|
||||||
// note, settings are persisted to the DB. To turn off `--feat open-amt=false`
|
|
||||||
func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
|
|
||||||
settings, err := dataStore.Settings().Settings()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if settings.FeatureFlagSettings == nil {
|
|
||||||
settings.FeatureFlagSettings = make(map[portainer.Feature]bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
// loop through feature flags to check if they are supported
|
|
||||||
for _, feat := range *flags.FeatureFlags {
|
|
||||||
var correspondingFeature *portainer.Feature
|
|
||||||
for i, supportedFeat := range portainer.SupportedFeatureFlags {
|
|
||||||
if strings.EqualFold(feat.Name, string(supportedFeat)) {
|
|
||||||
correspondingFeature = &portainer.SupportedFeatureFlags[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if correspondingFeature == nil {
|
|
||||||
return fmt.Errorf("unknown feature flag '%s'", feat.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
featureState, err := strconv.ParseBool(feat.Value)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if featureState {
|
|
||||||
logrus.Printf("Feature %v : on", *correspondingFeature)
|
|
||||||
} else {
|
|
||||||
logrus.Printf("Feature %v : off", *correspondingFeature)
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.FeatureFlagSettings[*correspondingFeature] = featureState
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataStore.Settings().UpdateSettings(settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||||
@@ -371,7 +343,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
|
|||||||
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
|
||||||
existingKeyPair, err := fileService.KeyPairFilesExist()
|
existingKeyPair, err := fileService.KeyPairFilesExist()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed checking for existing key pair: %v", err)
|
log.Fatal().Err(err).Msg("failed checking for existing key pair")
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingKeyPair {
|
if existingKeyPair {
|
||||||
@@ -380,146 +352,13 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D
|
|||||||
return generateAndStoreKeyPair(fileService, signatureService)
|
return generateAndStoreKeyPair(fileService, signatureService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
|
||||||
tlsConfiguration := portainer.TLSConfiguration{
|
|
||||||
TLS: *flags.TLS,
|
|
||||||
TLSSkipVerify: *flags.TLSSkipVerify,
|
|
||||||
}
|
|
||||||
|
|
||||||
if *flags.TLS {
|
|
||||||
tlsConfiguration.TLSCACertPath = *flags.TLSCacert
|
|
||||||
tlsConfiguration.TLSCertPath = *flags.TLSCert
|
|
||||||
tlsConfiguration.TLSKeyPath = *flags.TLSKey
|
|
||||||
} else if !*flags.TLS && *flags.TLSSkipVerify {
|
|
||||||
tlsConfiguration.TLS = true
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointID := dataStore.Endpoint().GetNextIdentifier()
|
|
||||||
endpoint := &portainer.Endpoint{
|
|
||||||
ID: portainer.EndpointID(endpointID),
|
|
||||||
Name: "primary",
|
|
||||||
URL: *flags.EndpointURL,
|
|
||||||
GroupID: portainer.EndpointGroupID(1),
|
|
||||||
Type: portainer.DockerEnvironment,
|
|
||||||
TLSConfig: tlsConfiguration,
|
|
||||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
|
||||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
|
||||||
TagIDs: []portainer.TagID{},
|
|
||||||
Status: portainer.EndpointStatusUp,
|
|
||||||
Snapshots: []portainer.DockerSnapshot{},
|
|
||||||
Kubernetes: portainer.KubernetesDefault(),
|
|
||||||
|
|
||||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
|
||||||
AllowVolumeBrowserForRegularUsers: false,
|
|
||||||
EnableHostManagementFeatures: false,
|
|
||||||
|
|
||||||
AllowSysctlSettingForRegularUsers: true,
|
|
||||||
AllowBindMountsForRegularUsers: true,
|
|
||||||
AllowPrivilegedModeForRegularUsers: true,
|
|
||||||
AllowHostNamespaceForRegularUsers: true,
|
|
||||||
AllowContainerCapabilitiesForRegularUsers: true,
|
|
||||||
AllowDeviceMappingForRegularUsers: true,
|
|
||||||
AllowStackManagementForRegularUsers: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
|
||||||
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(tlsConfiguration.TLSCACertPath, tlsConfiguration.TLSCertPath, tlsConfiguration.TLSKeyPath, tlsConfiguration.TLSSkipVerify)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
agentOnDockerEnvironment, err := client.ExecutePingOperation(endpoint.URL, tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if agentOnDockerEnvironment {
|
|
||||||
endpoint.Type = portainer.AgentOnDockerEnvironment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataStore.Endpoint().Create(endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
|
||||||
if strings.HasPrefix(endpointURL, "tcp://") {
|
|
||||||
_, err := client.ExecutePingOperation(endpointURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointID := dataStore.Endpoint().GetNextIdentifier()
|
|
||||||
endpoint := &portainer.Endpoint{
|
|
||||||
ID: portainer.EndpointID(endpointID),
|
|
||||||
Name: "primary",
|
|
||||||
URL: endpointURL,
|
|
||||||
GroupID: portainer.EndpointGroupID(1),
|
|
||||||
Type: portainer.DockerEnvironment,
|
|
||||||
TLSConfig: portainer.TLSConfiguration{},
|
|
||||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
|
||||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
|
||||||
TagIDs: []portainer.TagID{},
|
|
||||||
Status: portainer.EndpointStatusUp,
|
|
||||||
Snapshots: []portainer.DockerSnapshot{},
|
|
||||||
Kubernetes: portainer.KubernetesDefault(),
|
|
||||||
|
|
||||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
|
||||||
AllowVolumeBrowserForRegularUsers: false,
|
|
||||||
EnableHostManagementFeatures: false,
|
|
||||||
|
|
||||||
AllowSysctlSettingForRegularUsers: true,
|
|
||||||
AllowBindMountsForRegularUsers: true,
|
|
||||||
AllowPrivilegedModeForRegularUsers: true,
|
|
||||||
AllowHostNamespaceForRegularUsers: true,
|
|
||||||
AllowContainerCapabilitiesForRegularUsers: true,
|
|
||||||
AllowDeviceMappingForRegularUsers: true,
|
|
||||||
AllowStackManagementForRegularUsers: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dataStore.Endpoint().Create(endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, snapshotService portainer.SnapshotService) error {
|
|
||||||
if *flags.EndpointURL == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints, err := dataStore.Endpoint().Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(endpoints) > 0 {
|
|
||||||
logrus.Println("Instance already has defined environments. Skipping the environment defined via CLI.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if *flags.TLS || *flags.TLSSkipVerify {
|
|
||||||
return createTLSSecuredEndpoint(flags, dataStore, snapshotService)
|
|
||||||
}
|
|
||||||
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadEncryptionSecretKey(keyfilename string) []byte {
|
func loadEncryptionSecretKey(keyfilename string) []byte {
|
||||||
content, err := os.ReadFile(path.Join("/run/secrets", keyfilename))
|
content, err := os.ReadFile(path.Join("/run/secrets", keyfilename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
logrus.Printf("Encryption key file `%s` not present", keyfilename)
|
log.Info().Str("filename", keyfilename).Msg("encryption key file not present")
|
||||||
} else {
|
} else {
|
||||||
logrus.Printf("Error reading encryption key file: %v", err)
|
log.Info().Err(err).Msg("error reading encryption key file")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -533,70 +372,76 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
|
|||||||
func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
|
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
if flags.FeatureFlags != nil {
|
||||||
|
featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
|
||||||
|
}
|
||||||
|
|
||||||
fileService := initFileService(*flags.Data)
|
fileService := initFileService(*flags.Data)
|
||||||
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
|
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
|
||||||
if encryptionKey == nil {
|
if encryptionKey == nil {
|
||||||
logrus.Println("Proceeding without encryption key")
|
log.Info().Msg("proceeding without encryption key")
|
||||||
}
|
}
|
||||||
|
|
||||||
dataStore := initDataStore(flags, encryptionKey, fileService, shutdownCtx)
|
dataStore := initDataStore(flags, encryptionKey, fileService, shutdownCtx)
|
||||||
|
|
||||||
if err := dataStore.CheckCurrentEdition(); err != nil {
|
if err := dataStore.CheckCurrentEdition(); err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceID, err := dataStore.Version().InstanceID()
|
instanceID, err := dataStore.Version().InstanceID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed getting instance id: %v", err)
|
log.Fatal().Err(err).Msg("failed getting instance id")
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKeyService := initAPIKeyService(dataStore)
|
apiKeyService := initAPIKeyService(dataStore)
|
||||||
|
|
||||||
settings, err := dataStore.Settings().Settings()
|
settings, err := dataStore.Settings().Settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
|
||||||
jwtService, err := initJWTService(settings.UserSessionTimeout, dataStore)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatalf("Failed initializing JWT service: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = enableFeaturesFromFlags(dataStore, flags)
|
jwtService, err := initJWTService(settings.UserSessionTimeout, dataStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed enabling feature flag: %v", err)
|
log.Fatal().Err(err).Msg("failed initializing JWT service")
|
||||||
}
|
}
|
||||||
|
|
||||||
ldapService := initLDAPService()
|
ldapService := initLDAPService()
|
||||||
|
|
||||||
oauthService := initOAuthService()
|
oauthService := initOAuthService()
|
||||||
gitService := initGitService()
|
|
||||||
|
gitService := initGitService(shutdownCtx)
|
||||||
|
|
||||||
openAMTService := openamt.NewService()
|
openAMTService := openamt.NewService()
|
||||||
|
|
||||||
cryptoService := initCryptoService()
|
cryptoService := initCryptoService()
|
||||||
|
|
||||||
digitalSignatureService := initDigitalSignatureService()
|
digitalSignatureService := initDigitalSignatureService()
|
||||||
|
|
||||||
|
edgeStacksService := edgestacks.NewService(dataStore)
|
||||||
|
|
||||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
log.Fatal().Err(err).Msg("")
|
||||||
}
|
}
|
||||||
|
|
||||||
sslSettings, err := sslService.GetSSLSettings()
|
sslSettings, err := sslService.GetSSLSettings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed to get ssl settings: %s", err)
|
log.Fatal().Err(err).Msg("failed to get SSL settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = initKeyPair(fileService, digitalSignatureService)
|
err = initKeyPair(fileService, digitalSignatureService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed initializing key pair: %v", err)
|
log.Fatal().Err(err).Msg("failed initializing key pair")
|
||||||
}
|
}
|
||||||
|
|
||||||
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
|
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx, fileService)
|
||||||
|
|
||||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
|
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
|
||||||
|
|
||||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
|
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed initializing snapshot service: %v", err)
|
log.Fatal().Err(err).Msg("failed initializing snapshot service")
|
||||||
}
|
}
|
||||||
snapshotService.Start()
|
snapshotService.Start()
|
||||||
|
|
||||||
@@ -607,47 +452,61 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
|
|
||||||
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)
|
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
|
||||||
|
|
||||||
reverseTunnelService.ProxyManager = proxyManager
|
reverseTunnelService.ProxyManager = proxyManager
|
||||||
|
|
||||||
dockerConfigPath := fileService.GetDockerConfigPath()
|
dockerConfigPath := fileService.GetDockerConfigPath()
|
||||||
|
|
||||||
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
|
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed initializing compose deployer")
|
||||||
|
}
|
||||||
|
|
||||||
|
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
|
||||||
|
|
||||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
|
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed initializing swarm stack manager: %v", err)
|
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, digitalSignatureService, proxyManager, *flags.Assets)
|
||||||
|
|
||||||
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
|
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed initializing helm package manager: %v", err)
|
log.Fatal().Err(err).Msg("failed initializing helm package manager")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
|
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed loading edge jobs from database: %v", err)
|
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationStatus := initStatus(instanceID)
|
applicationStatus := initStatus(instanceID)
|
||||||
|
|
||||||
err = initEndpoint(flags, dataStore, snapshotService)
|
demoService := demo.NewService()
|
||||||
|
if *flags.DemoEnvironment {
|
||||||
|
err := demoService.Init(dataStore, cryptoService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed initializing environment: %v", err)
|
log.Fatal().Err(err).Msg("failed initializing demo environment")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel to control when the admin user is created
|
||||||
|
adminCreationDone := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
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 {
|
||||||
logrus.Fatalf("Failed getting admin password file: %v", err)
|
log.Fatal().Err(err).Msg("failed getting admin password file")
|
||||||
}
|
}
|
||||||
|
|
||||||
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
|
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed hashing admin password: %v", err)
|
log.Fatal().Err(err).Msg("failed hashing admin password")
|
||||||
}
|
}
|
||||||
} else if *flags.AdminPassword != "" {
|
} else if *flags.AdminPassword != "" {
|
||||||
adminPasswordHash = *flags.AdminPassword
|
adminPasswordHash = *flags.AdminPassword
|
||||||
@@ -656,38 +515,60 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
if adminPasswordHash != "" {
|
if adminPasswordHash != "" {
|
||||||
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
|
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed getting admin user: %v", err)
|
log.Fatal().Err(err).Msg("failed getting admin user")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
logrus.Println("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)
|
err := dataStore.User().Create(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed creating admin user: %v", err)
|
log.Fatal().Err(err).Msg("failed creating admin user")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// notify the admin user is created, the endpoint initialization can start
|
||||||
|
adminCreationDone <- struct{}{}
|
||||||
} else {
|
} else {
|
||||||
logrus.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
|
log.Info().Msg("instance already has an administrator user defined, skipping admin password related flags.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
|
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed starting tunnel server: %v", err)
|
log.Fatal().Err(err).Msg("failed starting tunnel server")
|
||||||
}
|
|
||||||
|
|
||||||
sslDBSettings, err := dataStore.SSLSettings().Settings()
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatalf("Failed to fetch ssl settings from DB")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduler := scheduler.NewScheduler(shutdownCtx)
|
scheduler := scheduler.NewScheduler(shutdownCtx)
|
||||||
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
|
stackDeployer := deployments.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer, dockerClientFactory, dataStore)
|
||||||
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
|
deployments.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
|
||||||
|
|
||||||
|
sslDBSettings, err := dataStore.SSLSettings().Settings()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Msg("failed to fetch SSL settings from DB")
|
||||||
|
}
|
||||||
|
|
||||||
|
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failed initializing upgrade service")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our normal migrations run as part of the database initialization
|
||||||
|
// but some more complex migrations require access to a kubernetes or docker
|
||||||
|
// client. Therefore we run a separate migration process just before
|
||||||
|
// starting the server.
|
||||||
|
postInitMigrator := datastore.NewPostInitMigrator(
|
||||||
|
kubernetesClientFactory,
|
||||||
|
dockerClientFactory,
|
||||||
|
dataStore,
|
||||||
|
)
|
||||||
|
if err := postInitMigrator.PostInitMigrate(); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("failure during post init migrations")
|
||||||
|
}
|
||||||
|
|
||||||
return &http.Server{
|
return &http.Server{
|
||||||
AuthorizationService: authorizationService,
|
AuthorizationService: authorizationService,
|
||||||
@@ -698,12 +579,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
HTTPEnabled: sslDBSettings.HTTPEnabled,
|
HTTPEnabled: sslDBSettings.HTTPEnabled,
|
||||||
AssetsPath: *flags.Assets,
|
AssetsPath: *flags.Assets,
|
||||||
DataStore: dataStore,
|
DataStore: dataStore,
|
||||||
|
EdgeStacksService: edgeStacksService,
|
||||||
SwarmStackManager: swarmStackManager,
|
SwarmStackManager: swarmStackManager,
|
||||||
ComposeStackManager: composeStackManager,
|
ComposeStackManager: composeStackManager,
|
||||||
KubernetesDeployer: kubernetesDeployer,
|
KubernetesDeployer: kubernetesDeployer,
|
||||||
HelmPackageManager: helmPackageManager,
|
HelmPackageManager: helmPackageManager,
|
||||||
CryptoService: cryptoService,
|
|
||||||
APIKeyService: apiKeyService,
|
APIKeyService: apiKeyService,
|
||||||
|
CryptoService: cryptoService,
|
||||||
JWTService: jwtService,
|
JWTService: jwtService,
|
||||||
FileService: fileService,
|
FileService: fileService,
|
||||||
LDAPService: ldapService,
|
LDAPService: ldapService,
|
||||||
@@ -722,18 +604,36 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||||||
ShutdownCtx: shutdownCtx,
|
ShutdownCtx: shutdownCtx,
|
||||||
ShutdownTrigger: shutdownTrigger,
|
ShutdownTrigger: shutdownTrigger,
|
||||||
StackDeployer: stackDeployer,
|
StackDeployer: stackDeployer,
|
||||||
|
DemoService: demoService,
|
||||||
|
UpgradeService: upgradeService,
|
||||||
|
AdminCreationDone: adminCreationDone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flags := initCLI()
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
configureLogger()
|
configureLogger()
|
||||||
|
setLoggingMode("PRETTY")
|
||||||
|
|
||||||
|
flags := initCLI()
|
||||||
|
|
||||||
|
setLoggingLevel(*flags.LogLevel)
|
||||||
|
setLoggingMode(*flags.LogMode)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
server := buildServer(flags)
|
server := buildServer(flags)
|
||||||
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
|
log.Info().
|
||||||
|
Str("version", portainer.APIVersion).
|
||||||
|
Str("build_number", build.BuildNumber).
|
||||||
|
Str("image_tag", build.ImageTag).
|
||||||
|
Str("nodejs_version", build.NodejsVersion).
|
||||||
|
Str("yarn_version", build.YarnVersion).
|
||||||
|
Str("webpack_version", build.WebpackVersion).
|
||||||
|
Str("go_version", build.GoVersion).
|
||||||
|
Msg("starting Portainer")
|
||||||
|
|
||||||
err := server.Start()
|
err := server.Start()
|
||||||
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
|
log.Info().Err(err).Msg("HTTP server exited")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/cli"
|
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
|
||||||
"github.com/portainer/portainer/api/datastore"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"gopkg.in/alecthomas/kingpin.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockKingpinSetting string
|
|
||||||
|
|
||||||
func (m mockKingpinSetting) SetValue(value kingpin.Value) {
|
|
||||||
value.Set(string(m))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_enableFeaturesFromFlags(t *testing.T) {
|
|
||||||
is := assert.New(t)
|
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
featureFlag string
|
|
||||||
isSupported bool
|
|
||||||
}{
|
|
||||||
{"test", false},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
|
|
||||||
mockKingpinSetting := mockKingpinSetting(test.featureFlag)
|
|
||||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
|
||||||
err := enableFeaturesFromFlags(store, flags)
|
|
||||||
if test.isSupported {
|
|
||||||
is.NoError(err)
|
|
||||||
} else {
|
|
||||||
is.Error(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("passes for all supported feature flags", func(t *testing.T) {
|
|
||||||
for _, flag := range portainer.SupportedFeatureFlags {
|
|
||||||
mockKingpinSetting := mockKingpinSetting(flag)
|
|
||||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
|
||||||
err := enableFeaturesFromFlags(store, flags)
|
|
||||||
is.NoError(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const FeatTest portainer.Feature = "optional-test"
|
|
||||||
|
|
||||||
func optionalFunc(dataStore dataservices.DataStore) string {
|
|
||||||
|
|
||||||
// TODO: this is a code smell - finding out if a feature flag is enabled should not require having access to the store, and asking for a settings obj.
|
|
||||||
// ideally, the `if` should look more like:
|
|
||||||
// if featureflags.FlagEnabled(FeatTest) {}
|
|
||||||
settings, err := dataStore.Settings().Settings()
|
|
||||||
if err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
if settings.FeatureFlagSettings[FeatTest] {
|
|
||||||
return "enabled"
|
|
||||||
}
|
|
||||||
return "disabled"
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_optionalFeature(t *testing.T) {
|
|
||||||
portainer.SupportedFeatureFlags = append(portainer.SupportedFeatureFlags, FeatTest)
|
|
||||||
|
|
||||||
is := assert.New(t)
|
|
||||||
|
|
||||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
|
||||||
defer teardown()
|
|
||||||
|
|
||||||
// Enable the test feature
|
|
||||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
|
||||||
mockKingpinSetting := mockKingpinSetting(FeatTest)
|
|
||||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
|
||||||
err := enableFeaturesFromFlags(store, flags)
|
|
||||||
is.NoError(err)
|
|
||||||
is.Equal("enabled", optionalFunc(store))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Same store, so the feature flag should still be enabled
|
|
||||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
|
||||||
is.Equal("enabled", optionalFunc(store))
|
|
||||||
})
|
|
||||||
|
|
||||||
// disable the test feature
|
|
||||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
|
||||||
mockKingpinSetting := mockKingpinSetting(FeatTest + "=false")
|
|
||||||
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
|
|
||||||
err := enableFeaturesFromFlags(store, flags)
|
|
||||||
is.NoError(err)
|
|
||||||
is.Equal("disabled", optionalFunc(store))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Same store, so feature flag should still be disabled
|
|
||||||
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
|
|
||||||
is.Equal("disabled", optionalFunc(store))
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,35 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ReadTransaction interface {
|
||||||
|
GetObject(bucketName string, key []byte, object interface{}) error
|
||||||
|
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||||
|
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||||
|
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Transaction interface {
|
||||||
|
ReadTransaction
|
||||||
|
|
||||||
|
SetServiceName(bucketName string) error
|
||||||
|
UpdateObject(bucketName string, key []byte, object interface{}) error
|
||||||
|
DeleteObject(bucketName string, key []byte) error
|
||||||
|
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
|
||||||
|
CreateObjectWithId(bucketName string, id int, obj interface{}) error
|
||||||
|
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
|
||||||
|
DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error
|
||||||
|
GetNextIdentifier(bucketName string) int
|
||||||
|
}
|
||||||
|
|
||||||
type Connection interface {
|
type Connection interface {
|
||||||
|
Transaction
|
||||||
|
|
||||||
Open() error
|
Open() error
|
||||||
Close() error
|
Close() error
|
||||||
|
|
||||||
|
UpdateTx(fn func(Transaction) error) error
|
||||||
|
ViewTx(fn func(Transaction) error) error
|
||||||
|
|
||||||
// write the db contents to filename as json (the schema needs defining)
|
// write the db contents to filename as json (the schema needs defining)
|
||||||
ExportRaw(filename string) error
|
ExportRaw(filename string) error
|
||||||
|
|
||||||
@@ -21,20 +46,9 @@ type Connection interface {
|
|||||||
NeedsEncryptionMigration() (bool, error)
|
NeedsEncryptionMigration() (bool, error)
|
||||||
SetEncrypted(encrypted bool)
|
SetEncrypted(encrypted bool)
|
||||||
|
|
||||||
SetServiceName(bucketName string) error
|
|
||||||
GetObject(bucketName string, key []byte, object interface{}) error
|
|
||||||
UpdateObject(bucketName string, key []byte, object interface{}) error
|
|
||||||
DeleteObject(bucketName string, key []byte) error
|
|
||||||
DeleteAllObjects(bucketName string, matching func(o interface{}) (id int, ok bool)) error
|
|
||||||
GetNextIdentifier(bucketName string) int
|
|
||||||
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
|
|
||||||
CreateObjectWithId(bucketName string, id int, obj interface{}) error
|
|
||||||
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
|
|
||||||
CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error
|
|
||||||
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
|
||||||
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
|
|
||||||
ConvertToKey(v int) []byte
|
|
||||||
|
|
||||||
BackupMetadata() (map[string]interface{}, error)
|
BackupMetadata() (map[string]interface{}, error)
|
||||||
RestoreMetadata(s map[string]interface{}) error
|
RestoreMetadata(s map[string]interface{}) error
|
||||||
|
|
||||||
|
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
|
||||||
|
ConvertToKey(v int) []byte
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
// Person with better knowledge is welcomed to improve it.
|
// Person with better knowledge is welcomed to improve it.
|
||||||
// sourced from https://golang.org/src/crypto/cipher/example_test.go
|
// sourced from https://golang.org/src/crypto/cipher/example_test.go
|
||||||
|
|
||||||
var emptySalt []byte = make([]byte, 0, 0)
|
var emptySalt []byte = make([]byte, 0)
|
||||||
|
|
||||||
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
|
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
|
||||||
// passphrase is used to generate an encryption key.
|
// passphrase is used to generate an encryption key.
|
||||||
|
|||||||
@@ -2,18 +2,15 @@ package crypto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
||||||
tmpdir, _ := ioutils.TempDir("", "encrypt")
|
tmpdir := t.TempDir()
|
||||||
defer os.RemoveAll(tmpdir)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
originFilePath = filepath.Join(tmpdir, "origin")
|
originFilePath = filepath.Join(tmpdir, "origin")
|
||||||
@@ -22,7 +19,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
content := []byte("content")
|
content := []byte("content")
|
||||||
ioutil.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
defer originFile.Close()
|
defer originFile.Close()
|
||||||
@@ -32,7 +29,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
|||||||
|
|
||||||
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")
|
||||||
encryptedContent, err := ioutil.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")
|
||||||
|
|
||||||
@@ -47,13 +44,12 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
|
|||||||
|
|
||||||
io.Copy(decryptedFileWriter, decryptedReader)
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
|
|
||||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
||||||
tmpdir, _ := ioutils.TempDir("", "encrypt")
|
tmpdir := t.TempDir()
|
||||||
defer os.RemoveAll(tmpdir)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
originFilePath = filepath.Join(tmpdir, "origin")
|
originFilePath = filepath.Join(tmpdir, "origin")
|
||||||
@@ -62,7 +58,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
content := []byte("content")
|
content := []byte("content")
|
||||||
ioutil.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
defer originFile.Close()
|
defer originFile.Close()
|
||||||
@@ -72,7 +68,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
|||||||
|
|
||||||
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
|
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
|
||||||
assert.Nil(t, err, "Failed to encrypt a file")
|
assert.Nil(t, err, "Failed to encrypt a file")
|
||||||
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
|
encryptedContent, err := os.ReadFile(encryptedFilePath)
|
||||||
assert.Nil(t, err, "Couldn't read encrypted file")
|
assert.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")
|
||||||
|
|
||||||
@@ -87,13 +83,12 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
|
|||||||
|
|
||||||
io.Copy(decryptedFileWriter, decryptedReader)
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
|
|
||||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
|
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
|
||||||
tmpdir, _ := ioutils.TempDir("", "encrypt")
|
tmpdir := t.TempDir()
|
||||||
defer os.RemoveAll(tmpdir)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
originFilePath = filepath.Join(tmpdir, "origin")
|
originFilePath = filepath.Join(tmpdir, "origin")
|
||||||
@@ -102,7 +97,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
|||||||
)
|
)
|
||||||
|
|
||||||
content := []byte("content")
|
content := []byte("content")
|
||||||
ioutil.WriteFile(originFilePath, content, 0600)
|
os.WriteFile(originFilePath, content, 0600)
|
||||||
|
|
||||||
originFile, _ := os.Open(originFilePath)
|
originFile, _ := os.Open(originFilePath)
|
||||||
defer originFile.Close()
|
defer originFile.Close()
|
||||||
@@ -112,7 +107,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
|||||||
|
|
||||||
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")
|
||||||
encryptedContent, err := ioutil.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")
|
||||||
|
|
||||||
@@ -127,6 +122,6 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
|
|||||||
|
|
||||||
io.Copy(decryptedFileWriter, decryptedReader)
|
io.Copy(decryptedFileWriter, decryptedReader)
|
||||||
|
|
||||||
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
|
decryptedContent, _ := os.ReadFile(decryptedFilePath)
|
||||||
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
|
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/portainer/libcrypto"
|
"github.com/portainer/libcrypto"
|
||||||
)
|
)
|
||||||
@@ -115,9 +114,6 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
|||||||
|
|
||||||
hash := libcrypto.HashFromBytes([]byte(message))
|
hash := libcrypto.HashFromBytes([]byte(message))
|
||||||
|
|
||||||
r := big.NewInt(0)
|
|
||||||
s := big.NewInt(0)
|
|
||||||
|
|
||||||
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package crypto
|
|||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"io/ioutil"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
|
// CreateTLSConfiguration creates a basic tls.Config with recommended TLS settings
|
||||||
func CreateServerTLSConfiguration() *tls.Config {
|
func CreateTLSConfiguration() *tls.Config {
|
||||||
return &tls.Config{
|
return &tls.Config{
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
CipherSuites: []uint16{
|
CipherSuites: []uint16{
|
||||||
@@ -20,6 +20,8 @@ func CreateServerTLSConfiguration() *tls.Config {
|
|||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
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_256_GCM_SHA384,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +29,7 @@ func CreateServerTLSConfiguration() *tls.Config {
|
|||||||
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
|
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
|
||||||
// loaded from memory.
|
// loaded from memory.
|
||||||
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
|
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
|
||||||
config := &tls.Config{}
|
config := CreateTLSConfiguration()
|
||||||
config.InsecureSkipVerify = skipServerVerification
|
config.InsecureSkipVerify = skipServerVerification
|
||||||
|
|
||||||
if !skipClientVerification {
|
if !skipClientVerification {
|
||||||
@@ -50,7 +52,7 @@ func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerific
|
|||||||
// CreateTLSConfigurationFromDisk initializes a tls.Config using a CA certificate, a certificate and a key
|
// CreateTLSConfigurationFromDisk initializes a tls.Config using a CA certificate, a certificate and a key
|
||||||
// loaded from disk.
|
// loaded from disk.
|
||||||
func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipServerVerification bool) (*tls.Config, error) {
|
func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipServerVerification bool) (*tls.Config, error) {
|
||||||
config := &tls.Config{}
|
config := CreateTLSConfiguration()
|
||||||
config.InsecureSkipVerify = skipServerVerification
|
config.InsecureSkipVerify = skipServerVerification
|
||||||
|
|
||||||
if certPath != "" && keyPath != "" {
|
if certPath != "" && keyPath != "" {
|
||||||
@@ -63,7 +65,7 @@ func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !skipServerVerification && caCertPath != "" {
|
if !skipServerVerification && caCertPath != "" {
|
||||||
caCert, err := ioutil.ReadFile(caCertPath)
|
caCert, err := os.ReadFile(caCertPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -120,7 +122,7 @@ 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 {
|
||||||
|
|
||||||
logrus.Infof("Loading PortainerDB: %s", connection.GetDatabaseFileName())
|
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()
|
||||||
@@ -131,9 +133,11 @@ func (connection *DbConnection) Open() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
db.MaxBatchSize = connection.MaxBatchSize
|
db.MaxBatchSize = connection.MaxBatchSize
|
||||||
db.MaxBatchDelay = connection.MaxBatchDelay
|
db.MaxBatchDelay = connection.MaxBatchDelay
|
||||||
connection.DB = db
|
connection.DB = db
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +147,30 @@ func (connection *DbConnection) Close() error {
|
|||||||
if connection.DB != nil {
|
if connection.DB != nil {
|
||||||
return connection.DB.Close()
|
return connection.DB.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (connection *DbConnection) txFn(fn func(portainer.Transaction) error) func(*bolt.Tx) error {
|
||||||
|
return func(tx *bolt.Tx) error {
|
||||||
|
return fn(&DbTransaction{conn: connection, tx: tx})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTx executes the given function inside a read-write transaction
|
||||||
|
func (connection *DbConnection) UpdateTx(fn func(portainer.Transaction) error) error {
|
||||||
|
if connection.MaxBatchDelay > 0 && connection.MaxBatchSize > 1 {
|
||||||
|
return connection.Batch(connection.txFn(fn))
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection.Update(connection.txFn(fn))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewTx executes the given function inside a read-only transaction
|
||||||
|
func (connection *DbConnection) ViewTx(fn func(portainer.Transaction) error) error {
|
||||||
|
return connection.View(connection.txFn(fn))
|
||||||
|
}
|
||||||
|
|
||||||
// BackupTo backs up db to a provided writer.
|
// BackupTo backs up db to a provided writer.
|
||||||
// It does hot backup and doesn't block other database reads and writes
|
// It does hot backup and doesn't block other database reads and writes
|
||||||
func (connection *DbConnection) BackupTo(w io.Writer) error {
|
func (connection *DbConnection) BackupTo(w io.Writer) error {
|
||||||
@@ -158,14 +183,14 @@ func (connection *DbConnection) BackupTo(w io.Writer) error {
|
|||||||
func (connection *DbConnection) ExportRaw(filename string) error {
|
func (connection *DbConnection) ExportRaw(filename string) error {
|
||||||
databasePath := connection.GetDatabaseFilePath()
|
databasePath := connection.GetDatabaseFilePath()
|
||||||
if _, err := os.Stat(databasePath); err != nil {
|
if _, err := os.Stat(databasePath); err != nil {
|
||||||
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
|
return fmt.Errorf("stat on %s failed, error: %w", databasePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, err := connection.ExportJson(databasePath, true)
|
b, err := connection.ExportJSON(databasePath, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return ioutil.WriteFile(filename, b, 0600)
|
return os.WriteFile(filename, b, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertToKey returns an 8-byte big endian representation of v.
|
// ConvertToKey returns an 8-byte big endian representation of v.
|
||||||
@@ -177,36 +202,32 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBucket is a generic function used to create a bucket inside a database database.
|
// keyToString Converts a key to a string value suitable for logging
|
||||||
|
func keyToString(b []byte) string {
|
||||||
|
if len(b) != 8 {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := binary.BigEndian.Uint64(b)
|
||||||
|
if v <= math.MaxInt32 {
|
||||||
|
return fmt.Sprintf("%d", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBucket is a generic function used to create a bucket inside a database.
|
||||||
func (connection *DbConnection) SetServiceName(bucketName string) error {
|
func (connection *DbConnection) SetServiceName(bucketName string) error {
|
||||||
return connection.Batch(func(tx *bolt.Tx) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
|
return tx.SetServiceName(bucketName)
|
||||||
return err
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObject is a generic function used to retrieve an unmarshalled object from a database database.
|
// GetObject is a generic function used to retrieve an unmarshalled object from a database.
|
||||||
func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error {
|
func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error {
|
||||||
var data []byte
|
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
return tx.GetObject(bucketName, key, object)
|
||||||
err := connection.View(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
|
||||||
|
|
||||||
value := bucket.Get(key)
|
|
||||||
if value == nil {
|
|
||||||
return dserrors.ErrObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
data = make([]byte, len(value))
|
|
||||||
copy(data, value)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return connection.UnmarshalObjectWithJsoniter(data, object)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (connection *DbConnection) getEncryptionKey() []byte {
|
func (connection *DbConnection) getEncryptionKey() []byte {
|
||||||
@@ -217,50 +238,51 @@ func (connection *DbConnection) getEncryptionKey() []byte {
|
|||||||
return connection.EncryptionKey
|
return connection.EncryptionKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateObject is a generic function used to update an object inside a database database.
|
// UpdateObject is a generic function used to update an object inside a database.
|
||||||
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
||||||
data, err := connection.MarshalObject(object)
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return tx.UpdateObject(bucketName, key, object)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateObjectFunc is a generic function used to update an object safely without race conditions.
|
||||||
|
func (connection *DbConnection) UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error {
|
||||||
|
return connection.Batch(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
|
data := bucket.Get(key)
|
||||||
|
if data == nil {
|
||||||
|
return fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := connection.UnmarshalObjectWithJsoniter(data, object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFn()
|
||||||
|
|
||||||
|
data, err = connection.MarshalObject(object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return connection.Batch(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
|
||||||
return bucket.Put(key, data)
|
return bucket.Put(key, data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteObject is a generic function used to delete an object inside a database database.
|
// DeleteObject is a generic function used to delete an object inside a database.
|
||||||
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
|
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
|
||||||
return connection.Batch(func(tx *bolt.Tx) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
return tx.DeleteObject(bucketName, key)
|
||||||
return bucket.Delete(key)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, matching func(o interface{}) (id int, ok bool)) error {
|
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
|
||||||
return connection.Batch(func(tx *bolt.Tx) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
return tx.DeleteAllObjects(bucketName, obj, matching)
|
||||||
|
|
||||||
cursor := bucket.Cursor()
|
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
|
||||||
var obj interface{}
|
|
||||||
err := connection.UnmarshalObject(v, &obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if id, ok := matching(obj); ok {
|
|
||||||
err := bucket.Delete(connection.ConvertToKey(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,13 +290,8 @@ func (connection *DbConnection) DeleteAllObjects(bucketName string, matching fun
|
|||||||
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
|
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
|
||||||
var identifier int
|
var identifier int
|
||||||
|
|
||||||
connection.Batch(func(tx *bolt.Tx) error {
|
_ = connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
identifier = tx.GetNextIdentifier(bucketName)
|
||||||
id, err := bucket.NextSequence()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
identifier = int(id)
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -283,117 +300,51 @@ 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, interface{})) error {
|
||||||
return connection.Batch(func(tx *bolt.Tx) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
return tx.CreateObject(bucketName, fn)
|
||||||
|
|
||||||
seqId, _ := bucket.NextSequence()
|
|
||||||
id, obj := fn(seqId)
|
|
||||||
|
|
||||||
data, err := connection.MarshalObject(obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucket.Put(connection.ConvertToKey(int(id)), data)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 interface{}) error {
|
||||||
return connection.Batch(func(tx *bolt.Tx) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
return tx.CreateObjectWithId(bucketName, id, obj)
|
||||||
data, err := connection.MarshalObject(obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucket.Put(connection.ConvertToKey(id), data)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 interface{}) error {
|
||||||
return connection.Batch(func(tx *bolt.Tx) error {
|
return connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
return tx.CreateObjectWithStringId(bucketName, id, obj)
|
||||||
data, err := connection.MarshalObject(obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucket.Put(id, data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateObjectWithSetSequence creates a new object in the bucket, using the specified id, and sets the bucket sequence
|
|
||||||
// avoid this :)
|
|
||||||
func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error {
|
|
||||||
return connection.Batch(func(tx *bolt.Tx) error {
|
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
|
||||||
|
|
||||||
// We manually manage sequences for schedules
|
|
||||||
err := bucket.SetSequence(uint64(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := connection.MarshalObject(obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return bucket.Put(connection.ConvertToKey(id), data)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||||
err := connection.View(func(tx *bolt.Tx) error {
|
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
return tx.GetAll(bucketName, obj, append)
|
||||||
cursor := bucket.Cursor()
|
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
|
||||||
err := connection.UnmarshalObject(v, obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
obj, err = append(obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: decide which Unmarshal to use, and use one...
|
// TODO: decide which Unmarshal to use, and use one...
|
||||||
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
|
||||||
err := connection.View(func(tx *bolt.Tx) error {
|
return connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
return tx.GetAllWithJsoniter(bucketName, obj, append)
|
||||||
|
|
||||||
cursor := bucket.Cursor()
|
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
|
||||||
err := connection.UnmarshalObjectWithJsoniter(v, obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
obj, err = append(obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
|
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
|
||||||
buckets := map[string]interface{}{}
|
buckets := map[string]interface{}{}
|
||||||
|
|
||||||
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)
|
||||||
bucket = tx.Bucket([]byte(bucketName))
|
|
||||||
seqId := bucket.Sequence()
|
seqId := bucket.Sequence()
|
||||||
buckets[bucketName] = int(seqId)
|
buckets[bucketName] = int(seqId)
|
||||||
return nil
|
return nil
|
||||||
@@ -405,13 +356,14 @@ func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error)
|
|||||||
return buckets, err
|
return buckets, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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]interface{}) 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 {
|
||||||
logrus.Errorf("Failed to restore metadata to bucket %s, skipped", bucketName)
|
log.Error().Str("bucket", bucketName).Msg("failed to restore metadata to bucket, skipped")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +372,7 @@ func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return bucket.SetSequence(uint64(id))
|
return bucket.SetSequence(uint64(id))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/rs/zerolog/log"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,7 +14,6 @@ func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
|
|||||||
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)
|
||||||
bucket = tx.Bucket([]byte(bucketName))
|
|
||||||
seqId := bucket.Sequence()
|
seqId := bucket.Sequence()
|
||||||
buckets[bucketName] = int(seqId)
|
buckets[bucketName] = int(seqId)
|
||||||
return nil
|
return nil
|
||||||
@@ -28,11 +27,11 @@ func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
|
|||||||
|
|
||||||
// ExportJSON creates a JSON representation from a DbConnection. You can include
|
// ExportJSON creates a JSON representation from a DbConnection. You can include
|
||||||
// the database's metadata or ignore it. Ensure the database is closed before
|
// the database's metadata or ignore it. Ensure the database is closed before
|
||||||
// using this function
|
// using this function.
|
||||||
// inspired by github.com/konoui/boltdb-exporter (which has no license)
|
// inspired by github.com/konoui/boltdb-exporter (which has no license)
|
||||||
// but very much simplified, based on how we use boltdb
|
// but very much simplified, based on how we use boltdb
|
||||||
func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, error) {
|
func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, error) {
|
||||||
logrus.WithField("databasePath", databasePath).Infof("exportJson")
|
log.Debug().Str("databasePath", databasePath).Msg("exportJson")
|
||||||
|
|
||||||
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
|
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,8 +43,9 @@ func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, e
|
|||||||
if metadata {
|
if metadata {
|
||||||
meta, err := backupMetadata(connection)
|
meta, err := backupMetadata(connection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Errorf("Failed exporting metadata: %v", err)
|
log.Error().Err(err).Msg("failed exporting metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
backup["__metadata"] = meta
|
backup["__metadata"] = meta
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,22 +59,31 @@ func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, e
|
|||||||
if v == nil {
|
if v == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj interface{}
|
var obj interface{}
|
||||||
err := c.UnmarshalObject(v, &obj)
|
err := c.UnmarshalObject(v, &obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Errorf("Failed to unmarshal (bucket %s): %v", bucketName, string(v))
|
log.Error().
|
||||||
|
Str("bucket", bucketName).
|
||||||
|
Str("object", string(v)).
|
||||||
|
Err(err).
|
||||||
|
Msg("failed to unmarshal")
|
||||||
|
|
||||||
obj = v
|
obj = v
|
||||||
}
|
}
|
||||||
|
|
||||||
if bucketName == "version" {
|
if bucketName == "version" {
|
||||||
version[string(k)] = string(v)
|
version[string(k)] = string(v)
|
||||||
} else {
|
} else {
|
||||||
list = append(list, obj)
|
list = append(list, obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bucketName == "version" {
|
if bucketName == "version" {
|
||||||
backup[bucketName] = version
|
backup[bucketName] = version
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(list) > 0 {
|
if len(list) > 0 {
|
||||||
if bucketName == "ssl" ||
|
if bucketName == "ssl" ||
|
||||||
bucketName == "settings" ||
|
bucketName == "settings" ||
|
||||||
@@ -91,8 +100,10 @@ func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, e
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte("{}"), err
|
return []byte("{}"), err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
|
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
|
||||||
passphrase = "my secret key"
|
passphrase = "my secret key"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
|
|||||||
var object string
|
var object string
|
||||||
err := conn.UnmarshalObject(test.object, &object)
|
err := conn.UnmarshalObject(test.object, &object)
|
||||||
is.NoError(err)
|
is.NoError(err)
|
||||||
is.Equal(test.expected, string(object))
|
is.Equal(test.expected, object)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
162
api/database/boltdb/tx.go
Normal file
162
api/database/boltdb/tx.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package boltdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DbTransaction struct {
|
||||||
|
conn *DbConnection
|
||||||
|
tx *bolt.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) SetServiceName(bucketName string) error {
|
||||||
|
_, err := tx.tx.CreateBucketIfNotExists([]byte(bucketName))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interface{}) error {
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
|
value := bucket.Get(key)
|
||||||
|
if value == nil {
|
||||||
|
return fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.conn.UnmarshalObjectWithJsoniter(value, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
|
||||||
|
data, err := tx.conn.MarshalObject(object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
return bucket.Put(key, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) DeleteObject(bucketName string, key []byte) error {
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
return bucket.Delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, matchingFn func(o interface{}) (id int, ok bool)) error {
|
||||||
|
var ids []int
|
||||||
|
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
err := tx.conn.UnmarshalObject(v, &obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, ok := matchingFn(obj); ok {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range ids {
|
||||||
|
if err := bucket.Delete(tx.conn.ConvertToKey(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
id, err := bucket.NextSequence()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifer")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
|
seqId, _ := bucket.NextSequence()
|
||||||
|
id, obj := fn(seqId)
|
||||||
|
|
||||||
|
data, err := tx.conn.MarshalObject(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
data, err := tx.conn.MarshalObject(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Put(tx.conn.ConvertToKey(id), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
data, err := tx.conn.MarshalObject(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Put(id, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, 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) GetAllWithJsoniter(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
|
||||||
|
bucket := tx.tx.Bucket([]byte(bucketName))
|
||||||
|
|
||||||
|
return bucket.ForEach(func(k []byte, v []byte) error {
|
||||||
|
err := tx.conn.UnmarshalObjectWithJsoniter(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()
|
||||||
|
|
||||||
|
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
|
||||||
|
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err = appendFn(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
126
api/database/boltdb/tx_test.go
Normal file
126
api/database/boltdb/tx_test.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package boltdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testBucketName = "test-bucket"
|
||||||
|
const testId = 1234
|
||||||
|
|
||||||
|
type testStruct struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTxs(t *testing.T) {
|
||||||
|
conn := DbConnection{
|
||||||
|
Path: t.TempDir(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := conn.Open()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Error propagation
|
||||||
|
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return errors.New("this is an error")
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("an error was expected, got nil instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an object
|
||||||
|
newObj := testStruct{
|
||||||
|
Key: "key",
|
||||||
|
Value: "value",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
err = tx.SetServiceName(testBucketName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.CreateObjectWithId(testBucketName, testId, newObj)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := testStruct{}
|
||||||
|
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Key != newObj.Key || obj.Value != newObj.Value {
|
||||||
|
t.Fatalf("expected %s:%s, got %s:%s instead", newObj.Key, newObj.Value, obj.Key, obj.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an object
|
||||||
|
updatedObj := testStruct{
|
||||||
|
Key: "updated-key",
|
||||||
|
Value: "updated-value",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return tx.UpdateObject(testBucketName, conn.ConvertToKey(testId), &updatedObj)
|
||||||
|
})
|
||||||
|
|
||||||
|
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Key != updatedObj.Key || obj.Value != updatedObj.Value {
|
||||||
|
t.Fatalf("expected %s:%s, got %s:%s instead", updatedObj.Key, updatedObj.Value, obj.Key, obj.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete an object
|
||||||
|
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return tx.DeleteObject(testBucketName, conn.ConvertToKey(testId))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
|
||||||
|
})
|
||||||
|
if !dataservices.IsErrObjectNotFound(err) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next identifier
|
||||||
|
err = conn.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
id1 := tx.GetNextIdentifier(testBucketName)
|
||||||
|
id2 := tx.GetNextIdentifier(testBucketName)
|
||||||
|
|
||||||
|
if id1+1 != id2 {
|
||||||
|
return errors.New("unexpected identifier sequence")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to write in a read transaction
|
||||||
|
err = conn.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
return tx.CreateObjectWithId(testBucketName, testId, newObj)
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("an error was expected, got nil instead")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,12 @@ import (
|
|||||||
|
|
||||||
// NewDatabase should use config options to return a connection to the requested database
|
// NewDatabase should use config options to return a connection to the requested database
|
||||||
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
|
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
|
||||||
switch storeType {
|
if storeType == "boltdb" {
|
||||||
case "boltdb":
|
|
||||||
return &boltdb.DbConnection{
|
return &boltdb.DbConnection{
|
||||||
Path: storePath,
|
Path: storePath,
|
||||||
EncryptionKey: encryptionKey,
|
EncryptionKey: encryptionKey,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unknown storage database: %s", storeType)
|
|
||||||
|
return nil, fmt.Errorf("Unknown storage database: %s", storeType)
|
||||||
}
|
}
|
||||||
|
|||||||
8
api/database/models/version.go
Normal file
8
api/database/models/version.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type Version struct {
|
||||||
|
SchemaVersion string
|
||||||
|
MigratorCount int
|
||||||
|
Edition int
|
||||||
|
InstanceID string
|
||||||
|
}
|
||||||
@@ -2,21 +2,22 @@ package apikeyrepository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices/errors"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/sirupsen/logrus"
|
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "api_key"
|
const BucketName = "api_key"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing api-key data.
|
// Service represents a service for managing api-key data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.APIKey, portainer.APIKeyID]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -27,7 +28,10 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.APIKey, portainer.APIKeyID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,18 +39,20 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
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)
|
var 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 interface{}) (interface{}, error) {
|
||||||
record, ok := obj.(*portainer.APIKey)
|
record, ok := obj.(*portainer.APIKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.WithField("obj", obj).Errorf("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 record.UserID == userID {
|
if record.UserID == userID {
|
||||||
result = append(result, *record)
|
result = append(result, *record)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &portainer.APIKey{}, nil
|
return &portainer.APIKey{}, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -58,34 +64,37 @@ func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer
|
|||||||
func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error) {
|
func (service *Service) GetAPIKeyByDigest(digest []byte) (*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 interface{}) (interface{}, error) {
|
||||||
key, ok := obj.(*portainer.APIKey)
|
key, ok := obj.(*portainer.APIKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.WithField("obj", obj).Errorf("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 bytes.Equal(key.Digest, digest) {
|
||||||
k = key
|
k = key
|
||||||
return nil, stop
|
return nil, stop
|
||||||
}
|
}
|
||||||
|
|
||||||
return &portainer.APIKey{}, nil
|
return &portainer.APIKey{}, nil
|
||||||
})
|
})
|
||||||
if err == stop {
|
|
||||||
|
if errors.Is(err, stop) {
|
||||||
return k, nil
|
return k, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil, errors.ErrObjectNotFound
|
return nil, dserrors.ErrObjectNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAPIKey creates a new APIKey object.
|
// Create creates a new APIKey object.
|
||||||
func (service *Service) CreateAPIKey(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, interface{}) {
|
||||||
record.ID = portainer.APIKeyID(id)
|
record.ID = portainer.APIKeyID(id)
|
||||||
@@ -94,26 +103,3 @@ func (service *Service) CreateAPIKey(record *portainer.APIKey) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAPIKey retrieves an existing APIKey object by api key ID.
|
|
||||||
func (service *Service) GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error) {
|
|
||||||
var key portainer.APIKey
|
|
||||||
identifier := service.connection.ConvertToKey(int(keyID))
|
|
||||||
|
|
||||||
err := service.connection.GetObject(BucketName, identifier, &key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) UpdateAPIKey(key *portainer.APIKey) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(key.ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) DeleteAPIKey(ID portainer.APIKeyID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
|
||||||
|
|||||||
66
api/dataservices/base.go
Normal file
66
api/dataservices/base.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package dataservices
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseCRUD[T any, I constraints.Integer] interface {
|
||||||
|
Create(element *T) error
|
||||||
|
Read(ID I) (*T, error)
|
||||||
|
ReadAll() ([]T, error)
|
||||||
|
Update(ID I, element *T) error
|
||||||
|
Delete(ID I) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseDataService[T any, I constraints.Integer] struct {
|
||||||
|
Bucket string
|
||||||
|
Connection portainer.Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *BaseDataService[T, I]) BucketName() string {
|
||||||
|
return s.Bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *BaseDataService[T, I]) Tx(tx portainer.Transaction) BaseDataServiceTx[T, I] {
|
||||||
|
return BaseDataServiceTx[T, I]{
|
||||||
|
Bucket: service.Bucket,
|
||||||
|
Connection: service.Connection,
|
||||||
|
Tx: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataService[T, I]) Read(ID I) (*T, error) {
|
||||||
|
var element *T
|
||||||
|
|
||||||
|
return element, service.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
var err error
|
||||||
|
element, err = service.Tx(tx).Read(ID)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataService[T, I]) ReadAll() ([]T, error) {
|
||||||
|
var collection = make([]T, 0)
|
||||||
|
|
||||||
|
return collection, service.Connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
var err error
|
||||||
|
collection, err = service.Tx(tx).ReadAll()
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataService[T, I]) Update(ID I, element *T) error {
|
||||||
|
return service.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return service.Tx(tx).Update(ID, element)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataService[T, I]) Delete(ID I) error {
|
||||||
|
return service.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return service.Tx(tx).Delete(ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
49
api/dataservices/base_tx.go
Normal file
49
api/dataservices/base_tx.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package dataservices
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BaseDataServiceTx[T any, I constraints.Integer] struct {
|
||||||
|
Bucket string
|
||||||
|
Connection portainer.Connection
|
||||||
|
Tx portainer.Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataServiceTx[T, I]) BucketName() string {
|
||||||
|
return service.Bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataServiceTx[T, I]) Read(ID I) (*T, error) {
|
||||||
|
var element T
|
||||||
|
identifier := service.Connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
|
err := service.Tx.GetObject(service.Bucket, identifier, &element)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &element, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
|
||||||
|
var collection = make([]T, 0)
|
||||||
|
|
||||||
|
return collection, service.Tx.GetAllWithJsoniter(
|
||||||
|
service.Bucket,
|
||||||
|
new(T),
|
||||||
|
AppendFn(&collection),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataServiceTx[T, I]) Update(ID I, element *T) error {
|
||||||
|
identifier := service.Connection.ConvertToKey(int(ID))
|
||||||
|
return service.Tx.UpdateObject(service.Bucket, identifier, element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service BaseDataServiceTx[T, I]) Delete(ID I) error {
|
||||||
|
identifier := service.Connection.ConvertToKey(int(ID))
|
||||||
|
return service.Tx.DeleteObject(service.Bucket, identifier)
|
||||||
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
package customtemplate
|
package customtemplate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "customtemplates"
|
const BucketName = "customtemplates"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing custom template data.
|
// Service represents a service for managing custom template data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.CustomTemplate, portainer.CustomTemplateID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -29,63 +21,20 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.CustomTemplate, portainer.CustomTemplateID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomTemplates return an array containing all the custom templates.
|
|
||||||
func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
|
|
||||||
var customTemplates = make([]portainer.CustomTemplate, 0)
|
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
|
||||||
&portainer.CustomTemplate{},
|
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
//var tag portainer.Tag
|
|
||||||
customTemplate, ok := obj.(*portainer.CustomTemplate)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to CustomTemplate object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to CustomTemplate object: %s", obj)
|
|
||||||
}
|
|
||||||
customTemplates = append(customTemplates, *customTemplate)
|
|
||||||
return &portainer.CustomTemplate{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return customTemplates, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CustomTemplate returns an custom template by ID.
|
|
||||||
func (service *Service) CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error) {
|
|
||||||
var customTemplate portainer.CustomTemplate
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
|
|
||||||
err := service.connection.GetObject(BucketName, identifier, &customTemplate)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &customTemplate, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateCustomTemplate updates an custom template.
|
|
||||||
func (service *Service) UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, customTemplate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteCustomTemplate deletes an custom template.
|
|
||||||
func (service *Service) DeleteCustomTemplate(ID portainer.CustomTemplateID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateCustomTemplate uses the existing id and saves it.
|
// CreateCustomTemplate uses the existing id and saves it.
|
||||||
// TODO: where does the ID come from, and is it safe?
|
// TODO: where does the ID come from, and is it safe?
|
||||||
func (service *Service) Create(customTemplate *portainer.CustomTemplate) error {
|
func (service *Service) Create(customTemplate *portainer.CustomTemplate) error {
|
||||||
return service.connection.CreateObjectWithId(BucketName, int(customTemplate.ID), customTemplate)
|
return service.Connection.CreateObjectWithId(BucketName, int(customTemplate.ID), customTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
package edgegroup
|
package edgegroup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "edgegroups"
|
const BucketName = "edgegroups"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing Edge group data.
|
// Service represents a service for managing Edge group data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.EdgeGroup, portainer.EdgeGroupID]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
func (service *Service) BucketName() string {
|
||||||
@@ -29,62 +25,36 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.EdgeGroup, portainer.EdgeGroupID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeGroups return an array containing all the Edge groups.
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
|
return ServiceTx{
|
||||||
var groups = make([]portainer.EdgeGroup, 0)
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.EdgeGroup, portainer.EdgeGroupID]{
|
||||||
|
Bucket: BucketName,
|
||||||
err := service.connection.GetAllWithJsoniter(
|
Connection: service.Connection,
|
||||||
BucketName,
|
Tx: tx,
|
||||||
&portainer.EdgeGroup{},
|
},
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
group, ok := obj.(*portainer.EdgeGroup)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to EdgeGroup object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
|
|
||||||
}
|
}
|
||||||
groups = append(groups, *group)
|
}
|
||||||
return &portainer.EdgeGroup{}, nil
|
|
||||||
|
// Deprecated: UpdateEdgeGroupFunc updates an edge group inside a transaction avoiding data races.
|
||||||
|
func (service *Service) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
|
||||||
|
id := service.Connection.ConvertToKey(int(ID))
|
||||||
|
edgeGroup := &portainer.EdgeGroup{}
|
||||||
|
|
||||||
|
return service.Connection.UpdateObjectFunc(BucketName, id, edgeGroup, func() {
|
||||||
|
updateFunc(edgeGroup)
|
||||||
})
|
})
|
||||||
|
|
||||||
return groups, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// EdgeGroup returns an Edge group by ID.
|
|
||||||
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
|
|
||||||
var group portainer.EdgeGroup
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
|
|
||||||
err := service.connection.GetObject(BucketName, identifier, &group)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &group, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateEdgeGroup updates an Edge group.
|
|
||||||
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, group)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteEdgeGroup deletes an Edge group.
|
|
||||||
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
|
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
|
||||||
func (service *Service) Create(group *portainer.EdgeGroup) error {
|
func (service *Service) Create(group *portainer.EdgeGroup) error {
|
||||||
return service.connection.CreateObject(
|
return service.Connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
BucketName,
|
return service.Tx(tx).Create(group)
|
||||||
func(id uint64) (int, interface{}) {
|
})
|
||||||
group.ID = portainer.EdgeGroupID(id)
|
|
||||||
return int(group.ID), group
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
27
api/dataservices/edgegroup/tx.go
Normal file
27
api/dataservices/edgegroup/tx.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package edgegroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
dataservices.BaseDataServiceTx[portainer.EdgeGroup, portainer.EdgeGroupID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEdgeGroupFunc is a no-op inside a transaction.
|
||||||
|
func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
|
||||||
|
return errors.New("cannot be called inside a transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
|
||||||
|
return service.Tx.CreateObject(
|
||||||
|
BucketName,
|
||||||
|
func(id uint64) (int, interface{}) {
|
||||||
|
group.ID = portainer.EdgeGroupID(id)
|
||||||
|
return int(group.ID), group
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
package edgejob
|
package edgejob
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "edgejobs"
|
const BucketName = "edgejobs"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing edge jobs data.
|
// Service represents a service for managing edge jobs data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.EdgeJob, portainer.EdgeJobID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -29,68 +21,50 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.EdgeJob, portainer.EdgeJobID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeJobs returns a list of Edge jobs
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
|
return ServiceTx{
|
||||||
var edgeJobs = make([]portainer.EdgeJob, 0)
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.EdgeJob, portainer.EdgeJobID]{
|
||||||
|
Bucket: BucketName,
|
||||||
err := service.connection.GetAll(
|
Connection: service.Connection,
|
||||||
BucketName,
|
Tx: tx,
|
||||||
&portainer.EdgeJob{},
|
},
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
//var tag portainer.Tag
|
|
||||||
job, ok := obj.(*portainer.EdgeJob)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to EdgeJob object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to EdgeJob object: %s", obj)
|
|
||||||
}
|
}
|
||||||
edgeJobs = append(edgeJobs, *job)
|
|
||||||
return &portainer.EdgeJob{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return edgeJobs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// EdgeJob returns an Edge job by ID
|
|
||||||
func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
|
|
||||||
var edgeJob portainer.EdgeJob
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
|
|
||||||
err := service.connection.GetObject(BucketName, identifier, &edgeJob)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &edgeJob, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create creates a new EdgeJob
|
// Create creates a new EdgeJob
|
||||||
func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
func (service *Service) Create(edgeJob *portainer.EdgeJob) error {
|
||||||
|
return service.CreateWithID(portainer.EdgeJobID(service.GetNextIdentifier()), edgeJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateWithID creates a new EdgeJob
|
||||||
|
func (service *Service) CreateWithID(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||||
edgeJob.ID = ID
|
edgeJob.ID = ID
|
||||||
|
|
||||||
return service.connection.CreateObjectWithId(
|
return service.Connection.CreateObjectWithId(
|
||||||
BucketName,
|
BucketName,
|
||||||
int(edgeJob.ID),
|
int(edgeJob.ID),
|
||||||
edgeJob,
|
edgeJob,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateEdgeJob updates an Edge job by ID
|
// UpdateEdgeJobFunc updates an edge job inside a transaction avoiding data races.
|
||||||
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
func (service *Service) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
id := service.Connection.ConvertToKey(int(ID))
|
||||||
return service.connection.UpdateObject(BucketName, identifier, edgeJob)
|
edgeJob := &portainer.EdgeJob{}
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteEdgeJob deletes an Edge job
|
return service.Connection.UpdateObjectFunc(BucketName, id, edgeJob, func() {
|
||||||
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
|
updateFunc(edgeJob)
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
})
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||||
func (service *Service) GetNextIdentifier() int {
|
func (service *Service) GetNextIdentifier() int {
|
||||||
return service.connection.GetNextIdentifier(BucketName)
|
return service.Connection.GetNextIdentifier(BucketName)
|
||||||
}
|
}
|
||||||
|
|||||||
34
api/dataservices/edgejob/tx.go
Normal file
34
api/dataservices/edgejob/tx.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package edgejob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
dataservices.BaseDataServiceTx[portainer.EdgeJob, portainer.EdgeJobID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new EdgeJob
|
||||||
|
func (service ServiceTx) Create(edgeJob *portainer.EdgeJob) error {
|
||||||
|
return service.CreateWithID(portainer.EdgeJobID(service.GetNextIdentifier()), edgeJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateWithID creates a new EdgeJob
|
||||||
|
func (service ServiceTx) CreateWithID(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
|
||||||
|
edgeJob.ID = ID
|
||||||
|
|
||||||
|
return service.Tx.CreateObjectWithId(BucketName, int(edgeJob.ID), edgeJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEdgeJobFunc is a no-op inside a transaction.
|
||||||
|
func (service ServiceTx) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
|
||||||
|
return errors.New("cannot be called inside a transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||||
|
func (service ServiceTx) GetNextIdentifier() int {
|
||||||
|
return service.Tx.GetNextIdentifier(BucketName)
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
package edgestack
|
package edgestack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"sync"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "edge_stack"
|
const BucketName = "edge_stack"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing Edge stack data.
|
// Service represents a service for managing Edge stack data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
connection portainer.Connection
|
||||||
|
idxVersion map[portainer.EdgeStackID]int
|
||||||
|
mu sync.RWMutex
|
||||||
|
cacheInvalidationFn func(portainer.EdgeStackID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
func (service *Service) BucketName() string {
|
||||||
@@ -22,36 +23,50 @@ func (service *Service) BucketName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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, cacheInvalidationFn func(portainer.EdgeStackID)) (*Service, error) {
|
||||||
err := connection.SetServiceName(BucketName)
|
err := connection.SetServiceName(BucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
s := &Service{
|
||||||
connection: connection,
|
connection: connection,
|
||||||
}, nil
|
idxVersion: make(map[portainer.EdgeStackID]int),
|
||||||
|
cacheInvalidationFn: cacheInvalidationFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.cacheInvalidationFn == nil {
|
||||||
|
s.cacheInvalidationFn = func(portainer.EdgeStackID) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
es, err := s.EdgeStacks()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range es {
|
||||||
|
s.idxVersion[e.ID] = e.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
|
return ServiceTx{
|
||||||
|
service: service,
|
||||||
|
tx: tx,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeStacks returns an array containing all edge stacks
|
// EdgeStacks returns an array containing all edge stacks
|
||||||
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
|
func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
|
||||||
var stacks = make([]portainer.EdgeStack, 0)
|
var stacks = make([]portainer.EdgeStack, 0)
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
return stacks, service.connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.EdgeStack{},
|
&portainer.EdgeStack{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.AppendFn(&stacks),
|
||||||
//var tag portainer.Tag
|
)
|
||||||
stack, ok := obj.(*portainer.EdgeStack)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to EdgeStack object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to EdgeStack object: %s", obj)
|
|
||||||
}
|
|
||||||
stacks = append(stacks, *stack)
|
|
||||||
return &portainer.EdgeStack{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return stacks, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeStack returns an Edge stack by ID.
|
// EdgeStack returns an Edge stack by ID.
|
||||||
@@ -67,28 +82,92 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
|
|||||||
return &stack, nil
|
return &stack, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
|
||||||
|
func (service *Service) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
|
||||||
|
service.mu.RLock()
|
||||||
|
v, ok := service.idxVersion[ID]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
// CreateEdgeStack saves an Edge stack object to db.
|
// CreateEdgeStack saves an Edge stack object to db.
|
||||||
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||||
|
|
||||||
edgeStack.ID = id
|
edgeStack.ID = id
|
||||||
|
|
||||||
return service.connection.CreateObjectWithId(
|
err := service.connection.CreateObjectWithId(
|
||||||
BucketName,
|
BucketName,
|
||||||
int(edgeStack.ID),
|
int(edgeStack.ID),
|
||||||
edgeStack,
|
edgeStack,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateEdgeStack updates an Edge stack.
|
service.mu.Lock()
|
||||||
|
service.idxVersion[id] = edgeStack.Version
|
||||||
|
service.cacheInvalidationFn(id)
|
||||||
|
service.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use UpdateEdgeStackFunc instead.
|
||||||
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||||
|
service.mu.Lock()
|
||||||
|
defer service.mu.Unlock()
|
||||||
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
identifier := service.connection.ConvertToKey(int(ID))
|
||||||
return service.connection.UpdateObject(BucketName, identifier, edgeStack)
|
|
||||||
|
err := service.connection.UpdateObject(BucketName, identifier, edgeStack)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.idxVersion[ID] = edgeStack.Version
|
||||||
|
service.cacheInvalidationFn(ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEdgeStackFunc updates an Edge stack inside a transaction avoiding data races.
|
||||||
|
func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||||
|
id := service.connection.ConvertToKey(int(ID))
|
||||||
|
edgeStack := &portainer.EdgeStack{}
|
||||||
|
|
||||||
|
service.mu.Lock()
|
||||||
|
defer service.mu.Unlock()
|
||||||
|
|
||||||
|
return service.connection.UpdateObjectFunc(BucketName, id, edgeStack, func() {
|
||||||
|
updateFunc(edgeStack)
|
||||||
|
|
||||||
|
service.idxVersion[ID] = edgeStack.Version
|
||||||
|
service.cacheInvalidationFn(ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEdgeStackFuncTx is a helper function used to call UpdateEdgeStackFunc inside a transaction.
|
||||||
|
func (service *Service) UpdateEdgeStackFuncTx(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||||
|
return service.Tx(tx).UpdateEdgeStackFunc(ID, updateFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteEdgeStack deletes an Edge stack.
|
// DeleteEdgeStack deletes an Edge stack.
|
||||||
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||||
|
service.mu.Lock()
|
||||||
|
defer service.mu.Unlock()
|
||||||
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
identifier := service.connection.ConvertToKey(int(ID))
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
|
err := service.connection.DeleteObject(BucketName, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(service.idxVersion, ID)
|
||||||
|
|
||||||
|
service.cacheInvalidationFn(ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||||
|
|||||||
137
api/dataservices/edgestack/tx.go
Normal file
137
api/dataservices/edgestack/tx.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package edgestack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
service *Service
|
||||||
|
tx portainer.Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) BucketName() string {
|
||||||
|
return BucketName
|
||||||
|
}
|
||||||
|
|
||||||
|
// EdgeStacks returns an array containing all edge stacks
|
||||||
|
func (service ServiceTx) EdgeStacks() ([]portainer.EdgeStack, error) {
|
||||||
|
var stacks = make([]portainer.EdgeStack, 0)
|
||||||
|
|
||||||
|
err := service.tx.GetAll(
|
||||||
|
BucketName,
|
||||||
|
&portainer.EdgeStack{},
|
||||||
|
func(obj interface{}) (interface{}, error) {
|
||||||
|
stack, ok := obj.(*portainer.EdgeStack)
|
||||||
|
if !ok {
|
||||||
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
|
||||||
|
return nil, fmt.Errorf("failed to convert to EdgeStack object: %s", obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
stacks = append(stacks, *stack)
|
||||||
|
|
||||||
|
return &portainer.EdgeStack{}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return stacks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EdgeStack returns an Edge stack by ID.
|
||||||
|
func (service ServiceTx) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error) {
|
||||||
|
var stack portainer.EdgeStack
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
|
err := service.tx.GetObject(BucketName, identifier, &stack)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &stack, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
|
||||||
|
func (service ServiceTx) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
|
||||||
|
service.service.mu.RLock()
|
||||||
|
v, ok := service.service.idxVersion[ID]
|
||||||
|
service.service.mu.RUnlock()
|
||||||
|
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEdgeStack saves an Edge stack object to db.
|
||||||
|
func (service ServiceTx) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||||
|
edgeStack.ID = id
|
||||||
|
|
||||||
|
err := service.tx.CreateObjectWithId(
|
||||||
|
BucketName,
|
||||||
|
int(edgeStack.ID),
|
||||||
|
edgeStack,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.service.mu.Lock()
|
||||||
|
service.service.idxVersion[id] = edgeStack.Version
|
||||||
|
service.service.cacheInvalidationFn(id)
|
||||||
|
service.service.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEdgeStack updates an Edge stack.
|
||||||
|
func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
|
||||||
|
service.service.mu.Lock()
|
||||||
|
defer service.service.mu.Unlock()
|
||||||
|
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
|
err := service.tx.UpdateObject(BucketName, identifier, edgeStack)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.service.idxVersion[ID] = edgeStack.Version
|
||||||
|
service.service.cacheInvalidationFn(ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: use UpdateEdgeStack inside a transaction instead.
|
||||||
|
func (service ServiceTx) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
|
||||||
|
edgeStack, err := service.EdgeStack(ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFunc(edgeStack)
|
||||||
|
|
||||||
|
return service.UpdateEdgeStack(ID, edgeStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEdgeStack deletes an Edge stack.
|
||||||
|
func (service ServiceTx) DeleteEdgeStack(ID portainer.EdgeStackID) error {
|
||||||
|
service.service.mu.Lock()
|
||||||
|
defer service.service.mu.Unlock()
|
||||||
|
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
|
err := service.tx.DeleteObject(BucketName, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(service.service.idxVersion, ID)
|
||||||
|
|
||||||
|
service.service.cacheInvalidationFn(ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||||
|
func (service ServiceTx) GetNextIdentifier() int {
|
||||||
|
return service.tx.GetNextIdentifier(BucketName)
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
package endpoint
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "endpoints"
|
const BucketName = "endpoints"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) data.
|
// Service represents a service for managing environment(endpoint) data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
connection portainer.Connection
|
||||||
|
mu sync.RWMutex
|
||||||
|
idxEdgeID map[string]portainer.EndpointID
|
||||||
|
heartbeats sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
func (service *Service) BucketName() string {
|
||||||
@@ -28,62 +29,130 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
s := &Service{
|
||||||
connection: connection,
|
connection: connection,
|
||||||
}, nil
|
idxEdgeID: make(map[string]portainer.EndpointID),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint returns an environment(endpoint) by ID.
|
es, err := s.endpoints()
|
||||||
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
|
||||||
var endpoint portainer.Endpoint
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
|
|
||||||
err := service.connection.GetObject(BucketName, identifier, &endpoint)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &endpoint, nil
|
for _, e := range es {
|
||||||
|
if len(e.EdgeID) > 0 {
|
||||||
|
s.idxEdgeID[e.EdgeID] = e.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
s.heartbeats.Store(e.ID, e.LastCheckInDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
|
return ServiceTx{
|
||||||
|
service: service,
|
||||||
|
tx: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint returns an environment(endpoint) by ID.
|
||||||
|
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||||
|
var endpoint *portainer.Endpoint
|
||||||
|
var err error
|
||||||
|
|
||||||
|
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
|
endpoint, err = service.Tx(tx).Endpoint(ID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.LastCheckInDate, _ = service.Heartbeat(ID)
|
||||||
|
|
||||||
|
return endpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateEndpoint updates an environment(endpoint).
|
// UpdateEndpoint updates an environment(endpoint).
|
||||||
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return service.connection.UpdateObject(BucketName, identifier, endpoint)
|
return service.Tx(tx).UpdateEndpoint(ID, endpoint)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteEndpoint deletes an environment(endpoint).
|
// DeleteEndpoint deletes an environment(endpoint).
|
||||||
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
|
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
return service.Tx(tx).DeleteEndpoint(ID)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoints return an array containing all the environments(endpoints).
|
func (service *Service) endpoints() ([]portainer.Endpoint, error) {
|
||||||
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
var endpoints []portainer.Endpoint
|
||||||
var endpoints = make([]portainer.Endpoint, 0)
|
var err error
|
||||||
|
|
||||||
err := service.connection.GetAllWithJsoniter(
|
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
|
||||||
BucketName,
|
endpoints, err = service.Tx(tx).Endpoints()
|
||||||
&portainer.Endpoint{},
|
return err
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
endpoint, ok := obj.(*portainer.Endpoint)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Endpoint object")
|
|
||||||
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
|
|
||||||
}
|
|
||||||
endpoints = append(endpoints, *endpoint)
|
|
||||||
return &portainer.Endpoint{}, nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return endpoints, err
|
return endpoints, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Endpoints return an array containing all the environments(endpoints).
|
||||||
|
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||||
|
endpoints, err := service.endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, e := range endpoints {
|
||||||
|
t, _ := service.Heartbeat(e.ID)
|
||||||
|
endpoints[i].LastCheckInDate = t
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndpointIDByEdgeID returns the EndpointID from the given EdgeID using an in-memory index
|
||||||
|
func (service *Service) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
|
||||||
|
service.mu.RLock()
|
||||||
|
endpointID, ok := service.idxEdgeID[edgeID]
|
||||||
|
service.mu.RUnlock()
|
||||||
|
|
||||||
|
return endpointID, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
|
||||||
|
if t, ok := service.heartbeats.Load(endpointID); ok {
|
||||||
|
return t.(int64), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) UpdateHeartbeat(endpointID portainer.EndpointID) {
|
||||||
|
service.heartbeats.Store(endpointID, time.Now().Unix())
|
||||||
|
}
|
||||||
|
|
||||||
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||||
func (service *Service) Create(endpoint *portainer.Endpoint) error {
|
func (service *Service) Create(endpoint *portainer.Endpoint) error {
|
||||||
return service.connection.CreateObjectWithSetSequence(BucketName, int(endpoint.ID), endpoint)
|
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
return service.Tx(tx).Create(endpoint)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||||
func (service *Service) GetNextIdentifier() int {
|
func (service *Service) GetNextIdentifier() int {
|
||||||
return service.connection.GetNextIdentifier(BucketName)
|
var identifier int
|
||||||
|
|
||||||
|
service.connection.UpdateTx(func(tx portainer.Transaction) error {
|
||||||
|
identifier = service.Tx(tx).GetNextIdentifier()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return identifier
|
||||||
}
|
}
|
||||||
|
|||||||
128
api/dataservices/endpoint/tx.go
Normal file
128
api/dataservices/endpoint/tx.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
service *Service
|
||||||
|
tx portainer.Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) BucketName() string {
|
||||||
|
return BucketName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint returns an environment(endpoint) by ID.
|
||||||
|
func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
|
||||||
|
var endpoint portainer.Endpoint
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
|
err := service.tx.GetObject(BucketName, identifier, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.LastCheckInDate, _ = service.service.Heartbeat(ID)
|
||||||
|
|
||||||
|
return &endpoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEndpoint updates an environment(endpoint).
|
||||||
|
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
|
err := service.tx.UpdateObject(BucketName, identifier, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.service.mu.Lock()
|
||||||
|
if len(endpoint.EdgeID) > 0 {
|
||||||
|
service.service.idxEdgeID[endpoint.EdgeID] = ID
|
||||||
|
}
|
||||||
|
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
|
||||||
|
service.service.mu.Unlock()
|
||||||
|
|
||||||
|
cache.Del(endpoint.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEndpoint deletes an environment(endpoint).
|
||||||
|
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(ID))
|
||||||
|
|
||||||
|
err := service.tx.DeleteObject(BucketName, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.service.mu.Lock()
|
||||||
|
for edgeID, endpointID := range service.service.idxEdgeID {
|
||||||
|
if endpointID == ID {
|
||||||
|
delete(service.service.idxEdgeID, edgeID)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
service.service.heartbeats.Delete(ID)
|
||||||
|
service.service.mu.Unlock()
|
||||||
|
|
||||||
|
cache.Del(ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoints return an array containing all the environments(endpoints).
|
||||||
|
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
|
||||||
|
var endpoints = make([]portainer.Endpoint, 0)
|
||||||
|
|
||||||
|
return endpoints, service.tx.GetAllWithJsoniter(
|
||||||
|
BucketName,
|
||||||
|
&portainer.Endpoint{},
|
||||||
|
dataservices.AppendFn(&endpoints),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
|
||||||
|
log.Error().Str("func", "EndpointIDByEdgeID").Msg("cannot be called inside a transaction")
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
|
||||||
|
log.Error().Str("func", "Heartbeat").Msg("cannot be called inside a transaction")
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) UpdateHeartbeat(endpointID portainer.EndpointID) {
|
||||||
|
log.Error().Str("func", "UpdateHeartbeat").Msg("cannot be called inside a transaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
|
||||||
|
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
|
||||||
|
err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.service.mu.Lock()
|
||||||
|
if len(endpoint.EdgeID) > 0 {
|
||||||
|
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
|
||||||
|
}
|
||||||
|
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
|
||||||
|
service.service.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||||
|
func (service ServiceTx) GetNextIdentifier() int {
|
||||||
|
return service.tx.GetNextIdentifier(BucketName)
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package endpointgroup
|
package endpointgroup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -14,11 +12,7 @@ const (
|
|||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) data.
|
// Service represents a service for managing environment(endpoint) data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.EndpointGroup, portainer.EndpointGroupID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -29,59 +23,26 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.EndpointGroup, portainer.EndpointGroupID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointGroup returns an environment(endpoint) group by ID.
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
|
return ServiceTx{
|
||||||
var endpointGroup portainer.EndpointGroup
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.EndpointGroup, portainer.EndpointGroupID]{
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
Bucket: BucketName,
|
||||||
|
Connection: service.Connection,
|
||||||
err := service.connection.GetObject(BucketName, identifier, &endpointGroup)
|
Tx: tx,
|
||||||
if err != nil {
|
},
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &endpointGroup, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateEndpointGroup updates an environment(endpoint) group.
|
|
||||||
func (service *Service) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, endpointGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteEndpointGroup deletes an environment(endpoint) group.
|
|
||||||
func (service *Service) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndpointGroups return an array containing all the environment(endpoint) groups.
|
|
||||||
func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
|
|
||||||
var endpointGroups = make([]portainer.EndpointGroup, 0)
|
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
|
||||||
&portainer.EndpointGroup{},
|
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
//var tag portainer.Tag
|
|
||||||
endpointGroup, ok := obj.(*portainer.EndpointGroup)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to EndpointGroup object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to EndpointGroup object: %s", obj)
|
|
||||||
}
|
|
||||||
endpointGroups = append(endpointGroups, *endpointGroup)
|
|
||||||
return &portainer.EndpointGroup{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return endpointGroups, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
|
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
|
||||||
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, interface{}) {
|
||||||
endpointGroup.ID = portainer.EndpointGroupID(id)
|
endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||||
|
|||||||
21
api/dataservices/endpointgroup/tx.go
Normal file
21
api/dataservices/endpointgroup/tx.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package endpointgroup
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
dataservices.BaseDataServiceTx[portainer.EndpointGroup, portainer.EndpointGroupID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
|
||||||
|
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
|
||||||
|
return service.Tx.CreateObject(
|
||||||
|
BucketName,
|
||||||
|
func(id uint64) (int, interface{}) {
|
||||||
|
endpointGroup.ID = portainer.EndpointGroupID(id)
|
||||||
|
return int(endpointGroup.ID), endpointGroup
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,26 +1,35 @@
|
|||||||
package endpointrelation
|
package endpointrelation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "endpoint_relations"
|
const BucketName = "endpoint_relations"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) relation data.
|
// Service represents a service for managing environment(endpoint) relation data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
connection portainer.Connection
|
||||||
|
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||||
|
updateStackFnTx func(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
func (service *Service) BucketName() string {
|
||||||
return BucketName
|
return BucketName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) RegisterUpdateStackFunction(
|
||||||
|
updateFunc func(portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
|
||||||
|
updateFuncTx func(portainer.Transaction, portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
|
||||||
|
) {
|
||||||
|
service.updateStackFn = updateFunc
|
||||||
|
service.updateStackFnTx = updateFuncTx
|
||||||
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// 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)
|
err := connection.SetServiceName(BucketName)
|
||||||
@@ -33,24 +42,22 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
|
return ServiceTx{
|
||||||
|
service: service,
|
||||||
|
tx: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// EndpointRelations returns an array of all EndpointRelations
|
// EndpointRelations returns an array of all EndpointRelations
|
||||||
func (service *Service) EndpointRelations() ([]portainer.EndpointRelation, error) {
|
func (service *Service) EndpointRelations() ([]portainer.EndpointRelation, error) {
|
||||||
var all = make([]portainer.EndpointRelation, 0)
|
var all = make([]portainer.EndpointRelation, 0)
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
return all, service.connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.EndpointRelation{},
|
&portainer.EndpointRelation{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.AppendFn(&all),
|
||||||
r, ok := obj.(*portainer.EndpointRelation)
|
)
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to EndpointRelation object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to EndpointRelation object: %s", obj)
|
|
||||||
}
|
|
||||||
all = append(all, *r)
|
|
||||||
return &portainer.EndpointRelation{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return all, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointRelation returns a Environment(Endpoint) relation object by EndpointID
|
// EndpointRelation returns a Environment(Endpoint) relation object by EndpointID
|
||||||
@@ -68,17 +75,105 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
|
|||||||
|
|
||||||
// CreateEndpointRelation saves endpointRelation
|
// CreateEndpointRelation saves endpointRelation
|
||||||
func (service *Service) Create(endpointRelation *portainer.EndpointRelation) error {
|
func (service *Service) Create(endpointRelation *portainer.EndpointRelation) error {
|
||||||
return service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||||
|
cache.Del(endpointRelation.EndpointID)
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
|
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
|
||||||
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||||
identifier := service.connection.ConvertToKey(int(EndpointID))
|
previousRelationState, _ := service.EndpointRelation(endpointID)
|
||||||
return service.connection.UpdateObject(BucketName, identifier, endpointRelation)
|
|
||||||
|
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||||
|
err := service.connection.UpdateObject(BucketName, identifier, endpointRelation)
|
||||||
|
cache.Del(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRelationState, _ := service.EndpointRelation(endpointID)
|
||||||
|
|
||||||
|
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||||
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
|
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
|
||||||
identifier := service.connection.ConvertToKey(int(EndpointID))
|
deletedRelation, _ := service.EndpointRelation(endpointID)
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
|
identifier := service.connection.ConvertToKey(int(endpointID))
|
||||||
|
err := service.connection.DeleteObject(BucketName, identifier)
|
||||||
|
cache.Del(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
|
||||||
|
rels, err := service.EndpointRelations()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rel := range rels {
|
||||||
|
for id := range rel.EdgeStacks {
|
||||||
|
if edgeStackID == id {
|
||||||
|
cache.Del(rel.EndpointID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
|
||||||
|
relations, _ := service.EndpointRelations()
|
||||||
|
|
||||||
|
stacksToUpdate := map[portainer.EdgeStackID]bool{}
|
||||||
|
|
||||||
|
if previousRelationState != nil {
|
||||||
|
for stackId, enabled := range previousRelationState.EdgeStacks {
|
||||||
|
// flag stack for update if stack is not in the updated relation state
|
||||||
|
// = stack has been removed for this relation
|
||||||
|
// or this relation has been deleted
|
||||||
|
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
|
||||||
|
stacksToUpdate[stackId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedRelationState != nil {
|
||||||
|
for stackId, enabled := range updatedRelationState.EdgeStacks {
|
||||||
|
// flag stack for update if stack is not in the previous relation state
|
||||||
|
// = stack has been added for this relation
|
||||||
|
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
|
||||||
|
stacksToUpdate[stackId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each stack referenced by the updated relation
|
||||||
|
// list how many time this stack is referenced in all relations
|
||||||
|
// in order to update the stack deployments count
|
||||||
|
for refStackId, refStackEnabled := range stacksToUpdate {
|
||||||
|
if refStackEnabled {
|
||||||
|
numDeployments := 0
|
||||||
|
for _, r := range relations {
|
||||||
|
for sId, enabled := range r.EdgeStacks {
|
||||||
|
if enabled && sId == refStackId {
|
||||||
|
numDeployments += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||||
|
edgeStack.NumDeployments = numDeployments
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
api/dataservices/endpointrelation/tx.go
Normal file
147
api/dataservices/endpointrelation/tx.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package endpointrelation
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/internal/edge/cache"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
service *Service
|
||||||
|
tx portainer.Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) BucketName() string {
|
||||||
|
return BucketName
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndpointRelations returns an array of all EndpointRelations
|
||||||
|
func (service ServiceTx) EndpointRelations() ([]portainer.EndpointRelation, error) {
|
||||||
|
var all = make([]portainer.EndpointRelation, 0)
|
||||||
|
|
||||||
|
return all, service.tx.GetAll(
|
||||||
|
BucketName,
|
||||||
|
&portainer.EndpointRelation{},
|
||||||
|
dataservices.AppendFn(&all),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EndpointRelation returns an Environment(Endpoint) relation object by EndpointID
|
||||||
|
func (service ServiceTx) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
|
||||||
|
var endpointRelation portainer.EndpointRelation
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||||
|
|
||||||
|
err := service.tx.GetObject(BucketName, identifier, &endpointRelation)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &endpointRelation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEndpointRelation saves endpointRelation
|
||||||
|
func (service ServiceTx) Create(endpointRelation *portainer.EndpointRelation) error {
|
||||||
|
err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
|
||||||
|
cache.Del(endpointRelation.EndpointID)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
|
||||||
|
func (service ServiceTx) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
|
||||||
|
previousRelationState, _ := service.EndpointRelation(endpointID)
|
||||||
|
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||||
|
err := service.tx.UpdateObject(BucketName, identifier, endpointRelation)
|
||||||
|
cache.Del(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRelationState, _ := service.EndpointRelation(endpointID)
|
||||||
|
|
||||||
|
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
|
||||||
|
func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
|
||||||
|
deletedRelation, _ := service.EndpointRelation(endpointID)
|
||||||
|
|
||||||
|
identifier := service.service.connection.ConvertToKey(int(endpointID))
|
||||||
|
err := service.tx.DeleteObject(BucketName, identifier)
|
||||||
|
cache.Del(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
|
||||||
|
rels, err := service.EndpointRelations()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rel := range rels {
|
||||||
|
for id := range rel.EdgeStacks {
|
||||||
|
if edgeStackID == id {
|
||||||
|
cache.Del(rel.EndpointID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
|
||||||
|
relations, _ := service.EndpointRelations()
|
||||||
|
|
||||||
|
stacksToUpdate := map[portainer.EdgeStackID]bool{}
|
||||||
|
|
||||||
|
if previousRelationState != nil {
|
||||||
|
for stackId, enabled := range previousRelationState.EdgeStacks {
|
||||||
|
// flag stack for update if stack is not in the updated relation state
|
||||||
|
// = stack has been removed for this relation
|
||||||
|
// or this relation has been deleted
|
||||||
|
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
|
||||||
|
stacksToUpdate[stackId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatedRelationState != nil {
|
||||||
|
for stackId, enabled := range updatedRelationState.EdgeStacks {
|
||||||
|
// flag stack for update if stack is not in the previous relation state
|
||||||
|
// = stack has been added for this relation
|
||||||
|
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
|
||||||
|
stacksToUpdate[stackId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for each stack referenced by the updated relation
|
||||||
|
// list how many time this stack is referenced in all relations
|
||||||
|
// in order to update the stack deployments count
|
||||||
|
for refStackId, refStackEnabled := range stacksToUpdate {
|
||||||
|
if refStackEnabled {
|
||||||
|
numDeployments := 0
|
||||||
|
for _, r := range relations {
|
||||||
|
for sId, enabled := range r.EdgeStacks {
|
||||||
|
if enabled && sId == refStackId {
|
||||||
|
numDeployments += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
|
||||||
|
edgeStack.NumDeployments = numDeployments
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package errors
|
package errors
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// TODO: i'm pretty sure this needs wrapping at several levels
|
|
||||||
ErrObjectNotFound = errors.New("object not found inside the database")
|
ErrObjectNotFound = errors.New("object not found inside the database")
|
||||||
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
|
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
|
||||||
ErrDBImportFailed = errors.New("importing backup failed")
|
ErrDBImportFailed = errors.New("importing backup failed")
|
||||||
|
ErrDatabaseIsUpdating = errors.New("database is currently in updating state. Failed prior upgrade. Please restore from backup or delete the database and restart Portainer")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
package extension
|
package extension
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "extension"
|
const BucketName = "extension"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) data.
|
// Service represents a service for managing environment(endpoint) data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -50,20 +46,12 @@ func (service *Service) Extension(ID portainer.ExtensionID) (*portainer.Extensio
|
|||||||
func (service *Service) Extensions() ([]portainer.Extension, error) {
|
func (service *Service) Extensions() ([]portainer.Extension, error) {
|
||||||
var extensions = make([]portainer.Extension, 0)
|
var extensions = make([]portainer.Extension, 0)
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
return extensions, service.connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.Extension{},
|
&portainer.Extension{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.AppendFn(&extensions),
|
||||||
extension, ok := obj.(*portainer.Extension)
|
)
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Extension object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to Extension object: %s", obj)
|
|
||||||
}
|
|
||||||
extensions = append(extensions, *extension)
|
|
||||||
return &portainer.Extension{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return extensions, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist persists a extension inside the database.
|
// Persist persists a extension inside the database.
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
package fdoprofile
|
package fdoprofile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "fdo_profiles"
|
const BucketName = "fdo_profiles"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managingFDO Profiles data.
|
// Service represents a service for managingFDO Profiles data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -29,65 +21,23 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FDOProfiles return an array containing all the FDO Profiles.
|
|
||||||
func (service *Service) FDOProfiles() ([]portainer.FDOProfile, error) {
|
|
||||||
var fdoProfiles = make([]portainer.FDOProfile, 0)
|
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
|
||||||
&portainer.FDOProfile{},
|
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
fdoProfile, ok := obj.(*portainer.FDOProfile)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to FDOProfile object")
|
|
||||||
return nil, fmt.Errorf("failed to convert to FDOProfile object: %s", obj)
|
|
||||||
}
|
|
||||||
fdoProfiles = append(fdoProfiles, *fdoProfile)
|
|
||||||
return &portainer.FDOProfile{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return fdoProfiles, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FDOProfile returns an FDO Profile by ID.
|
|
||||||
func (service *Service) FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error) {
|
|
||||||
var FDOProfile portainer.FDOProfile
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
|
|
||||||
err := service.connection.GetObject(BucketName, identifier, &FDOProfile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &FDOProfile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create assign an ID to a new FDO Profile and saves it.
|
// Create assign an ID to a new FDO Profile and saves it.
|
||||||
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
|
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
|
||||||
return service.connection.CreateObjectWithId(
|
return service.Connection.CreateObjectWithId(
|
||||||
BucketName,
|
BucketName,
|
||||||
int(FDOProfile.ID),
|
int(FDOProfile.ID),
|
||||||
FDOProfile,
|
FDOProfile,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates an FDO Profile.
|
|
||||||
func (service *Service) Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, FDOProfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes an FDO Profile.
|
|
||||||
func (service *Service) Delete(ID portainer.FDOProfileID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNextIdentifier returns the next identifier for a FDO Profile.
|
// GetNextIdentifier returns the next identifier for a FDO Profile.
|
||||||
func (service *Service) GetNextIdentifier() int {
|
func (service *Service) GetNextIdentifier() int {
|
||||||
return service.connection.GetNextIdentifier(BucketName)
|
return service.Connection.GetNextIdentifier(BucketName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
package helmuserrepository
|
package helmuserrepository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "helm_user_repository"
|
const BucketName = "helm_user_repository"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) data.
|
// Service represents a service for managing environment(endpoint) data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -29,55 +21,29 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//HelmUserRepository returns an array of all HelmUserRepository
|
|
||||||
func (service *Service) HelmUserRepositories() ([]portainer.HelmUserRepository, error) {
|
|
||||||
var repos = make([]portainer.HelmUserRepository, 0)
|
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
|
||||||
&portainer.HelmUserRepository{},
|
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
r, ok := obj.(*portainer.HelmUserRepository)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to HelmUserRepository object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
|
|
||||||
}
|
|
||||||
repos = append(repos, *r)
|
|
||||||
return &portainer.HelmUserRepository{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return repos, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// HelmUserRepositoryByUserID return an array containing all the HelmUserRepository objects where the specified userID is present.
|
// HelmUserRepositoryByUserID return an array containing all the HelmUserRepository objects where the specified userID is present.
|
||||||
func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error) {
|
func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error) {
|
||||||
var result = make([]portainer.HelmUserRepository, 0)
|
var result = make([]portainer.HelmUserRepository, 0)
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
return result, service.Connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.HelmUserRepository{},
|
&portainer.HelmUserRepository{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.FilterFn(&result, func(e portainer.HelmUserRepository) bool {
|
||||||
record, ok := obj.(*portainer.HelmUserRepository)
|
return e.UserID == userID
|
||||||
if !ok {
|
}),
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to HelmUserRepository object")
|
)
|
||||||
return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
|
|
||||||
}
|
|
||||||
if record.UserID == userID {
|
|
||||||
result = append(result, *record)
|
|
||||||
}
|
|
||||||
return &portainer.HelmUserRepository{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return result, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateHelmUserRepository creates a new HelmUserRepository object.
|
// CreateHelmUserRepository creates a new HelmUserRepository object.
|
||||||
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, interface{}) {
|
||||||
record.ID = portainer.HelmUserRepositoryID(id)
|
record.ID = portainer.HelmUserRepositoryID(id)
|
||||||
@@ -85,15 +51,3 @@ func (service *Service) Create(record *portainer.HelmUserRepository) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateHelmUserRepostory updates an registry.
|
|
||||||
func (service *Service) UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, registry *portainer.HelmUserRepository) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, registry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteHelmUserRepository deletes an registry.
|
|
||||||
func (service *Service) DeleteHelmUserRepository(ID portainer.HelmUserRepositoryID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
|
||||||
|
|||||||
68
api/dataservices/helpers.go
Normal file
68
api/dataservices/helpers.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package dataservices
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
perrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrStop signals the stop of computation when filtering results
|
||||||
|
var ErrStop = errors.New("stop")
|
||||||
|
|
||||||
|
func IsErrObjectNotFound(e error) bool {
|
||||||
|
return errors.Is(e, perrors.ErrObjectNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendFn appends elements to the given collection slice
|
||||||
|
func AppendFn[T any](collection *[]T) func(obj interface{}) (interface{}, error) {
|
||||||
|
return func(obj interface{}) (interface{}, error) {
|
||||||
|
element, ok := obj.(*T)
|
||||||
|
if !ok {
|
||||||
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||||
|
return nil, fmt.Errorf("failed to convert to %T object: %#v", new(T), obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
*collection = append(*collection, *element)
|
||||||
|
|
||||||
|
return new(T), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return func(obj interface{}) (interface{}, error) {
|
||||||
|
element, ok := obj.(*T)
|
||||||
|
if !ok {
|
||||||
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||||
|
return nil, fmt.Errorf("failed to convert to %T object: %#v", new(T), obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if predicate(*element) {
|
||||||
|
*collection = append(*collection, *element)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(T), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstFn sets the element to the first one that satisfies the predicate and stops the computation, returns ErrStop on
|
||||||
|
// success
|
||||||
|
func FirstFn[T any](element *T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
|
||||||
|
return func(obj interface{}) (interface{}, error) {
|
||||||
|
e, ok := obj.(*T)
|
||||||
|
if !ok {
|
||||||
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
|
||||||
|
return nil, fmt.Errorf("failed to convert to %T object: %#v", new(T), obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if predicate(*e) {
|
||||||
|
*element = *e
|
||||||
|
return new(T), ErrStop
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(T), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,16 @@
|
|||||||
package dataservices
|
package dataservices
|
||||||
|
|
||||||
// "github.com/portainer/portainer/api/dataservices"
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/dataservices/errors"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/database/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// DataStore defines the interface to manage the data
|
DataStoreTx interface {
|
||||||
DataStore interface {
|
|
||||||
Open() (newStore bool, err error)
|
|
||||||
Init() error
|
|
||||||
Close() error
|
|
||||||
MigrateData() error
|
|
||||||
Rollback(force bool) error
|
|
||||||
CheckCurrentEdition() error
|
|
||||||
BackupTo(w io.Writer) error
|
|
||||||
Export(filename string) (err error)
|
|
||||||
IsErrObjectNotFound(err error) bool
|
IsErrObjectNotFound(err error) bool
|
||||||
|
|
||||||
CustomTemplate() CustomTemplateService
|
CustomTemplate() CustomTemplateService
|
||||||
EdgeGroup() EdgeGroupService
|
EdgeGroup() EdgeGroupService
|
||||||
EdgeJob() EdgeJobService
|
EdgeJob() EdgeJobService
|
||||||
@@ -38,6 +25,7 @@ type (
|
|||||||
Role() RoleService
|
Role() RoleService
|
||||||
APIKeyRepository() APIKeyRepository
|
APIKeyRepository() APIKeyRepository
|
||||||
Settings() SettingsService
|
Settings() SettingsService
|
||||||
|
Snapshot() SnapshotService
|
||||||
SSLSettings() SSLSettingsService
|
SSLSettings() SSLSettingsService
|
||||||
Stack() StackService
|
Stack() StackService
|
||||||
Tag() TagService
|
Tag() TagService
|
||||||
@@ -49,44 +37,49 @@ type (
|
|||||||
Webhook() WebhookService
|
Webhook() WebhookService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DataStore interface {
|
||||||
|
Open() (newStore bool, err error)
|
||||||
|
Init() error
|
||||||
|
Close() error
|
||||||
|
UpdateTx(func(DataStoreTx) error) error
|
||||||
|
ViewTx(func(DataStoreTx) error) error
|
||||||
|
MigrateData() error
|
||||||
|
Rollback(force bool) error
|
||||||
|
CheckCurrentEdition() error
|
||||||
|
BackupTo(w io.Writer) error
|
||||||
|
Export(filename string) (err error)
|
||||||
|
|
||||||
|
DataStoreTx
|
||||||
|
}
|
||||||
|
|
||||||
// CustomTemplateService represents a service to manage custom templates
|
// CustomTemplateService represents a service to manage custom templates
|
||||||
CustomTemplateService interface {
|
CustomTemplateService interface {
|
||||||
|
BaseCRUD[portainer.CustomTemplate, portainer.CustomTemplateID]
|
||||||
GetNextIdentifier() int
|
GetNextIdentifier() int
|
||||||
CustomTemplates() ([]portainer.CustomTemplate, error)
|
|
||||||
CustomTemplate(ID portainer.CustomTemplateID) (*portainer.CustomTemplate, error)
|
|
||||||
Create(customTemplate *portainer.CustomTemplate) error
|
|
||||||
UpdateCustomTemplate(ID portainer.CustomTemplateID, customTemplate *portainer.CustomTemplate) error
|
|
||||||
DeleteCustomTemplate(ID portainer.CustomTemplateID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeGroupService represents a service to manage Edge groups
|
// EdgeGroupService represents a service to manage Edge groups
|
||||||
EdgeGroupService interface {
|
EdgeGroupService interface {
|
||||||
EdgeGroups() ([]portainer.EdgeGroup, error)
|
BaseCRUD[portainer.EdgeGroup, portainer.EdgeGroupID]
|
||||||
EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error)
|
UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(group *portainer.EdgeGroup)) error
|
||||||
Create(group *portainer.EdgeGroup) error
|
|
||||||
UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error
|
|
||||||
DeleteEdgeGroup(ID portainer.EdgeGroupID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeJobService represents a service to manage Edge jobs
|
// EdgeJobService represents a service to manage Edge jobs
|
||||||
EdgeJobService interface {
|
EdgeJobService interface {
|
||||||
EdgeJobs() ([]portainer.EdgeJob, error)
|
BaseCRUD[portainer.EdgeJob, portainer.EdgeJobID]
|
||||||
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
|
CreateWithID(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
||||||
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error
|
||||||
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
|
|
||||||
DeleteEdgeJob(ID portainer.EdgeJobID) error
|
|
||||||
GetNextIdentifier() int
|
GetNextIdentifier() int
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeStackService represents a service to manage Edge stacks
|
// EdgeStackService represents a service to manage Edge stacks
|
||||||
EdgeStackService interface {
|
EdgeStackService interface {
|
||||||
EdgeStacks() ([]portainer.EdgeStack, error)
|
EdgeStacks() ([]portainer.EdgeStack, error)
|
||||||
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
|
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
|
||||||
|
EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
|
||||||
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
||||||
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
|
||||||
|
UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
|
||||||
DeleteEdgeStack(ID portainer.EdgeStackID) error
|
DeleteEdgeStack(ID portainer.EdgeStackID) error
|
||||||
GetNextIdentifier() int
|
GetNextIdentifier() int
|
||||||
BucketName() string
|
BucketName() string
|
||||||
@@ -95,6 +88,9 @@ type (
|
|||||||
// EndpointService represents a service for managing environment(endpoint) data
|
// EndpointService represents a service for managing environment(endpoint) data
|
||||||
EndpointService interface {
|
EndpointService interface {
|
||||||
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
||||||
|
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
|
||||||
|
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
|
||||||
|
UpdateHeartbeat(endpointID portainer.EndpointID)
|
||||||
Endpoints() ([]portainer.Endpoint, error)
|
Endpoints() ([]portainer.Endpoint, error)
|
||||||
Create(endpoint *portainer.Endpoint) error
|
Create(endpoint *portainer.Endpoint) error
|
||||||
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
|
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
|
||||||
@@ -105,12 +101,7 @@ type (
|
|||||||
|
|
||||||
// EndpointGroupService represents a service for managing environment(endpoint) group data
|
// EndpointGroupService represents a service for managing environment(endpoint) group data
|
||||||
EndpointGroupService interface {
|
EndpointGroupService interface {
|
||||||
EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error)
|
BaseCRUD[portainer.EndpointGroup, portainer.EndpointGroupID]
|
||||||
EndpointGroups() ([]portainer.EndpointGroup, error)
|
|
||||||
Create(group *portainer.EndpointGroup) error
|
|
||||||
UpdateEndpointGroup(ID portainer.EndpointGroupID, group *portainer.EndpointGroup) error
|
|
||||||
DeleteEndpointGroup(ID portainer.EndpointGroupID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointRelationService represents a service for managing environment(endpoint) relations data
|
// EndpointRelationService represents a service for managing environment(endpoint) relations data
|
||||||
@@ -125,23 +116,14 @@ type (
|
|||||||
|
|
||||||
// FDOProfileService represents a service to manage FDO Profiles
|
// FDOProfileService represents a service to manage FDO Profiles
|
||||||
FDOProfileService interface {
|
FDOProfileService interface {
|
||||||
FDOProfiles() ([]portainer.FDOProfile, error)
|
BaseCRUD[portainer.FDOProfile, portainer.FDOProfileID]
|
||||||
FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error)
|
|
||||||
Create(FDOProfile *portainer.FDOProfile) error
|
|
||||||
Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error
|
|
||||||
Delete(ID portainer.FDOProfileID) error
|
|
||||||
GetNextIdentifier() int
|
GetNextIdentifier() int
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
|
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
|
||||||
HelmUserRepositoryService interface {
|
HelmUserRepositoryService interface {
|
||||||
HelmUserRepositories() ([]portainer.HelmUserRepository, error)
|
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
||||||
HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error)
|
HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error)
|
||||||
Create(record *portainer.HelmUserRepository) error
|
|
||||||
UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, repository *portainer.HelmUserRepository) error
|
|
||||||
DeleteHelmUserRepository(ID portainer.HelmUserRepositoryID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWTService represents a service for managing JWT tokens
|
// JWTService represents a service for managing JWT tokens
|
||||||
@@ -155,40 +137,23 @@ type (
|
|||||||
|
|
||||||
// RegistryService represents a service for managing registry data
|
// RegistryService represents a service for managing registry data
|
||||||
RegistryService interface {
|
RegistryService interface {
|
||||||
Registry(ID portainer.RegistryID) (*portainer.Registry, error)
|
BaseCRUD[portainer.Registry, portainer.RegistryID]
|
||||||
Registries() ([]portainer.Registry, error)
|
|
||||||
Create(registry *portainer.Registry) error
|
|
||||||
UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error
|
|
||||||
DeleteRegistry(ID portainer.RegistryID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControlService represents a service for managing resource control data
|
// ResourceControlService represents a service for managing resource control data
|
||||||
ResourceControlService interface {
|
ResourceControlService interface {
|
||||||
ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error)
|
BaseCRUD[portainer.ResourceControl, portainer.ResourceControlID]
|
||||||
ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error)
|
ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error)
|
||||||
ResourceControls() ([]portainer.ResourceControl, error)
|
|
||||||
Create(rc *portainer.ResourceControl) error
|
|
||||||
UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error
|
|
||||||
DeleteResourceControl(ID portainer.ResourceControlID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoleService represents a service for managing user roles
|
// RoleService represents a service for managing user roles
|
||||||
RoleService interface {
|
RoleService interface {
|
||||||
Role(ID portainer.RoleID) (*portainer.Role, error)
|
BaseCRUD[portainer.Role, portainer.RoleID]
|
||||||
Roles() ([]portainer.Role, error)
|
|
||||||
Create(role *portainer.Role) error
|
|
||||||
UpdateRole(ID portainer.RoleID, role *portainer.Role) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIKeyRepositoryService
|
// APIKeyRepositoryService
|
||||||
APIKeyRepository interface {
|
APIKeyRepository interface {
|
||||||
CreateAPIKey(key *portainer.APIKey) error
|
BaseCRUD[portainer.APIKey, portainer.APIKeyID]
|
||||||
GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error)
|
|
||||||
UpdateAPIKey(key *portainer.APIKey) error
|
|
||||||
DeleteAPIKey(ID portainer.APIKeyID) error
|
|
||||||
GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error)
|
GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error)
|
||||||
GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error)
|
GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error)
|
||||||
}
|
}
|
||||||
@@ -197,10 +162,13 @@ type (
|
|||||||
SettingsService interface {
|
SettingsService interface {
|
||||||
Settings() (*portainer.Settings, error)
|
Settings() (*portainer.Settings, error)
|
||||||
UpdateSettings(settings *portainer.Settings) error
|
UpdateSettings(settings *portainer.Settings) error
|
||||||
IsFeatureFlagEnabled(feature portainer.Feature) bool
|
|
||||||
BucketName() string
|
BucketName() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnapshotService interface {
|
||||||
|
BaseCRUD[portainer.Snapshot, portainer.EndpointID]
|
||||||
|
}
|
||||||
|
|
||||||
// SSLSettingsService represents a service for managing application settings
|
// SSLSettingsService represents a service for managing application settings
|
||||||
SSLSettingsService interface {
|
SSLSettingsService interface {
|
||||||
Settings() (*portainer.SSLSettings, error)
|
Settings() (*portainer.SSLSettings, error)
|
||||||
@@ -210,52 +178,34 @@ type (
|
|||||||
|
|
||||||
// StackService represents a service for managing stack data
|
// StackService represents a service for managing stack data
|
||||||
StackService interface {
|
StackService interface {
|
||||||
Stack(ID portainer.StackID) (*portainer.Stack, error)
|
BaseCRUD[portainer.Stack, portainer.StackID]
|
||||||
StackByName(name string) (*portainer.Stack, error)
|
StackByName(name string) (*portainer.Stack, error)
|
||||||
StacksByName(name string) ([]portainer.Stack, error)
|
StacksByName(name string) ([]portainer.Stack, error)
|
||||||
Stacks() ([]portainer.Stack, error)
|
|
||||||
Create(stack *portainer.Stack) error
|
|
||||||
UpdateStack(ID portainer.StackID, stack *portainer.Stack) error
|
|
||||||
DeleteStack(ID portainer.StackID) error
|
|
||||||
GetNextIdentifier() int
|
GetNextIdentifier() int
|
||||||
StackByWebhookID(ID string) (*portainer.Stack, error)
|
StackByWebhookID(ID string) (*portainer.Stack, error)
|
||||||
RefreshableStacks() ([]portainer.Stack, error)
|
RefreshableStacks() ([]portainer.Stack, error)
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagService represents a service for managing tag data
|
// TagService represents a service for managing tag data
|
||||||
TagService interface {
|
TagService interface {
|
||||||
Tags() ([]portainer.Tag, error)
|
BaseCRUD[portainer.Tag, portainer.TagID]
|
||||||
Tag(ID portainer.TagID) (*portainer.Tag, error)
|
UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error
|
||||||
Create(tag *portainer.Tag) error
|
|
||||||
UpdateTag(ID portainer.TagID, tag *portainer.Tag) error
|
|
||||||
DeleteTag(ID portainer.TagID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamService represents a service for managing user data
|
// TeamService represents a service for managing user data
|
||||||
TeamService interface {
|
TeamService interface {
|
||||||
Team(ID portainer.TeamID) (*portainer.Team, error)
|
BaseCRUD[portainer.Team, portainer.TeamID]
|
||||||
TeamByName(name string) (*portainer.Team, error)
|
TeamByName(name string) (*portainer.Team, error)
|
||||||
Teams() ([]portainer.Team, error)
|
|
||||||
Create(team *portainer.Team) error
|
|
||||||
UpdateTeam(ID portainer.TeamID, team *portainer.Team) error
|
|
||||||
DeleteTeam(ID portainer.TeamID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamMembershipService represents a service for managing team membership data
|
// TeamMembershipService represents a service for managing team membership data
|
||||||
TeamMembershipService interface {
|
TeamMembershipService interface {
|
||||||
TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error)
|
BaseCRUD[portainer.TeamMembership, portainer.TeamMembershipID]
|
||||||
TeamMemberships() ([]portainer.TeamMembership, error)
|
|
||||||
TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error)
|
TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error)
|
||||||
TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error)
|
TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error)
|
||||||
Create(membership *portainer.TeamMembership) error
|
|
||||||
UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error
|
|
||||||
DeleteTeamMembership(ID portainer.TeamMembershipID) error
|
|
||||||
DeleteTeamMembershipByUserID(userID portainer.UserID) error
|
DeleteTeamMembershipByUserID(userID portainer.UserID) error
|
||||||
DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error
|
DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error
|
||||||
BucketName() string
|
DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TunnelServerService represents a service for managing data associated to the tunnel server
|
// TunnelServerService represents a service for managing data associated to the tunnel server
|
||||||
@@ -267,39 +217,24 @@ type (
|
|||||||
|
|
||||||
// UserService represents a service for managing user data
|
// UserService represents a service for managing user data
|
||||||
UserService interface {
|
UserService interface {
|
||||||
User(ID portainer.UserID) (*portainer.User, error)
|
BaseCRUD[portainer.User, portainer.UserID]
|
||||||
UserByUsername(username string) (*portainer.User, error)
|
UserByUsername(username string) (*portainer.User, error)
|
||||||
Users() ([]portainer.User, error)
|
|
||||||
UsersByRole(role portainer.UserRole) ([]portainer.User, error)
|
UsersByRole(role portainer.UserRole) ([]portainer.User, error)
|
||||||
Create(user *portainer.User) error
|
|
||||||
UpdateUser(ID portainer.UserID, user *portainer.User) error
|
|
||||||
DeleteUser(ID portainer.UserID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VersionService represents a service for managing version data
|
// VersionService represents a service for managing version data
|
||||||
VersionService interface {
|
VersionService interface {
|
||||||
DBVersion() (int, error)
|
|
||||||
Edition() (portainer.SoftwareEdition, error)
|
|
||||||
InstanceID() (string, error)
|
InstanceID() (string, error)
|
||||||
StoreDBVersion(version int) error
|
UpdateInstanceID(ID string) error
|
||||||
StoreInstanceID(ID string) error
|
Edition() (portainer.SoftwareEdition, error)
|
||||||
BucketName() string
|
Version() (*models.Version, error)
|
||||||
|
UpdateVersion(*models.Version) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebhookService represents a service for managing webhook data.
|
// WebhookService represents a service for managing webhook data.
|
||||||
WebhookService interface {
|
WebhookService interface {
|
||||||
Webhooks() ([]portainer.Webhook, error)
|
BaseCRUD[portainer.Webhook, portainer.WebhookID]
|
||||||
Webhook(ID portainer.WebhookID) (*portainer.Webhook, error)
|
|
||||||
Create(portainer *portainer.Webhook) error
|
|
||||||
UpdateWebhook(ID portainer.WebhookID, webhook *portainer.Webhook) error
|
|
||||||
WebhookByResourceID(resourceID string) (*portainer.Webhook, error)
|
WebhookByResourceID(resourceID string) (*portainer.Webhook, error)
|
||||||
WebhookByToken(token string) (*portainer.Webhook, error)
|
WebhookByToken(token string) (*portainer.Webhook, error)
|
||||||
DeleteWebhook(ID portainer.WebhookID) error
|
|
||||||
BucketName() string
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsErrObjectNotFound(e error) bool {
|
|
||||||
return e == errors.ErrObjectNotFound
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "registries"
|
const BucketName = "registries"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) data.
|
// Service represents a service for managing environment(endpoint) data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.Registry, portainer.RegistryID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -29,46 +21,26 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.Registry, portainer.RegistryID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registry returns an registry by ID.
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
return ServiceTx{
|
||||||
var registry portainer.Registry
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Registry, portainer.RegistryID]{
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
Bucket: BucketName,
|
||||||
|
Connection: service.Connection,
|
||||||
err := service.connection.GetObject(BucketName, identifier, ®istry)
|
Tx: tx,
|
||||||
if err != nil {
|
},
|
||||||
return nil, err
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ®istry, nil
|
// Create creates a new registry.
|
||||||
}
|
|
||||||
|
|
||||||
// Registries returns an array containing all the registries.
|
|
||||||
func (service *Service) Registries() ([]portainer.Registry, error) {
|
|
||||||
var registries = make([]portainer.Registry, 0)
|
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
|
||||||
&portainer.Registry{},
|
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
registry, ok := obj.(*portainer.Registry)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Registry object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to Registry object: %s", obj)
|
|
||||||
}
|
|
||||||
registries = append(registries, *registry)
|
|
||||||
return &portainer.Registry{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return registries, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRegistry creates a new registry.
|
|
||||||
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, interface{}) {
|
||||||
registry.ID = portainer.RegistryID(id)
|
registry.ID = portainer.RegistryID(id)
|
||||||
@@ -76,15 +48,3 @@ func (service *Service) Create(registry *portainer.Registry) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRegistry updates an registry.
|
|
||||||
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, registry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteRegistry deletes an registry.
|
|
||||||
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
|
||||||
|
|||||||
21
api/dataservices/registry/tx.go
Normal file
21
api/dataservices/registry/tx.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
dataservices.BaseDataServiceTx[portainer.Registry, portainer.RegistryID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new registry.
|
||||||
|
func (service ServiceTx) Create(registry *portainer.Registry) error {
|
||||||
|
return service.Tx.CreateObject(
|
||||||
|
BucketName,
|
||||||
|
func(id uint64) (int, interface{}) {
|
||||||
|
registry.ID = portainer.RegistryID(id)
|
||||||
|
return int(registry.ID), registry
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,21 @@
|
|||||||
package resourcecontrol
|
package resourcecontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "resource_control"
|
const BucketName = "resource_control"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) data.
|
// Service represents a service for managing environment(endpoint) data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.ResourceControl, portainer.ResourceControlID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -29,21 +26,21 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.ResourceControl, portainer.ResourceControlID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControl returns a ResourceControl object by ID
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
|
return ServiceTx{
|
||||||
var resourceControl portainer.ResourceControl
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.ResourceControl, portainer.ResourceControlID]{
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
Bucket: BucketName,
|
||||||
|
Connection: service.Connection,
|
||||||
err := service.connection.GetObject(BucketName, identifier, &resourceControl)
|
Tx: tx,
|
||||||
if err != nil {
|
},
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &resourceControl, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||||
@@ -52,14 +49,14 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
|
|||||||
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||||
var resourceControl *portainer.ResourceControl
|
var resourceControl *portainer.ResourceControl
|
||||||
stop := fmt.Errorf("ok")
|
stop := fmt.Errorf("ok")
|
||||||
err := service.connection.GetAll(
|
err := service.Connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.ResourceControl{},
|
&portainer.ResourceControl{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
func(obj interface{}) (interface{}, error) {
|
||||||
rc, ok := obj.(*portainer.ResourceControl)
|
rc, ok := obj.(*portainer.ResourceControl)
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to ResourceControl object")
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||||
return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
|
return nil, fmt.Errorf("failed to convert to ResourceControl object: %s", obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||||
@@ -73,38 +70,19 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
|
|||||||
return nil, stop
|
return nil, stop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &portainer.ResourceControl{}, nil
|
return &portainer.ResourceControl{}, nil
|
||||||
})
|
})
|
||||||
if err == stop {
|
if errors.Is(err, stop) {
|
||||||
return resourceControl, nil
|
return resourceControl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControls returns all the ResourceControl objects
|
|
||||||
func (service *Service) ResourceControls() ([]portainer.ResourceControl, error) {
|
|
||||||
var rcs = make([]portainer.ResourceControl, 0)
|
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
|
||||||
&portainer.ResourceControl{},
|
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
rc, ok := obj.(*portainer.ResourceControl)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to ResourceControl object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
|
|
||||||
}
|
|
||||||
rcs = append(rcs, *rc)
|
|
||||||
return &portainer.ResourceControl{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return rcs, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateResourceControl creates a new ResourceControl object
|
// CreateResourceControl creates a new ResourceControl object
|
||||||
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, interface{}) {
|
||||||
resourceControl.ID = portainer.ResourceControlID(id)
|
resourceControl.ID = portainer.ResourceControlID(id)
|
||||||
@@ -112,15 +90,3 @@ func (service *Service) Create(resourceControl *portainer.ResourceControl) error
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateResourceControl saves a ResourceControl object.
|
|
||||||
func (service *Service) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, resourceControl)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteResourceControl deletes a ResourceControl object by ID
|
|
||||||
func (service *Service) DeleteResourceControl(ID portainer.ResourceControlID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
|
||||||
|
|||||||
63
api/dataservices/resourcecontrol/tx.go
Normal file
63
api/dataservices/resourcecontrol/tx.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package resourcecontrol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
dataservices.BaseDataServiceTx[portainer.ResourceControl, portainer.ResourceControlID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||||
|
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
|
||||||
|
// if no ResourceControl was found.
|
||||||
|
func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||||
|
var resourceControl *portainer.ResourceControl
|
||||||
|
stop := fmt.Errorf("ok")
|
||||||
|
err := service.Tx.GetAll(
|
||||||
|
BucketName,
|
||||||
|
&portainer.ResourceControl{},
|
||||||
|
func(obj interface{}) (interface{}, error) {
|
||||||
|
rc, ok := obj.(*portainer.ResourceControl)
|
||||||
|
if !ok {
|
||||||
|
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
|
||||||
|
return nil, fmt.Errorf("failed to convert to ResourceControl object: %s", obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||||
|
resourceControl = rc
|
||||||
|
return nil, stop
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, subResourceID := range rc.SubResourceIDs {
|
||||||
|
if subResourceID == resourceID {
|
||||||
|
resourceControl = rc
|
||||||
|
return nil, stop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &portainer.ResourceControl{}, nil
|
||||||
|
})
|
||||||
|
if errors.Is(err, stop) {
|
||||||
|
return resourceControl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateResourceControl creates a new ResourceControl object
|
||||||
|
func (service ServiceTx) Create(resourceControl *portainer.ResourceControl) error {
|
||||||
|
return service.Tx.CreateObject(
|
||||||
|
BucketName,
|
||||||
|
func(id uint64) (int, interface{}) {
|
||||||
|
resourceControl.ID = portainer.ResourceControlID(id)
|
||||||
|
return int(resourceControl.ID), resourceControl
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
package role
|
package role
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "roles"
|
const BucketName = "roles"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) data.
|
// Service represents a service for managing environment(endpoint) data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.Role, portainer.RoleID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -29,46 +21,26 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.Role, portainer.RoleID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Role returns a Role by ID
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
|
return ServiceTx{
|
||||||
var set portainer.Role
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Role, portainer.RoleID]{
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
Bucket: BucketName,
|
||||||
|
Connection: service.Connection,
|
||||||
err := service.connection.GetObject(BucketName, identifier, &set)
|
Tx: tx,
|
||||||
if err != nil {
|
},
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &set, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roles return an array containing all the sets.
|
|
||||||
func (service *Service) Roles() ([]portainer.Role, error) {
|
|
||||||
var sets = make([]portainer.Role, 0)
|
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
|
||||||
&portainer.Role{},
|
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
set, ok := obj.(*portainer.Role)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Role object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to Role object: %s", obj)
|
|
||||||
}
|
|
||||||
sets = append(sets, *set)
|
|
||||||
return &portainer.Role{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return sets, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRole creates a new Role.
|
// CreateRole creates a new Role.
|
||||||
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, interface{}) {
|
||||||
role.ID = portainer.RoleID(id)
|
role.ID = portainer.RoleID(id)
|
||||||
@@ -76,9 +48,3 @@ func (service *Service) Create(role *portainer.Role) error {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRole updates a role.
|
|
||||||
func (service *Service) UpdateRole(ID portainer.RoleID, role *portainer.Role) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, role)
|
|
||||||
}
|
|
||||||
|
|||||||
21
api/dataservices/role/tx.go
Normal file
21
api/dataservices/role/tx.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package role
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
dataservices.BaseDataServiceTx[portainer.Role, portainer.RoleID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRole creates a new Role.
|
||||||
|
func (service ServiceTx) Create(role *portainer.Role) error {
|
||||||
|
return service.Tx.CreateObject(
|
||||||
|
BucketName,
|
||||||
|
func(id uint64) (int, interface{}) {
|
||||||
|
role.ID = portainer.RoleID(id)
|
||||||
|
return int(role.ID), role
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
package schedule
|
package schedule
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "schedules"
|
const BucketName = "schedules"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing schedule data.
|
// Service represents a service for managing schedule data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
@@ -62,20 +58,11 @@ func (service *Service) DeleteSchedule(ID portainer.ScheduleID) error {
|
|||||||
func (service *Service) Schedules() ([]portainer.Schedule, error) {
|
func (service *Service) Schedules() ([]portainer.Schedule, error) {
|
||||||
var schedules = make([]portainer.Schedule, 0)
|
var schedules = make([]portainer.Schedule, 0)
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
return schedules, service.connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.Schedule{},
|
&portainer.Schedule{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.AppendFn(&schedules),
|
||||||
schedule, ok := obj.(*portainer.Schedule)
|
)
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Schedule object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to Schedule object: %s", obj)
|
|
||||||
}
|
|
||||||
schedules = append(schedules, *schedule)
|
|
||||||
return &portainer.Schedule{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return schedules, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SchedulesByJobType return a array containing all the schedules
|
// SchedulesByJobType return a array containing all the schedules
|
||||||
@@ -83,27 +70,18 @@ func (service *Service) Schedules() ([]portainer.Schedule, error) {
|
|||||||
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
|
func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portainer.Schedule, error) {
|
||||||
var schedules = make([]portainer.Schedule, 0)
|
var schedules = make([]portainer.Schedule, 0)
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
return schedules, service.connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.Schedule{},
|
&portainer.Schedule{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.FilterFn(&schedules, func(e portainer.Schedule) bool {
|
||||||
schedule, ok := obj.(*portainer.Schedule)
|
return e.JobType == jobType
|
||||||
if !ok {
|
}),
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Schedule object")
|
)
|
||||||
return nil, fmt.Errorf("Failed to convert to Schedule object: %s", obj)
|
|
||||||
}
|
|
||||||
if schedule.JobType == jobType {
|
|
||||||
schedules = append(schedules, *schedule)
|
|
||||||
}
|
|
||||||
return &portainer.Schedule{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return schedules, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create assign an ID to a new schedule and saves it.
|
// Create assign an ID to a new schedule and saves it.
|
||||||
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
|
func (service *Service) CreateSchedule(schedule *portainer.Schedule) error {
|
||||||
return service.connection.CreateObjectWithSetSequence(BucketName, int(schedule.ID), schedule)
|
return service.connection.CreateObjectWithId(BucketName, int(schedule.ID), schedule)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextIdentifier returns the next identifier for a schedule.
|
// GetNextIdentifier returns the next identifier for a schedule.
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
|
return ServiceTx{
|
||||||
|
service: service,
|
||||||
|
tx: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Settings retrieve the settings object.
|
// Settings retrieve the settings object.
|
||||||
func (service *Service) Settings() (*portainer.Settings, error) {
|
func (service *Service) Settings() (*portainer.Settings, error) {
|
||||||
var settings portainer.Settings
|
var settings portainer.Settings
|
||||||
@@ -47,17 +54,3 @@ func (service *Service) Settings() (*portainer.Settings, error) {
|
|||||||
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
|
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
|
||||||
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
|
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
|
|
||||||
settings, err := service.Settings()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
featureFlagSetting, ok := settings.FeatureFlagSettings[feature]
|
|
||||||
if ok {
|
|
||||||
return featureFlagSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
31
api/dataservices/settings/tx.go
Normal file
31
api/dataservices/settings/tx.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
service *Service
|
||||||
|
tx portainer.Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) BucketName() string {
|
||||||
|
return BucketName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings retrieve the settings object.
|
||||||
|
func (service ServiceTx) Settings() (*portainer.Settings, error) {
|
||||||
|
var settings portainer.Settings
|
||||||
|
|
||||||
|
err := service.tx.GetObject(BucketName, []byte(settingsKey), &settings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSettings persists a Settings object.
|
||||||
|
func (service ServiceTx) UpdateSettings(settings *portainer.Settings) error {
|
||||||
|
return service.tx.UpdateObject(BucketName, []byte(settingsKey), settings)
|
||||||
|
}
|
||||||
42
api/dataservices/snapshot/snapshot.go
Normal file
42
api/dataservices/snapshot/snapshot.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package snapshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BucketName = "snapshots"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
dataservices.BaseDataService[portainer.Snapshot, portainer.EndpointID]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(connection portainer.Connection) (*Service, error) {
|
||||||
|
err := connection.SetServiceName(BucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Service{
|
||||||
|
BaseDataService: dataservices.BaseDataService[portainer.Snapshot, portainer.EndpointID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
|
return ServiceTx{
|
||||||
|
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Snapshot, portainer.EndpointID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: service.Connection,
|
||||||
|
Tx: tx,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) Create(snapshot *portainer.Snapshot) error {
|
||||||
|
return service.Connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||||
|
}
|
||||||
14
api/dataservices/snapshot/tx.go
Normal file
14
api/dataservices/snapshot/tx.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package snapshot
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceTx struct {
|
||||||
|
dataservices.BaseDataServiceTx[portainer.Snapshot, portainer.EndpointID]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) Create(snapshot *portainer.Snapshot) error {
|
||||||
|
return service.Tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
|
||||||
|
}
|
||||||
@@ -1,27 +1,20 @@
|
|||||||
package stack
|
package stack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices/errors"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
dserrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
BucketName = "stacks"
|
const BucketName = "stacks"
|
||||||
)
|
|
||||||
|
|
||||||
// Service represents a service for managing environment(endpoint) data.
|
// Service represents a service for managing environment(endpoint) data.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
connection portainer.Connection
|
dataservices.BaseDataService[portainer.Stack, portainer.StackID]
|
||||||
}
|
|
||||||
|
|
||||||
func (service *Service) BucketName() string {
|
|
||||||
return BucketName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService creates a new instance of a service.
|
// NewService creates a new instance of a service.
|
||||||
@@ -32,48 +25,37 @@ func NewService(connection portainer.Connection) (*Service, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &Service{
|
return &Service{
|
||||||
connection: connection,
|
BaseDataService: dataservices.BaseDataService[portainer.Stack, portainer.StackID]{
|
||||||
|
Bucket: BucketName,
|
||||||
|
Connection: connection,
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stack returns a stack object by ID.
|
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
|
||||||
func (service *Service) Stack(ID portainer.StackID) (*portainer.Stack, error) {
|
return ServiceTx{
|
||||||
var stack portainer.Stack
|
BaseDataServiceTx: service.BaseDataService.Tx(tx),
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
|
|
||||||
err := service.connection.GetObject(BucketName, identifier, &stack)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &stack, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackByName returns a stack object by name.
|
// StackByName returns a stack object by name.
|
||||||
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
|
func (service *Service) StackByName(name string) (*portainer.Stack, error) {
|
||||||
var s *portainer.Stack
|
var s portainer.Stack
|
||||||
|
|
||||||
stop := fmt.Errorf("ok")
|
err := service.Connection.GetAll(
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.Stack{},
|
&portainer.Stack{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.FirstFn(&s, func(e portainer.Stack) bool {
|
||||||
stack, ok := obj.(*portainer.Stack)
|
return e.Name == name
|
||||||
if !ok {
|
}),
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
|
)
|
||||||
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
|
|
||||||
}
|
if errors.Is(err, dataservices.ErrStop) {
|
||||||
if stack.Name == name {
|
return &s, nil
|
||||||
s = stack
|
|
||||||
return nil, stop
|
|
||||||
}
|
|
||||||
return &portainer.Stack{}, nil
|
|
||||||
})
|
|
||||||
if err == stop {
|
|
||||||
return s, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil, errors.ErrObjectNotFound
|
return nil, dserrors.ErrObjectNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -83,93 +65,44 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
|
|||||||
func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
|
func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
|
||||||
var stacks = make([]portainer.Stack, 0)
|
var stacks = make([]portainer.Stack, 0)
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
return stacks, service.Connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.Stack{},
|
&portainer.Stack{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.FilterFn(&stacks, func(e portainer.Stack) bool {
|
||||||
stack, ok := obj.(portainer.Stack)
|
return e.Name == name
|
||||||
if !ok {
|
}),
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
|
)
|
||||||
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
|
|
||||||
}
|
|
||||||
if stack.Name == name {
|
|
||||||
stacks = append(stacks, stack)
|
|
||||||
}
|
|
||||||
return &portainer.Stack{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return stacks, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stacks returns an array containing all the stacks.
|
|
||||||
func (service *Service) Stacks() ([]portainer.Stack, error) {
|
|
||||||
var stacks = make([]portainer.Stack, 0)
|
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
|
||||||
BucketName,
|
|
||||||
&portainer.Stack{},
|
|
||||||
func(obj interface{}) (interface{}, error) {
|
|
||||||
stack, ok := obj.(*portainer.Stack)
|
|
||||||
if !ok {
|
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
|
|
||||||
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
|
|
||||||
}
|
|
||||||
stacks = append(stacks, *stack)
|
|
||||||
return &portainer.Stack{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return stacks, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextIdentifier returns the next identifier for a stack.
|
// GetNextIdentifier returns the next identifier for a stack.
|
||||||
func (service *Service) GetNextIdentifier() int {
|
func (service *Service) GetNextIdentifier() int {
|
||||||
return service.connection.GetNextIdentifier(BucketName)
|
return service.Connection.GetNextIdentifier(BucketName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateStack creates a new stack.
|
// CreateStack creates a new stack.
|
||||||
func (service *Service) Create(stack *portainer.Stack) error {
|
func (service *Service) Create(stack *portainer.Stack) error {
|
||||||
return service.connection.CreateObjectWithSetSequence(BucketName, int(stack.ID), stack)
|
return service.Connection.CreateObjectWithId(BucketName, int(stack.ID), stack)
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateStack updates a stack.
|
|
||||||
func (service *Service) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.UpdateObject(BucketName, identifier, stack)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteStack deletes a stack.
|
|
||||||
func (service *Service) DeleteStack(ID portainer.StackID) error {
|
|
||||||
identifier := service.connection.ConvertToKey(int(ID))
|
|
||||||
return service.connection.DeleteObject(BucketName, identifier)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackByWebhookID returns a pointer to a stack object by webhook ID.
|
// StackByWebhookID returns a pointer to a stack object by webhook ID.
|
||||||
// It returns nil, errors.ErrObjectNotFound if there's no stack associated with the webhook ID.
|
// It returns nil, errors.ErrObjectNotFound if there's no stack associated with the webhook ID.
|
||||||
func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
|
func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
|
||||||
var s *portainer.Stack
|
var s portainer.Stack
|
||||||
stop := fmt.Errorf("ok")
|
|
||||||
err := service.connection.GetAll(
|
err := service.Connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.Stack{},
|
&portainer.Stack{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.FirstFn(&s, func(e portainer.Stack) bool {
|
||||||
var ok bool
|
return e.AutoUpdate != nil && strings.EqualFold(e.AutoUpdate.Webhook, id)
|
||||||
s, ok = obj.(*portainer.Stack)
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
if !ok {
|
if errors.Is(err, dataservices.ErrStop) {
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
|
return &s, nil
|
||||||
return &portainer.Stack{}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.AutoUpdate != nil && strings.EqualFold(s.AutoUpdate.Webhook, id) {
|
|
||||||
return nil, stop
|
|
||||||
}
|
|
||||||
return &portainer.Stack{}, nil
|
|
||||||
})
|
|
||||||
if err == stop {
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil, errors.ErrObjectNotFound
|
return nil, dserrors.ErrObjectNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -180,20 +113,11 @@ func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
|
|||||||
func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
|
func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
|
||||||
stacks := make([]portainer.Stack, 0)
|
stacks := make([]portainer.Stack, 0)
|
||||||
|
|
||||||
err := service.connection.GetAll(
|
return stacks, service.Connection.GetAll(
|
||||||
BucketName,
|
BucketName,
|
||||||
&portainer.Stack{},
|
&portainer.Stack{},
|
||||||
func(obj interface{}) (interface{}, error) {
|
dataservices.FilterFn(&stacks, func(e portainer.Stack) bool {
|
||||||
stack, ok := obj.(*portainer.Stack)
|
return e.AutoUpdate != nil && e.AutoUpdate.Interval != ""
|
||||||
if !ok {
|
}),
|
||||||
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
|
)
|
||||||
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
|
|
||||||
}
|
|
||||||
if stack.AutoUpdate != nil && stack.AutoUpdate.Interval != "" {
|
|
||||||
stacks = append(stacks, *stack)
|
|
||||||
}
|
|
||||||
return &portainer.Stack{}, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return stacks, err
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user